Add shared-scene 2D authoring, sprite animation and Rapier2D physics
CI / validate (push) Canceled after 0s
CI / validate (push) Canceled after 0s
This commit is contained in:
+105
-16
@@ -73,8 +73,14 @@ test("real MCP HTTP + stdio share revisions, models, scripts, resources and expo
|
||||
.length,
|
||||
);
|
||||
const prompts = await client.listPrompts();
|
||||
assert.deepEqual(prompts.prompts.map((p) => p.name), ["create_scene"]);
|
||||
const prompt = await client.getPrompt({ name: "create_scene", arguments: { theme: "architecture" } });
|
||||
assert.deepEqual(
|
||||
prompts.prompts.map((p) => p.name),
|
||||
["create_scene"],
|
||||
);
|
||||
const prompt = await client.getPrompt({
|
||||
name: "create_scene",
|
||||
arguments: { theme: "architecture" },
|
||||
});
|
||||
assert.match(JSON.stringify(prompt.messages), /architecture/);
|
||||
const invoke = async (name: string, args: any = {}) => {
|
||||
const r = await client.callTool({ name, arguments: args });
|
||||
@@ -185,18 +191,101 @@ test("real MCP HTTP + stdio share revisions, models, scripts, resources and expo
|
||||
}
|
||||
});
|
||||
|
||||
test('MCP scene ZIP import replaces the scene atomically and Undo restores the prior project',async()=>{
|
||||
const dir=await mkdtemp(path.join(os.tmpdir(),'forma-scene-'));
|
||||
const s=await createService({projectDir:dir,port:0,blank:true});const client=new Client({name:'scene-test',version:'1'});
|
||||
try{
|
||||
await client.connect(new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${s.port}/mcp`),{requestInit:{headers:{authorization:'Bearer '+s.token}}}));
|
||||
const original=structuredClone(s.service.store.project);
|
||||
const {zipSync}=await import('fflate');const {sceneAnimationGlb}=await import('./fixtures.ts');
|
||||
const archive=zipSync({'scene.glb':sceneAnimationGlb()});
|
||||
const result=await client.callTool({name:'asset_import_glb',arguments:{expectedRevision:0,name:'scene.zip',base64:Buffer.from(archive).toString('base64'),asScene:true}});
|
||||
assert.ok(!result.isError,JSON.stringify(result.content));
|
||||
const p=s.service.store.project,n=p.scenes[0].entities[0];assert.equal(p.revision,1);assert.equal(n.components.camera.mode,'imported');assert.equal(n.components.animator.autoplay,'Scene');
|
||||
const undo=await client.callTool({name:'history_undo',arguments:{expectedRevision:1}});assert.ok(!undo.isError,JSON.stringify(undo.content));
|
||||
assert.equal(s.service.store.project.id,original.id);assert.equal(s.service.store.project.assets.length,0);
|
||||
}finally{await client.close();await s.close();await rm(dir,{recursive:true,force:true});}
|
||||
test("MCP scene ZIP import replaces the scene atomically and Undo restores the prior project", async () => {
|
||||
const dir = await mkdtemp(path.join(os.tmpdir(), "forma-scene-"));
|
||||
const s = await createService({ projectDir: dir, port: 0, blank: true });
|
||||
const client = new Client({ name: "scene-test", version: "1" });
|
||||
try {
|
||||
await client.connect(
|
||||
new StreamableHTTPClientTransport(
|
||||
new URL(`http://127.0.0.1:${s.port}/mcp`),
|
||||
{ requestInit: { headers: { authorization: "Bearer " + s.token } } },
|
||||
),
|
||||
);
|
||||
const original = structuredClone(s.service.store.project);
|
||||
const { zipSync } = await import("fflate");
|
||||
const { sceneAnimationGlb } = await import("./fixtures.ts");
|
||||
const archive = zipSync({ "scene.glb": sceneAnimationGlb() });
|
||||
const result = await client.callTool({
|
||||
name: "asset_import_glb",
|
||||
arguments: {
|
||||
expectedRevision: 0,
|
||||
name: "scene.zip",
|
||||
base64: Buffer.from(archive).toString("base64"),
|
||||
asScene: true,
|
||||
},
|
||||
});
|
||||
assert.ok(!result.isError, JSON.stringify(result.content));
|
||||
const p = s.service.store.project,
|
||||
n = p.scenes[0].entities[0];
|
||||
assert.equal(p.revision, 1);
|
||||
assert.equal(n.components.camera.mode, "imported");
|
||||
assert.equal(n.components.animator.autoplay, "Scene");
|
||||
const undo = await client.callTool({
|
||||
name: "history_undo",
|
||||
arguments: { expectedRevision: 1 },
|
||||
});
|
||||
assert.ok(!undo.isError, JSON.stringify(undo.content));
|
||||
assert.equal(s.service.store.project.id, original.id);
|
||||
assert.equal(s.service.store.project.assets.length, 0);
|
||||
} finally {
|
||||
await client.close();
|
||||
await s.close();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("MCP imports image assets, configures XY scenes, saves portable sprites and rejects escaping paths", async () => {
|
||||
const dir = await mkdtemp(path.join(os.tmpdir(), "forma-2d-"));
|
||||
const s = await createService({ projectDir: dir, port: 0, blank: true }),
|
||||
client = new Client({ name: "2d-test", version: "1" });
|
||||
try {
|
||||
await client.connect(
|
||||
new StreamableHTTPClientTransport(
|
||||
new URL(`http://127.0.0.1:${s.port}/mcp`),
|
||||
{ requestInit: { headers: { authorization: "Bearer " + s.token } } },
|
||||
),
|
||||
);
|
||||
const call = (name: string, args: any) =>
|
||||
client.callTool({ name, arguments: args });
|
||||
const created = await call("scene_create", {
|
||||
name: "XY",
|
||||
mode: "2d",
|
||||
expectedRevision: s.service.store.project.revision,
|
||||
});
|
||||
assert.ok(!created.isError, JSON.stringify(created));
|
||||
const { activeScene } = await import("../engine/schema.ts");
|
||||
assert.equal(activeScene(s.service.store.project).mode, "2d");
|
||||
const png =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFgAI/ScLttAAAAABJRU5ErkJggg==";
|
||||
const imported = await call("asset_import_image", {
|
||||
name: "pixel.png",
|
||||
base64: png,
|
||||
instantiate: true,
|
||||
expectedRevision: s.service.store.project.revision,
|
||||
});
|
||||
assert.ok(!imported.isError, JSON.stringify(imported));
|
||||
const project = s.service.store.project;
|
||||
assert.equal(project.assets[0].kind, "image");
|
||||
assert.equal(
|
||||
activeScene(project).entities[0].components.sprite.assetId,
|
||||
project.assets[0].id,
|
||||
);
|
||||
const bad = await call("asset_import_image", {
|
||||
name: "pixel.png",
|
||||
path: "../escape.png",
|
||||
expectedRevision: project.revision,
|
||||
});
|
||||
assert.ok(bad.isError);
|
||||
assert.equal(s.service.store.project.revision, project.revision);
|
||||
await call("project_save", {});
|
||||
const restored = unpackProject(
|
||||
new Uint8Array(await readFile(path.join(dir, "project.forma"))),
|
||||
);
|
||||
assert.deepEqual(restored, project);
|
||||
} finally {
|
||||
await client.close();
|
||||
await s.close();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user