import { test, expect } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const root = path.resolve(import.meta.dirname, "../../.."); const fixture = fs.readFileSync(path.join(root, "tests/files/web/basic_scene.blend")); test("shape keys, vertex groups, modifier copy and apply-as-shape-key", async ({ page }) => { test.setTimeout(180_000); await page.goto("/?engine=manual"); const result = await page.evaluate(async ({ bytes }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const run = async (setup: (client: InstanceType, node: { id: string; name: string; dataId: string }) => Promise>) => { const client = new WebEngineClient({ timeoutMs: 30_000 }); try { const open = await client.openBlend(Uint8Array.from(bytes).buffer); const node = open.snapshot.nodes.find((n) => n.name === "BasicCube"); if (!node?.dataId) throw new Error("BasicCube is missing"); return { ok: true, ...(await setup(client, node as { id: string; name: string; dataId: string })) }; } catch (error) { const e = error as { message?: string; code?: string }; return { ok: false, error: e?.message, code: e?.code }; } finally { client.terminate(); } }; const inspect = async (client: InstanceType) => { const res = await client.executeEmbeddedPython(`import bpy, json ob = bpy.data.objects.get("BasicCube") print("OBJECT_STATE:" + json.dumps({"shapeKeys": len(ob.data.shape_keys.key_blocks) if ob.data.shape_keys else 0, "vertexGroups": len(ob.vertex_groups), "modifiers": [m.name + ":" + m.type for m in ob.modifiers]}))`, "ObjectState.py", false); const line = res.python.output.split("\n").find((item) => item.startsWith("OBJECT_STATE:")); return line ? JSON.parse(line.slice("OBJECT_STATE:".length)) : null; }; const operator = async (client: InstanceType, operatorName: string, properties: Record) => { const payload = JSON.stringify({ operator: operatorName, properties, baseRevision: (await client.history()).revision, sceneName: "Scene", areaType: "VIEW_3D", mode: "OBJECT", native: true, selectedObjects: ["BasicCube"], activeObject: "BasicCube", meshSelection: null, nonMeshSelection: null }); return client.executeEmbeddedPython(`import json, web_blender\nweb_blender.desktop_operator(json.loads(${JSON.stringify(payload)}))`, "ObjectTools.py", false); }; const shapeKeys = await run(async (client) => { await operator(client, "object.shape_key_add", { from_mix: false }); const res = await operator(client, "object.shape_key_add", { from_mix: false }); return { pythonState: res.python.state, ...(await inspect(client) ?? {}) }; }); const vertexGroup = await run(async (client) => { const res = await operator(client, "object.vertex_group_add", {}); return { pythonState: res.python.state, ...(await inspect(client) ?? {}) }; }); const modifierCopy = await run(async (client, node) => { const history = await client.history(); const added = await client.applyCommand({ type: "addModifier", objectId: node.id, meshId: node.dataId, modifierType: "SUBSURF", baseRevision: history.revision } as never); const modifier = (added.snapshot.meshes.find((m) => m.id === node.dataId)?.modifierStack ?? []).find((m) => m.type === "SUBSURF"); if (!modifier) throw new Error("SUBSURF was not published"); const res = await operator(client, "object.modifier_copy", { modifier: modifier.name }); const stack = res.snapshot.meshes.find((m) => m.id === node.dataId)?.modifierStack ?? []; return { pythonState: res.python.state, stack: stack.map((m) => m.type) }; }); const applyAsShapeKey = await run(async (client, node) => { const history = await client.history(); const added = await client.applyCommand({ type: "addModifier", objectId: node.id, meshId: node.dataId, modifierType: "SIMPLE_DEFORM", baseRevision: history.revision } as never); const modifier = (added.snapshot.meshes.find((m) => m.id === node.dataId)?.modifierStack ?? []).find((m) => m.type === "SIMPLE_DEFORM"); if (!modifier) throw new Error("SIMPLE_DEFORM was not published"); const res = await operator(client, "object.modifier_apply_as_shapekey", { modifier: modifier.name, keep_modifier: true }); return { pythonState: res.python.state, ...(await inspect(client) ?? {}) }; }); return { shapeKeys, vertexGroup, modifierCopy, applyAsShapeKey }; }, { bytes: Array.from(fixture) }); fs.writeFileSync(path.join(root, "tests/golden/WBV2-P1425-001/verification/object-tools-production.json"), JSON.stringify({ schemaVersion: 1, browser: "chromium", scope: "shape keys, vertex groups, modifier copy, apply as shape key", ...result }, null, 2)); expect(result.shapeKeys).toMatchObject({ ok: true, pythonState: "DONE", shapeKeys: 2 }); expect(result.vertexGroup).toMatchObject({ ok: true, pythonState: "DONE", vertexGroups: 1 }); expect(result.modifierCopy).toMatchObject({ ok: true, pythonState: "DONE", stack: ["SUBSURF", "SUBSURF"] }); expect(result.applyAsShapeKey).toMatchObject({ ok: true, pythonState: "DONE", shapeKeys: 2 }); expect((result.applyAsShapeKey as { modifiers?: string[] }).modifiers?.some((item) => item.endsWith(":SIMPLE_DEFORM"))).toBe(true); });