Browse Source

fix: use type-specific dimensions for node-to-group drag assignment

handleNodeDragStop used a hardcoded 300x280 fallback for node dimensions,
causing miscalculated centers for non-standard nodes (easeCurve 340x480,
videoStitch 400x280, audioInput 300x200). Now uses the same 3-level
fallback (measured → style → defaultNodeDimensions) as createGroup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
99e9e4e9f9
  1. 6
      src/components/WorkflowCanvas.tsx
  2. 262
      src/store/__tests__/workflowStore.integration.test.ts

6
src/components/WorkflowCanvas.tsx

@ -42,6 +42,7 @@ import { EdgeToolbar } from "./EdgeToolbar";
import { GlobalImageHistory } from "./GlobalImageHistory"; import { GlobalImageHistory } from "./GlobalImageHistory";
import { GroupBackgroundsPortal, GroupControlsOverlay } from "./GroupsOverlay"; import { GroupBackgroundsPortal, GroupControlsOverlay } from "./GroupsOverlay";
import { NodeType, NanoBananaNodeData } from "@/types"; import { NodeType, NanoBananaNodeData } from "@/types";
import { defaultNodeDimensions } from "@/store/utils/nodeDefaults";
import { detectAndSplitGrid } from "@/utils/gridSplitter"; import { detectAndSplitGrid } from "@/utils/gridSplitter";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
import { WelcomeModal } from "./quickstart"; import { WelcomeModal } from "./quickstart";
@ -256,8 +257,9 @@ export function WorkflowCanvas() {
// Skip if it's a group node // Skip if it's a group node
if (node.id.startsWith("group-")) return; if (node.id.startsWith("group-")) return;
const nodeWidth = (node.style?.width as number) || 300; const defaults = defaultNodeDimensions[node.type as NodeType] || { width: 300, height: 280 };
const nodeHeight = (node.style?.height as number) || 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 nodeCenterX = node.position.x + nodeWidth / 2;
const nodeCenterY = node.position.y + nodeHeight / 2; const nodeCenterY = node.position.y + nodeHeight / 2;

262
src/store/__tests__/workflowStore.integration.test.ts

@ -2438,4 +2438,266 @@ describe("workflowStore integration tests", () => {
expect(mockFetch).toHaveBeenCalledTimes(3); 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)
});
});
});
}); });

Loading…
Cancel
Save