fui-studio-mcp 0.1.0 → 0.1.2
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 +17 -6
- package/dist/{chunk-W7L36Z34.js → chunk-S3AUIOGZ.js} +2187 -253
- package/dist/{chunk-RAHWGTHJ.js → chunk-U3VXQSCB.js} +1 -1
- package/dist/{dist-3URVYEPT.js → dist-LUAYBAOC.js} +1 -1
- package/dist/index.js +263 -20
- package/dist/{node-4RLELQ7B.js → node-XJJEAAN7.js} +2 -2
- package/dist/runtime.global.js +2407 -2249
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
allTtfBuffers,
|
|
5
5
|
fontDefs,
|
|
6
6
|
nodeOutliner
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-U3VXQSCB.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_FONT_FAMILY,
|
|
10
10
|
Keyframe,
|
|
11
11
|
OgrafControl,
|
|
12
|
+
PRESETS,
|
|
12
13
|
Param,
|
|
13
14
|
ROOT_TYPES,
|
|
14
15
|
adoptions,
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
wouldCycle,
|
|
39
40
|
yuvFrameBytes,
|
|
40
41
|
yuvToRgb
|
|
41
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-S3AUIOGZ.js";
|
|
42
43
|
|
|
43
44
|
// src/index.ts
|
|
44
45
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -46,6 +47,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
46
47
|
import { z as z2 } from "zod";
|
|
47
48
|
import { createRequire as createRequire3 } from "module";
|
|
48
49
|
import { homedir } from "os";
|
|
50
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
49
51
|
|
|
50
52
|
// src/session.ts
|
|
51
53
|
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync, writeFileSync, mkdirSync } from "fs";
|
|
@@ -755,6 +757,14 @@ var Session = class {
|
|
|
755
757
|
* come from that folder instead.
|
|
756
758
|
*/
|
|
757
759
|
held = /* @__PURE__ */ new Map();
|
|
760
|
+
/**
|
|
761
|
+
* Nodes added this session without a position. A save lays them out —
|
|
762
|
+
* columns by wire depth, as the studio's L does — so the graph reads in
|
|
763
|
+
* the studio instead of arriving as one endless row. Nodes with a
|
|
764
|
+
* position of their own (the model's, or the person's in a file that
|
|
765
|
+
* was opened) never move.
|
|
766
|
+
*/
|
|
767
|
+
placed = /* @__PURE__ */ new Set();
|
|
758
768
|
bumpIds() {
|
|
759
769
|
for (const n of this.project.graph.nodes) {
|
|
760
770
|
const m = /(\d+)$/.exec(n.id);
|
|
@@ -770,6 +780,19 @@ var Session = class {
|
|
|
770
780
|
this.path = void 0;
|
|
771
781
|
this.stamp = void 0;
|
|
772
782
|
this.held.clear();
|
|
783
|
+
this.placed.clear();
|
|
784
|
+
return this.project;
|
|
785
|
+
}
|
|
786
|
+
/** A preset's document as the open project — a clean start, like newProject. */
|
|
787
|
+
newFromPreset(name) {
|
|
788
|
+
const preset = PRESETS.find((p) => p.name === name);
|
|
789
|
+
if (!preset) fail(`No preset "${name}". The presets: ${PRESETS.map((p) => p.name).join(", ")}.`);
|
|
790
|
+
this.project = preset.build();
|
|
791
|
+
this.path = void 0;
|
|
792
|
+
this.stamp = void 0;
|
|
793
|
+
this.held.clear();
|
|
794
|
+
this.placed.clear();
|
|
795
|
+
this.bumpIds();
|
|
773
796
|
return this.project;
|
|
774
797
|
}
|
|
775
798
|
open(path) {
|
|
@@ -781,6 +804,7 @@ var Session = class {
|
|
|
781
804
|
this.path = abs;
|
|
782
805
|
this.stamp = stamp;
|
|
783
806
|
this.held.clear();
|
|
807
|
+
this.placed.clear();
|
|
784
808
|
this.bumpIds();
|
|
785
809
|
return { project, warnings };
|
|
786
810
|
}
|
|
@@ -802,6 +826,7 @@ var Session = class {
|
|
|
802
826
|
);
|
|
803
827
|
}
|
|
804
828
|
}
|
|
829
|
+
this.arrange();
|
|
805
830
|
mkdirSync(dirname2(target), { recursive: true });
|
|
806
831
|
this.writeHeldAssets(target);
|
|
807
832
|
writeFileSync(target, serializeProject(this.project));
|
|
@@ -923,6 +948,7 @@ var Session = class {
|
|
|
923
948
|
}
|
|
924
949
|
}
|
|
925
950
|
this.project.graph.nodes.push(node);
|
|
951
|
+
if (!options.position) this.placed.add(id);
|
|
926
952
|
return node;
|
|
927
953
|
}
|
|
928
954
|
/**
|
|
@@ -943,9 +969,47 @@ var Session = class {
|
|
|
943
969
|
const last = nodes[nodes.length - 1];
|
|
944
970
|
return last ? { x: last.position.x + 260, y: last.position.y } : { x: 0, y: 0 };
|
|
945
971
|
}
|
|
972
|
+
/**
|
|
973
|
+
* Lay out the nodes this session placed, page by page (the root, and each
|
|
974
|
+
* Layer's own). A page of nothing but such nodes is arranged whole, every
|
|
975
|
+
* save, so it stays tidy as it grows; on a page that also holds nodes with
|
|
976
|
+
* positions of their own, the new ones are arranged among themselves and
|
|
977
|
+
* set below what is already there.
|
|
978
|
+
*/
|
|
979
|
+
arrange() {
|
|
980
|
+
const pages = /* @__PURE__ */ new Map();
|
|
981
|
+
for (const n of this.project.graph.nodes) {
|
|
982
|
+
const key = n.layer ?? "";
|
|
983
|
+
const list = pages.get(key);
|
|
984
|
+
if (list) list.push(n);
|
|
985
|
+
else pages.set(key, [n]);
|
|
986
|
+
}
|
|
987
|
+
for (const nodes of pages.values()) {
|
|
988
|
+
const moving = nodes.filter((n) => this.placed.has(n.id));
|
|
989
|
+
if (moving.length === 0) continue;
|
|
990
|
+
const staying = nodes.filter((n) => !this.placed.has(n.id));
|
|
991
|
+
const ids = new Set(moving.map((n) => n.id));
|
|
992
|
+
const edges = this.project.graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to));
|
|
993
|
+
const origin = staying.length === 0 ? { x: 0, y: 0 } : {
|
|
994
|
+
x: Math.min(...staying.map((n) => n.position.x)),
|
|
995
|
+
y: Math.max(...staying.map((n) => n.position.y + nodeSize(n, registry.forNode(n)).height)) + 120
|
|
996
|
+
};
|
|
997
|
+
const at = columnLayout(moving, edges, origin);
|
|
998
|
+
for (const n of moving) n.position = at.get(n.id) ?? n.position;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Root-page nodes that draw straight onto the page because nothing takes
|
|
1003
|
+
* their shapes — valid, but in the studio they read as forgotten wires.
|
|
1004
|
+
*/
|
|
1005
|
+
looseDrawers() {
|
|
1006
|
+
const feeding = new Set(this.project.graph.edges.map((e) => e.from));
|
|
1007
|
+
return this.project.graph.nodes.filter((n) => n.layer === void 0 && !feeding.has(n.id)).filter((n) => registry.forNode(n)?.outputs.some((o) => o.type === "shapes")).map((n) => n.id);
|
|
1008
|
+
}
|
|
946
1009
|
removeNode(id) {
|
|
947
1010
|
this.node(id);
|
|
948
1011
|
this.project.graph.nodes = this.project.graph.nodes.filter((n) => n.id !== id);
|
|
1012
|
+
this.placed.delete(id);
|
|
949
1013
|
this.project.graph.edges = this.project.graph.edges.filter((e) => e.from !== id && e.to !== id);
|
|
950
1014
|
}
|
|
951
1015
|
setParam(nodeId, key, value) {
|
|
@@ -987,7 +1051,12 @@ var Session = class {
|
|
|
987
1051
|
}
|
|
988
1052
|
if (paramDef && !isExposable(paramDef)) fail(`${toNode.type}.${toPort} is not wireable.`);
|
|
989
1053
|
if (wouldCycle(this.project.graph, from, to, fromPort, toPort)) fail(`Connecting ${from} \u2192 ${to} would make a loop.`);
|
|
990
|
-
|
|
1054
|
+
if (toDef.inputs.find((i) => i.name === toPort)?.multi) {
|
|
1055
|
+
const same = this.project.graph.edges.find((e) => e.from === from && e.fromPort === fromPort && e.to === to && e.toPort === toPort);
|
|
1056
|
+
if (same) return same;
|
|
1057
|
+
} else {
|
|
1058
|
+
this.project.graph.edges = this.project.graph.edges.filter((e) => !(e.to === to && e.toPort === toPort));
|
|
1059
|
+
}
|
|
991
1060
|
const edge = { id: makeId("edge"), from, fromPort, to, toPort };
|
|
992
1061
|
this.project.graph.edges.push(edge);
|
|
993
1062
|
const outType = fromDef.outputs.find((o) => o.name === fromPort)?.type;
|
|
@@ -997,10 +1066,13 @@ var Session = class {
|
|
|
997
1066
|
}
|
|
998
1067
|
return edge;
|
|
999
1068
|
}
|
|
1000
|
-
|
|
1069
|
+
/** Remove the wires into a socket — only the one from `from`, when given (a Merge's `in` holds several). */
|
|
1070
|
+
disconnect(to, toPort, from) {
|
|
1001
1071
|
const before = this.project.graph.edges.length;
|
|
1002
|
-
this.project.graph.edges = this.project.graph.edges.filter(
|
|
1003
|
-
|
|
1072
|
+
this.project.graph.edges = this.project.graph.edges.filter(
|
|
1073
|
+
(e) => !(e.to === to && e.toPort === toPort && (from === void 0 || e.from === from))
|
|
1074
|
+
);
|
|
1075
|
+
if (this.project.graph.edges.length === before) fail(`Nothing${from ? ` from ${from}` : ""} is wired into ${to}.${toPort}.`);
|
|
1004
1076
|
}
|
|
1005
1077
|
/**
|
|
1006
1078
|
* Keyframes write into the same param model everything else uses — a track
|
|
@@ -1140,12 +1212,113 @@ var Session = class {
|
|
|
1140
1212
|
return svgToPng(await this.renderSvgString(t, false), { scale });
|
|
1141
1213
|
}
|
|
1142
1214
|
};
|
|
1215
|
+
function nodeSize(node, def) {
|
|
1216
|
+
if (node.micro) return { width: 80, height: 24 };
|
|
1217
|
+
const width = def?.wide ? 456 : 228;
|
|
1218
|
+
const outputs = def?.outputs.filter((o) => !o.hideWhen?.(node.params ?? {})).length ?? 0;
|
|
1219
|
+
const height = node.collapsed ? 44 : 66 + Math.max(def?.inputs.length ?? 0, outputs) * 20 + (def?.params.length ?? 0) * 24;
|
|
1220
|
+
return { width, height };
|
|
1221
|
+
}
|
|
1222
|
+
var COLUMN_HEIGHT = 1600;
|
|
1223
|
+
var ROW_GAP = 28;
|
|
1224
|
+
var COLUMN_GAP = 70;
|
|
1225
|
+
var BLOCK_GAP = 160;
|
|
1226
|
+
function columnLayout(nodes, edges, origin) {
|
|
1227
|
+
const root = new Map(nodes.map((n) => [n.id, n.id]));
|
|
1228
|
+
const find = (id) => {
|
|
1229
|
+
let r = id;
|
|
1230
|
+
while (root.get(r) !== r) r = root.get(r);
|
|
1231
|
+
root.set(id, r);
|
|
1232
|
+
return r;
|
|
1233
|
+
};
|
|
1234
|
+
for (const e of edges) if (root.has(e.from) && root.has(e.to)) root.set(find(e.from), find(e.to));
|
|
1235
|
+
const wired = new Set(edges.flatMap((e) => [e.from, e.to]));
|
|
1236
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
1237
|
+
const loose = [];
|
|
1238
|
+
for (const n of nodes) {
|
|
1239
|
+
if (!wired.has(n.id)) {
|
|
1240
|
+
loose.push(n);
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
const key = find(n.id);
|
|
1244
|
+
const list = blocks.get(key);
|
|
1245
|
+
if (list) list.push(n);
|
|
1246
|
+
else blocks.set(key, [n]);
|
|
1247
|
+
}
|
|
1248
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1249
|
+
let y = origin.y;
|
|
1250
|
+
for (const block of [...blocks.values(), ...loose.length ? [loose] : []]) {
|
|
1251
|
+
const bottom = layoutBlock(block, edges, { x: origin.x, y }, placed);
|
|
1252
|
+
y = bottom + BLOCK_GAP;
|
|
1253
|
+
}
|
|
1254
|
+
return placed;
|
|
1255
|
+
}
|
|
1256
|
+
function layoutBlock(nodes, allEdges, origin, placed) {
|
|
1257
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
1258
|
+
const edges = allEdges.filter((e) => ids.has(e.from) && ids.has(e.to));
|
|
1259
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
1260
|
+
for (const e of edges) {
|
|
1261
|
+
const list = incoming.get(e.to);
|
|
1262
|
+
if (list) list.push(e.from);
|
|
1263
|
+
else incoming.set(e.to, [e.from]);
|
|
1264
|
+
}
|
|
1265
|
+
const colOf = /* @__PURE__ */ new Map();
|
|
1266
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1267
|
+
const col = (id) => {
|
|
1268
|
+
const known = colOf.get(id);
|
|
1269
|
+
if (known !== void 0) return known;
|
|
1270
|
+
if (visiting.has(id)) return 0;
|
|
1271
|
+
visiting.add(id);
|
|
1272
|
+
const ins = incoming.get(id) ?? [];
|
|
1273
|
+
const c = ins.length === 0 ? 0 : Math.max(...ins.map(col)) + 1;
|
|
1274
|
+
visiting.delete(id);
|
|
1275
|
+
colOf.set(id, c);
|
|
1276
|
+
return c;
|
|
1277
|
+
};
|
|
1278
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1279
|
+
nodes.forEach((n) => {
|
|
1280
|
+
const c = col(n.id);
|
|
1281
|
+
const list = columns.get(c);
|
|
1282
|
+
if (list) list.push(n);
|
|
1283
|
+
else columns.set(c, [n]);
|
|
1284
|
+
});
|
|
1285
|
+
const order = new Map(nodes.map((n, i) => [n.id, i]));
|
|
1286
|
+
const rowOf = /* @__PURE__ */ new Map();
|
|
1287
|
+
let x = origin.x;
|
|
1288
|
+
let bottom = origin.y;
|
|
1289
|
+
for (const c of [...columns.keys()].sort((a, b) => a - b)) {
|
|
1290
|
+
const list = columns.get(c);
|
|
1291
|
+
const key = (n) => {
|
|
1292
|
+
const ins = (incoming.get(n.id) ?? []).map((id) => rowOf.get(id) ?? 0);
|
|
1293
|
+
return ins.length > 0 ? ins.reduce((sum, v) => sum + v, 0) / ins.length : -1;
|
|
1294
|
+
};
|
|
1295
|
+
list.sort((a, b) => key(a) - key(b) || order.get(a.id) - order.get(b.id));
|
|
1296
|
+
let y = origin.y;
|
|
1297
|
+
let widest = 0;
|
|
1298
|
+
list.forEach((n, i) => {
|
|
1299
|
+
const { width, height } = nodeSize(n, registry.forNode(n));
|
|
1300
|
+
if (y > origin.y && y + height > origin.y + COLUMN_HEIGHT) {
|
|
1301
|
+
x += widest + ROW_GAP;
|
|
1302
|
+
y = origin.y;
|
|
1303
|
+
widest = 0;
|
|
1304
|
+
}
|
|
1305
|
+
rowOf.set(n.id, i);
|
|
1306
|
+
placed.set(n.id, { x, y });
|
|
1307
|
+
y += height + ROW_GAP;
|
|
1308
|
+
widest = Math.max(widest, width);
|
|
1309
|
+
bottom = Math.max(bottom, y - ROW_GAP);
|
|
1310
|
+
});
|
|
1311
|
+
x += widest + COLUMN_GAP;
|
|
1312
|
+
}
|
|
1313
|
+
return bottom;
|
|
1314
|
+
}
|
|
1143
1315
|
|
|
1144
1316
|
// src/index.ts
|
|
1317
|
+
var DEFAULT_PROJECTS = "FUI-Studio";
|
|
1145
1318
|
function projectsFolder() {
|
|
1146
1319
|
const at = process.argv.indexOf("--projects");
|
|
1147
1320
|
const given = at >= 0 && process.argv[at + 1] ? process.argv[at + 1] : process.env.FUI_PROJECTS || void 0;
|
|
1148
|
-
return given?.replace(/^~(?=$|[\\/])/, homedir());
|
|
1321
|
+
return given?.replace(/^~(?=$|[\\/])/, homedir()) ?? join4(homedir(), DEFAULT_PROJECTS);
|
|
1149
1322
|
}
|
|
1150
1323
|
function version() {
|
|
1151
1324
|
try {
|
|
@@ -1163,11 +1336,17 @@ var session = new Session({ root: projectsFolder() });
|
|
|
1163
1336
|
var INSTRUCTIONS = `FUI Studio builds animated HUD and FUI graphics (rings, ticks, readouts, crosshairs, scanning bars) as a node graph that renders to SVG, PNG and DaVinci Resolve titles. These tools edit one open project at a time.
|
|
1164
1337
|
|
|
1165
1338
|
How to work:
|
|
1166
|
-
1. Start: new_project (name, width, height), or list_projects then open_project to continue one.
|
|
1167
|
-
2. Learn the library once: list_node_types, optionally by category. Generators draw shapes; modifiers change them; layout repeats and arranges; drivers (value, lfo, noise, time\u2026) produce numbers; post nodes are pixel effects.
|
|
1168
|
-
3. Build: add_node with params, then connect. Every param is also an input socket: connect(from: "lfo1", to: "ring1", toPort: "radius") makes the radius move. That is how things animate; set_keyframe is the other way
|
|
1339
|
+
1. Start: list_presets and new_from_preset when one is near what is asked (a radar, a dial, a boot sequence, panels in depth) \u2014 a working graph to change beats an empty one. Else new_project (name, width, height), or list_projects then open_project to continue one.
|
|
1340
|
+
2. Learn the library once: list_node_types, optionally by category. Generators draw shapes; modifiers change them; layout repeats and arranges; drivers (value, lfo, noise, time\u2026) produce numbers; post nodes are pixel effects. describe_node(type) tells what each param means, its unit and range \u2014 read it before using a node whose params are not obvious.
|
|
1341
|
+
3. Build: add_node with params, then connect. Every param is also an input socket: connect(from: "lfo1", to: "ring1", toPort: "radius") makes the radius move. That is how things animate; set_keyframe is the other way. A param's long form carries more: {"base": 5, "expr": "sin(t) * 10"} for an expression (t is time; in a node that handles several shapes, i is the shape's index, n the count, v the base value; sin, cos, rad, PI, TAU are there), {"base": 0, "stagger": {"amount": 30}} for a ramp across the shapes (shape i gets base + i \xD7 amount), {"base": 5, "jitter": {"amount": 2, "mode": "frozen"}} for seeded variation.
|
|
1169
1342
|
4. Look: render_preview after each few edits and actually check the picture. Pass t (seconds) to see a moment of the animation.
|
|
1170
|
-
5. Save: save_project with a bare name ("radar") saves radar.tui in the projects folder.
|
|
1343
|
+
5. Save: save_project with a bare name ("radar") saves radar.tui in the projects folder. The first time you save, tell the user the full path the save reports, and that the studio shows the project live once they choose that same folder there (folder\u2026 in the toolbar, then Open).
|
|
1344
|
+
|
|
1345
|
+
Pixels: shapes are vectors until a rasterize node draws them; post nodes (blur, bloom, glitch, depth-of-field\u2026) take and give textures, never shapes. The chain is shapes \u2192 rasterize \u2192 post nodes \u2192 a Layer's "in". A Layer draws one picture among its shapes.
|
|
1346
|
+
|
|
1347
|
+
3D: a transform3d node poses the shapes it gets as a plane under the camera (x right, y down, z away from the camera: positive Pos. Z draws smaller). Plane "shape" with a stagger on posZ or rotY poses an array's copies one by one; Order "depth" draws the nearer over the farther. The canvas's perspective is the lens (0 = orthographic). A Layer's own 3D group poses the whole layer the same way. Strokes stay screen pixels on a plane: put outline-stroke before the pose for strokes that thin with distance. depth-of-field wants the picture on "in" and the same shapes that made it on "depth"; shapes a transform3d posed remember their depth, so pose inside the layer, before rasterize.
|
|
1348
|
+
|
|
1349
|
+
Keep the graph readable, because the user opens it in the studio. A node whose output is wired to nothing draws straight onto the page. That works, but a page of loose nodes looks like a graph with its wires missing. Gather the parts of one element (a radar's rings, ticks, labels and crosshair) into a Merge node: add_node merge, then connect each part to its "in" port. Merge draws in connection order, so the first wire is at the bottom. Give the Merge a name ("radar", "readout") so the studio shows what it is. Leave position out of add_node: on save, nodes without one are laid out in columns that follow the wires.
|
|
1171
1350
|
|
|
1172
1351
|
Layers: a Layer node is a self-contained composition with its own page and its own clock. add_node with layer: "<layer id>" puts a node inside it. Arrange Layers in time with add_clip; a document with no clips plays every Layer all the time. list_layers explains the rules.
|
|
1173
1352
|
|
|
@@ -1223,7 +1402,15 @@ server.registerTool(
|
|
|
1223
1402
|
description: 'Save the project. Without a path, writes back to where it was opened from. A relative path or bare name (".tui" is added) saves into the projects folder, where an open studio shows it at once. Writing back over a file someone saved since you opened it is refused unless overwrite is true.',
|
|
1224
1403
|
inputSchema: { path: z2.string().optional(), overwrite: z2.boolean().default(false) }
|
|
1225
1404
|
},
|
|
1226
|
-
guard(({ path, overwrite }) =>
|
|
1405
|
+
guard(({ path, overwrite }) => {
|
|
1406
|
+
const saved = session.save(path, { overwrite });
|
|
1407
|
+
const loose = session.looseDrawers();
|
|
1408
|
+
const nudge = loose.length > 3 ? `
|
|
1409
|
+
${loose.length} nodes draw with nothing wired to their output (${loose.join(", ")}). In the studio that reads as missing wires: gather each element's parts into a named Merge (connect in drawing order, first at the bottom) and save again.` : "";
|
|
1410
|
+
const folder = session.root && dirname3(saved).startsWith(session.root) ? session.root : dirname3(saved);
|
|
1411
|
+
return text(`saved ${saved}
|
|
1412
|
+
In FUI Studio, choose the folder ${folder} (folder\u2026 in the toolbar, or change folder\u2026 under Open) and open it from Open.${nudge}`);
|
|
1413
|
+
})
|
|
1227
1414
|
);
|
|
1228
1415
|
server.registerTool(
|
|
1229
1416
|
"list_projects",
|
|
@@ -1266,6 +1453,58 @@ server.registerTool(
|
|
|
1266
1453
|
)
|
|
1267
1454
|
)
|
|
1268
1455
|
);
|
|
1456
|
+
server.registerTool(
|
|
1457
|
+
"describe_node",
|
|
1458
|
+
{
|
|
1459
|
+
description: "One node type in full: what every param means, its unit, range, group and choices, and which inputs it takes. Read this before using a node whose params are not obvious \u2014 the 3D nodes, the post effects, the arrays.",
|
|
1460
|
+
inputSchema: { type: z2.string() }
|
|
1461
|
+
},
|
|
1462
|
+
guard(({ type }) => {
|
|
1463
|
+
const d = registry.list().find((n) => n.type === type && !n.hidden);
|
|
1464
|
+
if (!d) throw new SessionError(`No node type "${type}". list_node_types has every type.`);
|
|
1465
|
+
return text({
|
|
1466
|
+
type: d.type,
|
|
1467
|
+
label: d.label,
|
|
1468
|
+
category: d.category,
|
|
1469
|
+
description: d.description,
|
|
1470
|
+
...d.keywords ? { keywords: d.keywords } : {},
|
|
1471
|
+
inputs: d.inputs.map((i) => ({ name: i.name, type: i.type, ...i.multi ? { multi: true } : {}, ...i.accepts ? { accepts: i.accepts } : {} })),
|
|
1472
|
+
outputs: d.outputs.map((o) => ({ name: o.name, type: o.type })),
|
|
1473
|
+
params: d.params.map((p) => ({
|
|
1474
|
+
key: p.key,
|
|
1475
|
+
label: p.label,
|
|
1476
|
+
type: p.type,
|
|
1477
|
+
default: p.default,
|
|
1478
|
+
...p.description ? { description: p.description } : {},
|
|
1479
|
+
...p.group ? { group: p.group } : {},
|
|
1480
|
+
..."unit" in p && p.unit ? { unit: p.unit } : {},
|
|
1481
|
+
..."min" in p && p.min !== void 0 ? { min: p.min } : {},
|
|
1482
|
+
..."max" in p && p.max !== void 0 ? { max: p.max } : {},
|
|
1483
|
+
...isExposable(p) ? {} : { wireable: false },
|
|
1484
|
+
..."options" in p ? { options: p.options.map((o) => ({ value: o.value, label: o.label })) } : {}
|
|
1485
|
+
}))
|
|
1486
|
+
});
|
|
1487
|
+
})
|
|
1488
|
+
);
|
|
1489
|
+
server.registerTool(
|
|
1490
|
+
"list_presets",
|
|
1491
|
+
{
|
|
1492
|
+
description: "The starter documents the studio\u2019s Presets menu offers: complete, working graphs, each a worked example of one subsystem (post chain, tokens, drivers, 3D). Start from the nearest one and change it, rather than building from nothing.",
|
|
1493
|
+
inputSchema: {}
|
|
1494
|
+
},
|
|
1495
|
+
guard(() => text(PRESETS.map((p) => ({ name: p.name, label: p.label, description: p.description }))))
|
|
1496
|
+
);
|
|
1497
|
+
server.registerTool(
|
|
1498
|
+
"new_from_preset",
|
|
1499
|
+
{
|
|
1500
|
+
description: "Open a preset as the project, replacing whatever is open \u2014 like new_project, but with a working graph in it. list_nodes then shows what there is to change.",
|
|
1501
|
+
inputSchema: { name: z2.string() }
|
|
1502
|
+
},
|
|
1503
|
+
guard(async ({ name }) => {
|
|
1504
|
+
session.newFromPreset(name);
|
|
1505
|
+
return text(await session.summary());
|
|
1506
|
+
})
|
|
1507
|
+
);
|
|
1269
1508
|
server.registerTool(
|
|
1270
1509
|
"list_nodes",
|
|
1271
1510
|
{ description: "Every node in the graph, with params and incoming wires.", inputSchema: {} },
|
|
@@ -1377,10 +1616,13 @@ server.registerTool(
|
|
|
1377
1616
|
);
|
|
1378
1617
|
server.registerTool(
|
|
1379
1618
|
"disconnect",
|
|
1380
|
-
{
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1619
|
+
{
|
|
1620
|
+
description: `Remove the wire feeding a socket. A multi input (a Merge's or Layer's "in") holds several: pass from to remove just that one, or leave it out to remove them all.`,
|
|
1621
|
+
inputSchema: { to: z2.string(), toPort: z2.string(), from: z2.string().optional() }
|
|
1622
|
+
},
|
|
1623
|
+
guard(({ to, toPort, from }) => {
|
|
1624
|
+
session.disconnect(to, toPort, from);
|
|
1625
|
+
return text(`disconnected ${from ? `${from} \u2192 ` : ""}${to}.${toPort}`);
|
|
1384
1626
|
})
|
|
1385
1627
|
);
|
|
1386
1628
|
server.registerTool(
|
|
@@ -1454,12 +1696,11 @@ server.registerTool(
|
|
|
1454
1696
|
},
|
|
1455
1697
|
guard(async ({ path, t, scale }) => {
|
|
1456
1698
|
const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2 } = await import("fs");
|
|
1457
|
-
const { dirname: dirname3 } = await import("path");
|
|
1458
1699
|
const abs = session.resolvePath(path);
|
|
1459
1700
|
mkdirSync2(dirname3(abs), { recursive: true });
|
|
1460
1701
|
if (/\.(ograf|drfx)$/i.test(abs)) {
|
|
1461
|
-
const { exportTemplate } = await import("./node-
|
|
1462
|
-
const { packDrfx, packOgraf } = await import("./dist-
|
|
1702
|
+
const { exportTemplate } = await import("./node-XJJEAAN7.js");
|
|
1703
|
+
const { packDrfx, packOgraf } = await import("./dist-LUAYBAOC.js");
|
|
1463
1704
|
const template = exportTemplate(session.project, void 0, session.assetResolver());
|
|
1464
1705
|
const errors = template.lint.filter((i) => i.level === "error");
|
|
1465
1706
|
if (errors.length > 0) throw new SessionError(`The manifest has errors: ${errors.map((i) => `${i.where}: ${i.message}`).join("; ")}`);
|
|
@@ -1489,7 +1730,6 @@ server.registerTool(
|
|
|
1489
1730
|
guard(async ({ dir, from, to, fps, scale }) => {
|
|
1490
1731
|
if (to <= from) throw new SessionError("`to` must be after `from`.");
|
|
1491
1732
|
const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2 } = await import("fs");
|
|
1492
|
-
const { join: join4 } = await import("path");
|
|
1493
1733
|
const abs = session.resolvePath(dir);
|
|
1494
1734
|
mkdirSync2(abs, { recursive: true });
|
|
1495
1735
|
const frames = Math.round((to - from) * fps);
|
|
@@ -1502,3 +1742,6 @@ server.registerTool(
|
|
|
1502
1742
|
})
|
|
1503
1743
|
);
|
|
1504
1744
|
await server.connect(new StdioServerTransport());
|
|
1745
|
+
export {
|
|
1746
|
+
DEFAULT_PROJECTS
|
|
1747
|
+
};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
loadFaces,
|
|
4
4
|
nodeOutliner
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-U3VXQSCB.js";
|
|
6
6
|
import {
|
|
7
7
|
buildTemplate
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-S3AUIOGZ.js";
|
|
9
9
|
|
|
10
10
|
// ../../packages/ograf/dist/node.js
|
|
11
11
|
import { existsSync, readFileSync } from "fs";
|