feat(canvas): enhance mixer node functionality with overlay dimensions and cropping support

This commit is contained in:
2026-04-15 08:49:54 +02:00
parent 2679a0cc4e
commit 7a06e0db7f
4 changed files with 383 additions and 10 deletions

View File

@@ -543,6 +543,74 @@ describe("useCanvasConnections", () => {
expect(latestHandlersRef.current?.connectionDropMenu).toBeNull();
});
it("rejects self-drops on a note instead of auto-splitting its incoming edge", async () => {
const runCreateEdgeMutation = vi.fn(async () => undefined);
const runSplitEdgeAtExistingNodeMutation = vi.fn(async () => undefined);
const showConnectionRejectedToast = vi.fn();
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(
<HookHarness
helperResult={{
sourceNodeId: "node-note",
targetNodeId: "node-note",
sourceHandle: undefined,
targetHandle: undefined,
}}
runCreateEdgeMutation={runCreateEdgeMutation}
runSplitEdgeAtExistingNodeMutation={runSplitEdgeAtExistingNodeMutation}
showConnectionRejectedToast={showConnectionRejectedToast}
nodes={[
{ id: "node-image", type: "image", position: { x: 0, y: 0 }, data: {} },
{ id: "node-note", type: "note", position: { x: 240, y: 120 }, data: {} },
]}
edges={[
{
id: "edge-image-note",
source: "node-image",
target: "node-note",
},
]}
/>,
);
});
await act(async () => {
latestHandlersRef.current?.onConnectStart?.(
{} as MouseEvent,
{
nodeId: "node-note",
handleId: null,
handleType: "source",
} as never,
);
latestHandlersRef.current?.onConnectEnd(
{ clientX: 260, clientY: 160 } as MouseEvent,
{
isValid: false,
from: { x: 0, y: 0 },
fromNode: { id: "node-note", type: "note" },
fromHandle: { id: null, type: "source" },
fromPosition: null,
to: { x: 260, y: 160 },
toHandle: null,
toNode: null,
toPosition: null,
pointer: null,
} as never,
);
});
expect(runSplitEdgeAtExistingNodeMutation).not.toHaveBeenCalled();
expect(runCreateEdgeMutation).not.toHaveBeenCalled();
expect(showConnectionRejectedToast).toHaveBeenCalledWith("self-loop");
expect(latestHandlersRef.current?.connectionDropMenu).toBeNull();
});
it("rejects text to ai-video body drops", async () => {
const runCreateEdgeMutation = vi.fn(async () => undefined);
const showConnectionRejectedToast = vi.fn();

View File

@@ -414,7 +414,7 @@ export function useCanvasConnections({
!isOptimisticEdgeId(edge.id),
);
const incomingEdge = incomingEdges.length === 1 ? incomingEdges[0] : undefined;
const splitValidationError =
const shouldAttemptAutoSplit =
validationError === "adjustment-incoming-limit" &&
droppedConnection.sourceNodeId === fromNode.id &&
fromHandle.type === "source" &&
@@ -424,16 +424,31 @@ export function useCanvasConnections({
hasHandleKey(splitHandles, "target") &&
incomingEdge !== undefined &&
incomingEdge.source !== fullFromNode.id &&
incomingEdge.target !== fullFromNode.id
? validateCanvasEdgeSplit({
nodes: nodesRef.current,
edges: edgesRef.current,
splitEdge: incomingEdge,
middleNode: fullFromNode,
})
: null;
incomingEdge.target !== fullFromNode.id;
const splitValidationError = shouldAttemptAutoSplit
? validateCanvasEdgeSplit({
nodes: nodesRef.current,
edges: edgesRef.current,
splitEdge: incomingEdge,
middleNode: fullFromNode,
})
: null;
if (!splitValidationError && incomingEdge && fullFromNode && splitHandles) {
logCanvasConnectionDebug("connect:end-auto-split-eval", {
point: pt,
flow,
droppedConnection,
validationError,
shouldAttemptAutoSplit,
splitValidationError,
fromNodeId: fromNode.id,
fromNodeType: fullFromNode?.type ?? null,
incomingEdgeId: incomingEdge?.id ?? null,
incomingEdgeSourceNodeId: incomingEdge?.source ?? null,
incomingEdgeTargetNodeId: incomingEdge?.target ?? null,
});
if (shouldAttemptAutoSplit && !splitValidationError && incomingEdge && fullFromNode && splitHandles) {
logCanvasConnectionDebug("connect:end-auto-split", {
point: pt,
flow,