fui-studio-mcp 0.1.0 → 0.1.1
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 +12 -4
- package/dist/{chunk-RAHWGTHJ.js → chunk-EFZHSV66.js} +1 -1
- package/dist/{chunk-W7L36Z34.js → chunk-NDQB3ETD.js} +29 -24
- package/dist/{dist-3URVYEPT.js → dist-RTISZO3Q.js} +1 -1
- package/dist/index.js +191 -17
- package/dist/{node-4RLELQ7B.js → node-3VDUDD3U.js} +2 -2
- package/dist/runtime.global.js +863 -860
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ on your computer when it needs it. You don't run it yourself.
|
|
|
22
22
|
|
|
23
23
|
Make a folder for your FUI projects, for example `FUI-Studio` in your home
|
|
24
24
|
folder. The assistant saves there, and the studio watches the same folder.
|
|
25
|
+
If the config names no folder, the server uses `FUI-Studio` in your home
|
|
26
|
+
folder.
|
|
25
27
|
|
|
26
28
|
## 2. Add the server to your AI app
|
|
27
29
|
|
|
@@ -82,7 +84,8 @@ over stdio. Instead of `--projects`, you can set the environment variable
|
|
|
82
84
|
## 3. Open the same folder in the studio
|
|
83
85
|
|
|
84
86
|
In FUI Studio, click **folder…** in the toolbar and choose your projects
|
|
85
|
-
folder
|
|
87
|
+
folder, the same one as in step 1. The assistant also tells you the folder
|
|
88
|
+
when it first saves. Once it has saved a project, open it from **Open**. From
|
|
86
89
|
then on, every save the assistant makes shows up within a second, on the page
|
|
87
90
|
you're looking at. One undo takes a change back.
|
|
88
91
|
|
|
@@ -130,14 +133,19 @@ projects fit together.
|
|
|
130
133
|
|
|
131
134
|
## Troubleshooting
|
|
132
135
|
|
|
133
|
-
- **The assistant
|
|
134
|
-
`--projects
|
|
136
|
+
- **The assistant saved somewhere else.** Its save message names the folder.
|
|
137
|
+
Without `--projects` in the config, it saves in `FUI-Studio` in your home
|
|
138
|
+
folder.
|
|
139
|
+
- **Changing the folder.** For the assistant, change the path after
|
|
140
|
+
`--projects` in its config and restart your AI app; with Claude Code, run
|
|
141
|
+
`claude mcp remove fui-studio` and the add command again. For the studio,
|
|
142
|
+
choose **change folder…** at the bottom of the **Open** dialog.
|
|
135
143
|
- **"npx: command not found", or the server won't start.** Install Node.js,
|
|
136
144
|
then restart your AI app. Apps started from the Dock or Start menu may not
|
|
137
145
|
see a Node installed through nvm; in that case, put the full path to `npx`
|
|
138
146
|
in `"command"`.
|
|
139
147
|
- **The studio doesn't update.** Check that it has the same folder open
|
|
140
|
-
(**
|
|
148
|
+
(the **Open** dialog shows which), and that you opened the very project the assistant is
|
|
141
149
|
saving.
|
|
142
150
|
- **Which version is this?** Run `npx fui-studio-mcp --version`.
|
|
143
151
|
|
|
@@ -2294,7 +2294,6 @@ function makeCookContext(input) {
|
|
|
2294
2294
|
params,
|
|
2295
2295
|
inputs,
|
|
2296
2296
|
wires,
|
|
2297
|
-
wiredOutputs: input.wiredOutputs ?? /* @__PURE__ */ new Set(),
|
|
2298
2297
|
inputList: (name) => multiInputs[name] ?? (inputs[name] ? [inputs[name]] : []),
|
|
2299
2298
|
scope,
|
|
2300
2299
|
num: (key, i, n, vars) => resolveNumber(params, key, fallbackNum(key), scope(i, n, vars)),
|
|
@@ -2805,10 +2804,6 @@ function evaluate(project, registry, opts = {}) {
|
|
|
2805
2804
|
}
|
|
2806
2805
|
const def = registry.requireNode(node);
|
|
2807
2806
|
const edges = incoming.get(node.id) ?? [];
|
|
2808
|
-
const claiming = def.outputs.filter((o) => o.claims).map((o) => o.name);
|
|
2809
|
-
const wiredOutputs = new Set(
|
|
2810
|
-
claiming.length === 0 ? [] : project.graph.edges.filter((e) => e.from === node.id && claiming.includes(e.fromPort)).map((e) => e.fromPort)
|
|
2811
|
-
);
|
|
2812
2807
|
const seed = node.seed ?? project.seed;
|
|
2813
2808
|
let wires = wiresOf.get(node.id);
|
|
2814
2809
|
if (!wires) {
|
|
@@ -2891,16 +2886,12 @@ function evaluate(project, registry, opts = {}) {
|
|
|
2891
2886
|
continue;
|
|
2892
2887
|
}
|
|
2893
2888
|
if (opts.vectors && readsPixels(def)) {
|
|
2889
|
+
const shapes = opts.vectors(node.id);
|
|
2890
|
+
const outPort = def.outputs[0];
|
|
2894
2891
|
const out = {};
|
|
2895
|
-
|
|
2896
|
-
for (const port of def.outputs) {
|
|
2897
|
-
if (port.type !== "shapes") continue;
|
|
2898
|
-
const shapes = opts.vectors(node.id, port.name);
|
|
2899
|
-
if (shapes) out[port.name] = shapesVal(shapes);
|
|
2900
|
-
keys.push(shapes ? String(identityKey(shapes)) : "none");
|
|
2901
|
-
}
|
|
2892
|
+
if (shapes && outPort) out[outPort.name] = shapesVal(shapes);
|
|
2902
2893
|
outputs.set(node.id, withTaps(node.id, out));
|
|
2903
|
-
hashes.set(node.id, hashOf("provided", node.id,
|
|
2894
|
+
hashes.set(node.id, hashOf("provided", node.id, shapes ? String(identityKey(shapes)) : "none"));
|
|
2904
2895
|
continue;
|
|
2905
2896
|
}
|
|
2906
2897
|
if (node.disabled) {
|
|
@@ -2936,9 +2927,7 @@ function evaluate(project, registry, opts = {}) {
|
|
|
2936
2927
|
timeDeps.has(node.id) ? String(tNode) : "",
|
|
2937
2928
|
// The outliner arriving mid-session must invalidate text cooked without
|
|
2938
2929
|
// it — one global salt, one full re-cook, then business as usual.
|
|
2939
|
-
opts.outliner ? "o1" : "o0"
|
|
2940
|
-
// A claiming output wired or not reshapes the others (Contours' major).
|
|
2941
|
-
[...wiredOutputs].sort().join(",")
|
|
2930
|
+
opts.outliner ? "o1" : "o0"
|
|
2942
2931
|
);
|
|
2943
2932
|
hashes.set(node.id, nodeHash);
|
|
2944
2933
|
const hit = upstreamTainted ? void 0 : cache22?.get(node.id, nodeHash);
|
|
@@ -2947,7 +2936,6 @@ function evaluate(project, registry, opts = {}) {
|
|
|
2947
2936
|
continue;
|
|
2948
2937
|
}
|
|
2949
2938
|
const ctx = makeCookContext({
|
|
2950
|
-
wiredOutputs,
|
|
2951
2939
|
nodeId: node.id,
|
|
2952
2940
|
name: node.name,
|
|
2953
2941
|
seed,
|
|
@@ -9025,6 +9013,16 @@ var transformNode = {
|
|
|
9025
9013
|
description: "With a Ref wired: the point on its bounds the pivot lands on. Move X/Y are measured from it; Rotate and Scale turn about it.",
|
|
9026
9014
|
dimWhen: refUnwired
|
|
9027
9015
|
},
|
|
9016
|
+
{
|
|
9017
|
+
key: "refMix",
|
|
9018
|
+
label: "Mix",
|
|
9019
|
+
type: "number",
|
|
9020
|
+
default: 1,
|
|
9021
|
+
step: 0.01,
|
|
9022
|
+
group: "Ref",
|
|
9023
|
+
description: "How far along the way to the Ref point: 0 leaves the shape where it was authored, 1 lands it on the point. Wire a Seq or Mapping in to glide it there; past 0\u20131 it overshoots.",
|
|
9024
|
+
dimWhen: refUnwired
|
|
9025
|
+
},
|
|
9028
9026
|
{
|
|
9029
9027
|
key: "refEach",
|
|
9030
9028
|
label: "Per Shape",
|
|
@@ -9057,10 +9055,11 @@ var transformNode = {
|
|
|
9057
9055
|
const b = each ? shapeBounds(ref[i % ref.length]) : whole;
|
|
9058
9056
|
if (!b) return shape;
|
|
9059
9057
|
const { fx, fy } = REF_POINTS[point];
|
|
9060
|
-
const
|
|
9058
|
+
const P = pivotFor(ctx, pivotMode, shape, i, n);
|
|
9059
|
+
const mix = ctx.num("refMix", i, n);
|
|
9060
|
+
const R = { x: P.x + mix * (b.x + fx * b.w - P.x), y: P.y + mix * (b.y + fy * b.h - P.y) };
|
|
9061
9061
|
const tx = ctx.num("tx", i, n);
|
|
9062
9062
|
const ty = ctx.num("ty", i, n);
|
|
9063
|
-
const P = pivotFor(ctx, pivotMode, shape, i, n);
|
|
9064
9063
|
const rot = ctx.num("rotation", i, n);
|
|
9065
9064
|
const sx = ctx.num("sx", i, n);
|
|
9066
9065
|
const sy = ctx.num("sy", i, n);
|
|
@@ -14942,10 +14941,16 @@ function fieldRef(p, shape, world, n, defs) {
|
|
|
14942
14941
|
defs.push(`<pattern id="${id}" ${attrs.join(" ")}>${image}</pattern>`);
|
|
14943
14942
|
return `url(#${id})`;
|
|
14944
14943
|
}
|
|
14945
|
-
function
|
|
14944
|
+
function paintAttr(name, color, n) {
|
|
14945
|
+
const h = color.startsWith("#") ? splitHex(color) : void 0;
|
|
14946
|
+
if (!h) return [`${name}="${color}"`];
|
|
14947
|
+
return h.a < 1 ? [`${name}="${h.rgb}"`, `${name}-opacity="${n(h.a)}"`] : [`${name}="${color}"`];
|
|
14948
|
+
}
|
|
14949
|
+
function styleAttrs(fill, stroke, style, n, split = true) {
|
|
14946
14950
|
const out = [];
|
|
14947
|
-
|
|
14948
|
-
if (
|
|
14951
|
+
const attr = (name, color) => split ? paintAttr(name, color, n) : [`${name}="${color}"`];
|
|
14952
|
+
if (fill !== void 0) out.push(...attr("fill", fill));
|
|
14953
|
+
if (stroke !== void 0 && stroke !== "none") out.push(...attr("stroke", stroke));
|
|
14949
14954
|
if (style.strokeWidth !== void 0 && stroke !== "none") {
|
|
14950
14955
|
out.push(`stroke-width="${n(style.strokeWidth)}"`);
|
|
14951
14956
|
}
|
|
@@ -14978,7 +14983,7 @@ function renderShape(shape, opts, depth, pretty, parent = IDENTITY) {
|
|
|
14978
14983
|
const defs = [];
|
|
14979
14984
|
const fill = paintRef(shape.style.fill, shape, world, n, defs);
|
|
14980
14985
|
const stroke = paintRef(shape.style.stroke, shape, world, n, defs);
|
|
14981
|
-
const element = renderElement(shape, opts, depth, pretty, world, styleAttrs(fill, stroke, shape.style, n));
|
|
14986
|
+
const element = renderElement(shape, opts, depth, pretty, world, styleAttrs(fill, stroke, shape.style, n, shape.kind !== "group"));
|
|
14982
14987
|
if (defs.length === 0) return element;
|
|
14983
14988
|
return `${pad}<g>${nl}${pad}<defs>${defs.join("")}</defs>${nl}${element}${pad}</g>${nl}`;
|
|
14984
14989
|
}
|
|
@@ -15067,7 +15072,7 @@ function renderSvg(scene, options = {}) {
|
|
|
15067
15072
|
const defs = options.defs ? `${pretty ? " " : ""}<defs>${options.defs}</defs>${nl}` : "";
|
|
15068
15073
|
const ox = scene.origin === "center" ? -scene.width / 2 : 0;
|
|
15069
15074
|
const oy = scene.origin === "center" ? -scene.height / 2 : 0;
|
|
15070
|
-
const bg = scene.background && scene.background !== "none" ? `${pretty ? " " : ""}<rect x="${n(ox)}" y="${n(oy)}" width="${n(scene.width)}" height="${n(scene.height)}" fill
|
|
15075
|
+
const bg = scene.background && scene.background !== "none" ? `${pretty ? " " : ""}<rect x="${n(ox)}" y="${n(oy)}" width="${n(scene.width)}" height="${n(scene.height)}" ${paintAttr("fill", scene.background, n).join(" ")}/>${nl}` : "";
|
|
15071
15076
|
const view = options.crop ?? { x: ox, y: oy, w: scene.width, h: scene.height };
|
|
15072
15077
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(view.w)}" height="${n(view.h)}" viewBox="${n(view.x)} ${n(view.y)} ${n(view.w)} ${n(view.h)}">${nl}${defs}${bg}${body}</svg>${nl}`;
|
|
15073
15078
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
allTtfBuffers,
|
|
5
5
|
fontDefs,
|
|
6
6
|
nodeOutliner
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-EFZHSV66.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_FONT_FAMILY,
|
|
10
10
|
Keyframe,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
wouldCycle,
|
|
39
39
|
yuvFrameBytes,
|
|
40
40
|
yuvToRgb
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-NDQB3ETD.js";
|
|
42
42
|
|
|
43
43
|
// src/index.ts
|
|
44
44
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -46,6 +46,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
46
46
|
import { z as z2 } from "zod";
|
|
47
47
|
import { createRequire as createRequire3 } from "module";
|
|
48
48
|
import { homedir } from "os";
|
|
49
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
49
50
|
|
|
50
51
|
// src/session.ts
|
|
51
52
|
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync, writeFileSync, mkdirSync } from "fs";
|
|
@@ -755,6 +756,14 @@ var Session = class {
|
|
|
755
756
|
* come from that folder instead.
|
|
756
757
|
*/
|
|
757
758
|
held = /* @__PURE__ */ new Map();
|
|
759
|
+
/**
|
|
760
|
+
* Nodes added this session without a position. A save lays them out —
|
|
761
|
+
* columns by wire depth, as the studio's L does — so the graph reads in
|
|
762
|
+
* the studio instead of arriving as one endless row. Nodes with a
|
|
763
|
+
* position of their own (the model's, or the person's in a file that
|
|
764
|
+
* was opened) never move.
|
|
765
|
+
*/
|
|
766
|
+
placed = /* @__PURE__ */ new Set();
|
|
758
767
|
bumpIds() {
|
|
759
768
|
for (const n of this.project.graph.nodes) {
|
|
760
769
|
const m = /(\d+)$/.exec(n.id);
|
|
@@ -770,6 +779,7 @@ var Session = class {
|
|
|
770
779
|
this.path = void 0;
|
|
771
780
|
this.stamp = void 0;
|
|
772
781
|
this.held.clear();
|
|
782
|
+
this.placed.clear();
|
|
773
783
|
return this.project;
|
|
774
784
|
}
|
|
775
785
|
open(path) {
|
|
@@ -781,6 +791,7 @@ var Session = class {
|
|
|
781
791
|
this.path = abs;
|
|
782
792
|
this.stamp = stamp;
|
|
783
793
|
this.held.clear();
|
|
794
|
+
this.placed.clear();
|
|
784
795
|
this.bumpIds();
|
|
785
796
|
return { project, warnings };
|
|
786
797
|
}
|
|
@@ -802,6 +813,7 @@ var Session = class {
|
|
|
802
813
|
);
|
|
803
814
|
}
|
|
804
815
|
}
|
|
816
|
+
this.arrange();
|
|
805
817
|
mkdirSync(dirname2(target), { recursive: true });
|
|
806
818
|
this.writeHeldAssets(target);
|
|
807
819
|
writeFileSync(target, serializeProject(this.project));
|
|
@@ -923,6 +935,7 @@ var Session = class {
|
|
|
923
935
|
}
|
|
924
936
|
}
|
|
925
937
|
this.project.graph.nodes.push(node);
|
|
938
|
+
if (!options.position) this.placed.add(id);
|
|
926
939
|
return node;
|
|
927
940
|
}
|
|
928
941
|
/**
|
|
@@ -943,9 +956,47 @@ var Session = class {
|
|
|
943
956
|
const last = nodes[nodes.length - 1];
|
|
944
957
|
return last ? { x: last.position.x + 260, y: last.position.y } : { x: 0, y: 0 };
|
|
945
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* Lay out the nodes this session placed, page by page (the root, and each
|
|
961
|
+
* Layer's own). A page of nothing but such nodes is arranged whole, every
|
|
962
|
+
* save, so it stays tidy as it grows; on a page that also holds nodes with
|
|
963
|
+
* positions of their own, the new ones are arranged among themselves and
|
|
964
|
+
* set below what is already there.
|
|
965
|
+
*/
|
|
966
|
+
arrange() {
|
|
967
|
+
const pages = /* @__PURE__ */ new Map();
|
|
968
|
+
for (const n of this.project.graph.nodes) {
|
|
969
|
+
const key = n.layer ?? "";
|
|
970
|
+
const list = pages.get(key);
|
|
971
|
+
if (list) list.push(n);
|
|
972
|
+
else pages.set(key, [n]);
|
|
973
|
+
}
|
|
974
|
+
for (const nodes of pages.values()) {
|
|
975
|
+
const moving = nodes.filter((n) => this.placed.has(n.id));
|
|
976
|
+
if (moving.length === 0) continue;
|
|
977
|
+
const staying = nodes.filter((n) => !this.placed.has(n.id));
|
|
978
|
+
const ids = new Set(moving.map((n) => n.id));
|
|
979
|
+
const edges = this.project.graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to));
|
|
980
|
+
const origin = staying.length === 0 ? { x: 0, y: 0 } : {
|
|
981
|
+
x: Math.min(...staying.map((n) => n.position.x)),
|
|
982
|
+
y: Math.max(...staying.map((n) => n.position.y + nodeSize(n, registry.forNode(n)).height)) + 120
|
|
983
|
+
};
|
|
984
|
+
const at = columnLayout(moving, edges, origin);
|
|
985
|
+
for (const n of moving) n.position = at.get(n.id) ?? n.position;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Root-page nodes that draw straight onto the page because nothing takes
|
|
990
|
+
* their shapes — valid, but in the studio they read as forgotten wires.
|
|
991
|
+
*/
|
|
992
|
+
looseDrawers() {
|
|
993
|
+
const feeding = new Set(this.project.graph.edges.map((e) => e.from));
|
|
994
|
+
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);
|
|
995
|
+
}
|
|
946
996
|
removeNode(id) {
|
|
947
997
|
this.node(id);
|
|
948
998
|
this.project.graph.nodes = this.project.graph.nodes.filter((n) => n.id !== id);
|
|
999
|
+
this.placed.delete(id);
|
|
949
1000
|
this.project.graph.edges = this.project.graph.edges.filter((e) => e.from !== id && e.to !== id);
|
|
950
1001
|
}
|
|
951
1002
|
setParam(nodeId, key, value) {
|
|
@@ -987,7 +1038,12 @@ var Session = class {
|
|
|
987
1038
|
}
|
|
988
1039
|
if (paramDef && !isExposable(paramDef)) fail(`${toNode.type}.${toPort} is not wireable.`);
|
|
989
1040
|
if (wouldCycle(this.project.graph, from, to, fromPort, toPort)) fail(`Connecting ${from} \u2192 ${to} would make a loop.`);
|
|
990
|
-
|
|
1041
|
+
if (toDef.inputs.find((i) => i.name === toPort)?.multi) {
|
|
1042
|
+
const same = this.project.graph.edges.find((e) => e.from === from && e.fromPort === fromPort && e.to === to && e.toPort === toPort);
|
|
1043
|
+
if (same) return same;
|
|
1044
|
+
} else {
|
|
1045
|
+
this.project.graph.edges = this.project.graph.edges.filter((e) => !(e.to === to && e.toPort === toPort));
|
|
1046
|
+
}
|
|
991
1047
|
const edge = { id: makeId("edge"), from, fromPort, to, toPort };
|
|
992
1048
|
this.project.graph.edges.push(edge);
|
|
993
1049
|
const outType = fromDef.outputs.find((o) => o.name === fromPort)?.type;
|
|
@@ -997,10 +1053,13 @@ var Session = class {
|
|
|
997
1053
|
}
|
|
998
1054
|
return edge;
|
|
999
1055
|
}
|
|
1000
|
-
|
|
1056
|
+
/** Remove the wires into a socket — only the one from `from`, when given (a Merge's `in` holds several). */
|
|
1057
|
+
disconnect(to, toPort, from) {
|
|
1001
1058
|
const before = this.project.graph.edges.length;
|
|
1002
|
-
this.project.graph.edges = this.project.graph.edges.filter(
|
|
1003
|
-
|
|
1059
|
+
this.project.graph.edges = this.project.graph.edges.filter(
|
|
1060
|
+
(e) => !(e.to === to && e.toPort === toPort && (from === void 0 || e.from === from))
|
|
1061
|
+
);
|
|
1062
|
+
if (this.project.graph.edges.length === before) fail(`Nothing${from ? ` from ${from}` : ""} is wired into ${to}.${toPort}.`);
|
|
1004
1063
|
}
|
|
1005
1064
|
/**
|
|
1006
1065
|
* Keyframes write into the same param model everything else uses — a track
|
|
@@ -1140,12 +1199,113 @@ var Session = class {
|
|
|
1140
1199
|
return svgToPng(await this.renderSvgString(t, false), { scale });
|
|
1141
1200
|
}
|
|
1142
1201
|
};
|
|
1202
|
+
function nodeSize(node, def) {
|
|
1203
|
+
if (node.micro) return { width: 80, height: 24 };
|
|
1204
|
+
const width = def?.wide ? 456 : 228;
|
|
1205
|
+
const outputs = def?.outputs.filter((o) => !o.hideWhen?.(node.params ?? {})).length ?? 0;
|
|
1206
|
+
const height = node.collapsed ? 44 : 66 + Math.max(def?.inputs.length ?? 0, outputs) * 20 + (def?.params.length ?? 0) * 24;
|
|
1207
|
+
return { width, height };
|
|
1208
|
+
}
|
|
1209
|
+
var COLUMN_HEIGHT = 1600;
|
|
1210
|
+
var ROW_GAP = 28;
|
|
1211
|
+
var COLUMN_GAP = 70;
|
|
1212
|
+
var BLOCK_GAP = 160;
|
|
1213
|
+
function columnLayout(nodes, edges, origin) {
|
|
1214
|
+
const root = new Map(nodes.map((n) => [n.id, n.id]));
|
|
1215
|
+
const find = (id) => {
|
|
1216
|
+
let r = id;
|
|
1217
|
+
while (root.get(r) !== r) r = root.get(r);
|
|
1218
|
+
root.set(id, r);
|
|
1219
|
+
return r;
|
|
1220
|
+
};
|
|
1221
|
+
for (const e of edges) if (root.has(e.from) && root.has(e.to)) root.set(find(e.from), find(e.to));
|
|
1222
|
+
const wired = new Set(edges.flatMap((e) => [e.from, e.to]));
|
|
1223
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
1224
|
+
const loose = [];
|
|
1225
|
+
for (const n of nodes) {
|
|
1226
|
+
if (!wired.has(n.id)) {
|
|
1227
|
+
loose.push(n);
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
const key = find(n.id);
|
|
1231
|
+
const list = blocks.get(key);
|
|
1232
|
+
if (list) list.push(n);
|
|
1233
|
+
else blocks.set(key, [n]);
|
|
1234
|
+
}
|
|
1235
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1236
|
+
let y = origin.y;
|
|
1237
|
+
for (const block of [...blocks.values(), ...loose.length ? [loose] : []]) {
|
|
1238
|
+
const bottom = layoutBlock(block, edges, { x: origin.x, y }, placed);
|
|
1239
|
+
y = bottom + BLOCK_GAP;
|
|
1240
|
+
}
|
|
1241
|
+
return placed;
|
|
1242
|
+
}
|
|
1243
|
+
function layoutBlock(nodes, allEdges, origin, placed) {
|
|
1244
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
1245
|
+
const edges = allEdges.filter((e) => ids.has(e.from) && ids.has(e.to));
|
|
1246
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
1247
|
+
for (const e of edges) {
|
|
1248
|
+
const list = incoming.get(e.to);
|
|
1249
|
+
if (list) list.push(e.from);
|
|
1250
|
+
else incoming.set(e.to, [e.from]);
|
|
1251
|
+
}
|
|
1252
|
+
const colOf = /* @__PURE__ */ new Map();
|
|
1253
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1254
|
+
const col = (id) => {
|
|
1255
|
+
const known = colOf.get(id);
|
|
1256
|
+
if (known !== void 0) return known;
|
|
1257
|
+
if (visiting.has(id)) return 0;
|
|
1258
|
+
visiting.add(id);
|
|
1259
|
+
const ins = incoming.get(id) ?? [];
|
|
1260
|
+
const c = ins.length === 0 ? 0 : Math.max(...ins.map(col)) + 1;
|
|
1261
|
+
visiting.delete(id);
|
|
1262
|
+
colOf.set(id, c);
|
|
1263
|
+
return c;
|
|
1264
|
+
};
|
|
1265
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1266
|
+
nodes.forEach((n) => {
|
|
1267
|
+
const c = col(n.id);
|
|
1268
|
+
const list = columns.get(c);
|
|
1269
|
+
if (list) list.push(n);
|
|
1270
|
+
else columns.set(c, [n]);
|
|
1271
|
+
});
|
|
1272
|
+
const order = new Map(nodes.map((n, i) => [n.id, i]));
|
|
1273
|
+
const rowOf = /* @__PURE__ */ new Map();
|
|
1274
|
+
let x = origin.x;
|
|
1275
|
+
let bottom = origin.y;
|
|
1276
|
+
for (const c of [...columns.keys()].sort((a, b) => a - b)) {
|
|
1277
|
+
const list = columns.get(c);
|
|
1278
|
+
const key = (n) => {
|
|
1279
|
+
const ins = (incoming.get(n.id) ?? []).map((id) => rowOf.get(id) ?? 0);
|
|
1280
|
+
return ins.length > 0 ? ins.reduce((sum, v) => sum + v, 0) / ins.length : -1;
|
|
1281
|
+
};
|
|
1282
|
+
list.sort((a, b) => key(a) - key(b) || order.get(a.id) - order.get(b.id));
|
|
1283
|
+
let y = origin.y;
|
|
1284
|
+
let widest = 0;
|
|
1285
|
+
list.forEach((n, i) => {
|
|
1286
|
+
const { width, height } = nodeSize(n, registry.forNode(n));
|
|
1287
|
+
if (y > origin.y && y + height > origin.y + COLUMN_HEIGHT) {
|
|
1288
|
+
x += widest + ROW_GAP;
|
|
1289
|
+
y = origin.y;
|
|
1290
|
+
widest = 0;
|
|
1291
|
+
}
|
|
1292
|
+
rowOf.set(n.id, i);
|
|
1293
|
+
placed.set(n.id, { x, y });
|
|
1294
|
+
y += height + ROW_GAP;
|
|
1295
|
+
widest = Math.max(widest, width);
|
|
1296
|
+
bottom = Math.max(bottom, y - ROW_GAP);
|
|
1297
|
+
});
|
|
1298
|
+
x += widest + COLUMN_GAP;
|
|
1299
|
+
}
|
|
1300
|
+
return bottom;
|
|
1301
|
+
}
|
|
1143
1302
|
|
|
1144
1303
|
// src/index.ts
|
|
1304
|
+
var DEFAULT_PROJECTS = "FUI-Studio";
|
|
1145
1305
|
function projectsFolder() {
|
|
1146
1306
|
const at = process.argv.indexOf("--projects");
|
|
1147
1307
|
const given = at >= 0 && process.argv[at + 1] ? process.argv[at + 1] : process.env.FUI_PROJECTS || void 0;
|
|
1148
|
-
return given?.replace(/^~(?=$|[\\/])/, homedir());
|
|
1308
|
+
return given?.replace(/^~(?=$|[\\/])/, homedir()) ?? join4(homedir(), DEFAULT_PROJECTS);
|
|
1149
1309
|
}
|
|
1150
1310
|
function version() {
|
|
1151
1311
|
try {
|
|
@@ -1167,7 +1327,9 @@ How to work:
|
|
|
1167
1327
|
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
1328
|
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, and params also take expressions ({"base": 5, "expr": "sin(t) * 10"}).
|
|
1169
1329
|
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.
|
|
1330
|
+
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).
|
|
1331
|
+
|
|
1332
|
+
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
1333
|
|
|
1172
1334
|
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
1335
|
|
|
@@ -1223,7 +1385,15 @@ server.registerTool(
|
|
|
1223
1385
|
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
1386
|
inputSchema: { path: z2.string().optional(), overwrite: z2.boolean().default(false) }
|
|
1225
1387
|
},
|
|
1226
|
-
guard(({ path, overwrite }) =>
|
|
1388
|
+
guard(({ path, overwrite }) => {
|
|
1389
|
+
const saved = session.save(path, { overwrite });
|
|
1390
|
+
const loose = session.looseDrawers();
|
|
1391
|
+
const nudge = loose.length > 3 ? `
|
|
1392
|
+
${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.` : "";
|
|
1393
|
+
const folder = session.root && dirname3(saved).startsWith(session.root) ? session.root : dirname3(saved);
|
|
1394
|
+
return text(`saved ${saved}
|
|
1395
|
+
In FUI Studio, choose the folder ${folder} (folder\u2026 in the toolbar, or change folder\u2026 under Open) and open it from Open.${nudge}`);
|
|
1396
|
+
})
|
|
1227
1397
|
);
|
|
1228
1398
|
server.registerTool(
|
|
1229
1399
|
"list_projects",
|
|
@@ -1377,10 +1547,13 @@ server.registerTool(
|
|
|
1377
1547
|
);
|
|
1378
1548
|
server.registerTool(
|
|
1379
1549
|
"disconnect",
|
|
1380
|
-
{
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1550
|
+
{
|
|
1551
|
+
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.`,
|
|
1552
|
+
inputSchema: { to: z2.string(), toPort: z2.string(), from: z2.string().optional() }
|
|
1553
|
+
},
|
|
1554
|
+
guard(({ to, toPort, from }) => {
|
|
1555
|
+
session.disconnect(to, toPort, from);
|
|
1556
|
+
return text(`disconnected ${from ? `${from} \u2192 ` : ""}${to}.${toPort}`);
|
|
1384
1557
|
})
|
|
1385
1558
|
);
|
|
1386
1559
|
server.registerTool(
|
|
@@ -1454,12 +1627,11 @@ server.registerTool(
|
|
|
1454
1627
|
},
|
|
1455
1628
|
guard(async ({ path, t, scale }) => {
|
|
1456
1629
|
const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2 } = await import("fs");
|
|
1457
|
-
const { dirname: dirname3 } = await import("path");
|
|
1458
1630
|
const abs = session.resolvePath(path);
|
|
1459
1631
|
mkdirSync2(dirname3(abs), { recursive: true });
|
|
1460
1632
|
if (/\.(ograf|drfx)$/i.test(abs)) {
|
|
1461
|
-
const { exportTemplate } = await import("./node-
|
|
1462
|
-
const { packDrfx, packOgraf } = await import("./dist-
|
|
1633
|
+
const { exportTemplate } = await import("./node-3VDUDD3U.js");
|
|
1634
|
+
const { packDrfx, packOgraf } = await import("./dist-RTISZO3Q.js");
|
|
1463
1635
|
const template = exportTemplate(session.project, void 0, session.assetResolver());
|
|
1464
1636
|
const errors = template.lint.filter((i) => i.level === "error");
|
|
1465
1637
|
if (errors.length > 0) throw new SessionError(`The manifest has errors: ${errors.map((i) => `${i.where}: ${i.message}`).join("; ")}`);
|
|
@@ -1489,7 +1661,6 @@ server.registerTool(
|
|
|
1489
1661
|
guard(async ({ dir, from, to, fps, scale }) => {
|
|
1490
1662
|
if (to <= from) throw new SessionError("`to` must be after `from`.");
|
|
1491
1663
|
const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2 } = await import("fs");
|
|
1492
|
-
const { join: join4 } = await import("path");
|
|
1493
1664
|
const abs = session.resolvePath(dir);
|
|
1494
1665
|
mkdirSync2(abs, { recursive: true });
|
|
1495
1666
|
const frames = Math.round((to - from) * fps);
|
|
@@ -1502,3 +1673,6 @@ server.registerTool(
|
|
|
1502
1673
|
})
|
|
1503
1674
|
);
|
|
1504
1675
|
await server.connect(new StdioServerTransport());
|
|
1676
|
+
export {
|
|
1677
|
+
DEFAULT_PROJECTS
|
|
1678
|
+
};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
loadFaces,
|
|
4
4
|
nodeOutliner
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EFZHSV66.js";
|
|
6
6
|
import {
|
|
7
7
|
buildTemplate
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-NDQB3ETD.js";
|
|
9
9
|
|
|
10
10
|
// ../../packages/ograf/dist/node.js
|
|
11
11
|
import { existsSync, readFileSync } from "fs";
|