diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 4da48eaf..8d6d0671 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -42,6 +42,7 @@ import { EdgeToolbar } from "./EdgeToolbar"; import { GlobalImageHistory } from "./GlobalImageHistory"; import { GroupBackgroundsPortal, GroupControlsOverlay } from "./GroupsOverlay"; import { NodeType, NanoBananaNodeData } from "@/types"; +import { defaultNodeDimensions } from "@/store/utils/nodeDefaults"; import { detectAndSplitGrid } from "@/utils/gridSplitter"; import { logger } from "@/utils/logger"; import { WelcomeModal } from "./quickstart"; @@ -256,8 +257,9 @@ export function WorkflowCanvas() { // Skip if it's a group node if (node.id.startsWith("group-")) return; - const nodeWidth = (node.style?.width as number) || 300; - const nodeHeight = (node.style?.height as number) || 280; + const defaults = defaultNodeDimensions[node.type as NodeType] || { width: 300, height: 280 }; + const nodeWidth = node.measured?.width || (node.style?.width as number) || defaults.width; + const nodeHeight = node.measured?.height || (node.style?.height as number) || defaults.height; const nodeCenterX = node.position.x + nodeWidth / 2; const nodeCenterY = node.position.y + nodeHeight / 2; diff --git a/src/store/__tests__/workflowStore.integration.test.ts b/src/store/__tests__/workflowStore.integration.test.ts index 192f37bd..78a91afa 100644 --- a/src/store/__tests__/workflowStore.integration.test.ts +++ b/src/store/__tests__/workflowStore.integration.test.ts @@ -2438,4 +2438,266 @@ describe("workflowStore integration tests", () => { expect(mockFetch).toHaveBeenCalledTimes(3); }); }); + + describe("Group operations with non-standard node types", () => { + describe("createGroup bounding box calculation", () => { + it("should correctly calculate bounding box for easeCurve nodes (340x480)", () => { + useWorkflowStore.setState({ + nodes: [ + createTestNode("ease-1", "easeCurve", { + bezierHandles: [0.445, 0.05, 0.55, 0.95], + easingPreset: "easeInOutSine", + inheritedFrom: null, + outputDuration: 1.5, + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }, { x: 100, y: 100 }), + createTestNode("ease-2", "easeCurve", { + bezierHandles: [0.445, 0.05, 0.55, 0.95], + easingPreset: "easeInOutSine", + inheritedFrom: null, + outputDuration: 1.5, + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }, { x: 500, y: 100 }), + ], + edges: [], + groups: {}, + }); + + const store = useWorkflowStore.getState(); + const groupId = store.createGroup(["ease-1", "ease-2"]); + + expect(groupId).toBeTruthy(); + const group = useWorkflowStore.getState().groups[groupId]; + // easeCurve is 340x480, so maxX = 500 + 340 = 840, maxY = 100 + 480 = 580 + // With padding=20 and headerHeight=32: position.x = 100-20=80, width = 840-100+40=780 + expect(group.size.width).toBeGreaterThanOrEqual(740); // Must account for 340px wide nodes + expect(group.size.height).toBeGreaterThanOrEqual(480); // Must account for 480px tall nodes + }); + + it("should correctly calculate bounding box for videoStitch nodes (400x280)", () => { + useWorkflowStore.setState({ + nodes: [ + createTestNode("vs-1", "videoStitch", { + clips: [], + clipOrder: [], + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }, { x: 0, y: 0 }), + ], + edges: [], + groups: {}, + }); + + const store = useWorkflowStore.getState(); + const groupId = store.createGroup(["vs-1"]); + + const group = useWorkflowStore.getState().groups[groupId]; + // videoStitch is 400x280, with padding=20: width = 400+40=440 + expect(group.size.width).toBeGreaterThanOrEqual(400); // Must account for 400px wide node + expect(group.size.height).toBeGreaterThanOrEqual(280); // Must account for 280px tall node + }); + + it("should correctly calculate bounding box for audioInput nodes (300x200)", () => { + useWorkflowStore.setState({ + nodes: [ + createTestNode("audio-1", "audioInput", { + audioFile: null, + filename: null, + duration: null, + format: null, + }, { x: 200, y: 200 }), + ], + edges: [], + groups: {}, + }); + + const store = useWorkflowStore.getState(); + const groupId = store.createGroup(["audio-1"]); + + const group = useWorkflowStore.getState().groups[groupId]; + // audioInput is 300x200, with padding=20: width = 300+40=340 + expect(group.size.width).toBeGreaterThanOrEqual(300); + expect(group.size.height).toBeGreaterThanOrEqual(200); + }); + }); + + describe("addNodesToGroup with non-standard node types", () => { + it("should assign groupId to easeCurve, videoStitch, and audioInput nodes", () => { + useWorkflowStore.setState({ + nodes: [ + createTestNode("ease-1", "easeCurve", { + bezierHandles: [0.445, 0.05, 0.55, 0.95], + easingPreset: "easeInOutSine", + inheritedFrom: null, + outputDuration: 1.5, + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }), + createTestNode("vs-1", "videoStitch", { + clips: [], + clipOrder: [], + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }), + createTestNode("audio-1", "audioInput", { + audioFile: null, + filename: null, + duration: null, + format: null, + }), + ], + edges: [], + groups: { + "group-1": { + id: "group-1", + name: "Test Group", + color: "neutral" as const, + position: { x: 0, y: 0 }, + size: { width: 800, height: 600 }, + locked: false, + }, + }, + }); + + const store = useWorkflowStore.getState(); + store.addNodesToGroup(["ease-1", "vs-1", "audio-1"], "group-1"); + + const nodes = useWorkflowStore.getState().nodes; + expect(nodes.find((n) => n.id === "ease-1")?.groupId).toBe("group-1"); + expect(nodes.find((n) => n.id === "vs-1")?.groupId).toBe("group-1"); + expect(nodes.find((n) => n.id === "audio-1")?.groupId).toBe("group-1"); + }); + }); + + describe("setNodeGroupId with non-standard node types", () => { + it("should assign and remove groupId for easeCurve, videoStitch, and audioInput nodes", () => { + useWorkflowStore.setState({ + nodes: [ + createTestNode("ease-1", "easeCurve", { + bezierHandles: [0.445, 0.05, 0.55, 0.95], + easingPreset: "easeInOutSine", + inheritedFrom: null, + outputDuration: 1.5, + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }), + createTestNode("vs-1", "videoStitch", { + clips: [], + clipOrder: [], + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }), + createTestNode("audio-1", "audioInput", { + audioFile: null, + filename: null, + duration: null, + format: null, + }), + ], + edges: [], + groups: {}, + }); + + const store = useWorkflowStore.getState(); + + // Assign groupId + store.setNodeGroupId("ease-1", "group-1"); + store.setNodeGroupId("vs-1", "group-1"); + store.setNodeGroupId("audio-1", "group-1"); + + let nodes = useWorkflowStore.getState().nodes; + expect(nodes.find((n) => n.id === "ease-1")?.groupId).toBe("group-1"); + expect(nodes.find((n) => n.id === "vs-1")?.groupId).toBe("group-1"); + expect(nodes.find((n) => n.id === "audio-1")?.groupId).toBe("group-1"); + + // Remove groupId + store.setNodeGroupId("ease-1", undefined); + store.setNodeGroupId("vs-1", undefined); + store.setNodeGroupId("audio-1", undefined); + + nodes = useWorkflowStore.getState().nodes; + expect(nodes.find((n) => n.id === "ease-1")?.groupId).toBeUndefined(); + expect(nodes.find((n) => n.id === "vs-1")?.groupId).toBeUndefined(); + expect(nodes.find((n) => n.id === "audio-1")?.groupId).toBeUndefined(); + }); + }); + + describe("Locked group execution with non-standard node types", () => { + it("should skip easeCurve, videoStitch, and audioInput nodes in locked groups", async () => { + useWorkflowStore.setState({ + nodes: [ + { ...createTestNode("ease-1", "easeCurve", { + bezierHandles: [0.445, 0.05, 0.55, 0.95], + easingPreset: "easeInOutSine", + inheritedFrom: null, + outputDuration: 1.5, + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }), groupId: "group-locked" }, + { ...createTestNode("vs-1", "videoStitch", { + clips: [], + clipOrder: [], + outputVideo: null, + status: "idle", + error: null, + progress: 0, + encoderSupported: null, + }), groupId: "group-locked" }, + { ...createTestNode("audio-1", "audioInput", { + audioFile: null, + filename: null, + duration: null, + format: null, + }), groupId: "group-locked" }, + // Unlocked node + createTestNode("prompt-1", "prompt", { prompt: "test" }), + ], + edges: [], + groups: { + "group-locked": { + id: "group-locked", + name: "Locked Group", + color: "neutral" as const, + position: { x: 0, y: 0 }, + size: { width: 800, height: 600 }, + locked: true, + }, + }, + }); + + const store = useWorkflowStore.getState(); + await store.executeWorkflow(); + + // Workflow should complete without errors + expect(useWorkflowStore.getState().isRunning).toBe(false); + // The locked nodes should have been skipped (no API calls, no errors) + }); + }); + }); });