fix: center canvas agent writebacks without anchors
This commit is contained in:
parent
38dba2b1bc
commit
1e41335ebb
@ -1211,7 +1211,7 @@ canvasViewOperationPlan?: CanvasViewOperationPlan;
|
||||
|
||||
- 有 selected slot:`fill_slot`
|
||||
- 有 selected image:`insert_beside`
|
||||
- 无选中:`insert_beside` 当前视口或 `viewport-center`
|
||||
- 无选中:`insert_beside` + `placement = "viewport-center"`,并在 `metadata.canvasAgentViewport` 写入请求时的 viewport(含 width/height),服务端 mutation 按真实当前视口中心计算落点
|
||||
|
||||
图片编辑:
|
||||
|
||||
|
||||
@ -168,6 +168,7 @@ export function buildCanvasAgentDirectWritebackPlan(
|
||||
: params.mode === "edit"
|
||||
? "append_revision"
|
||||
: "insert_beside";
|
||||
const hasWritebackAnchor = Boolean(anchorNode?.nodeId);
|
||||
const mediaItems: CanvasMediaItem[] = resultUrls.map((url, index) => {
|
||||
const mediaItemId = `${params.assistantMessageId}:${params.operationLabel}:${index + 1}`;
|
||||
const itemSourceNodeIds = perResultSourceNodeIds[index]
|
||||
@ -206,7 +207,7 @@ export function buildCanvasAgentDirectWritebackPlan(
|
||||
sourceNodeIds,
|
||||
targetSlotId: targetSlotId || undefined,
|
||||
anchorNodeId: shouldFillSlot ? undefined : anchorNode?.nodeId,
|
||||
placement: "right",
|
||||
placement: hasWritebackAnchor ? "right" : "viewport-center",
|
||||
placementStrategy:
|
||||
perResultSourceNodeIds.length === mediaItems.length
|
||||
? "beside_each_source"
|
||||
@ -220,6 +221,7 @@ export function buildCanvasAgentDirectWritebackPlan(
|
||||
chatId: params.chatId,
|
||||
mode: params.mode,
|
||||
prompt: params.prompt || params.requestTextContent || "",
|
||||
canvasAgentViewport: params.context.viewport,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -17,6 +17,14 @@ export type ResolvedCanvasAgentMediaPlacement = {
|
||||
edgeSourceId?: string;
|
||||
};
|
||||
|
||||
export type CanvasAgentPlacementViewport = {
|
||||
x: number;
|
||||
y: number;
|
||||
zoom: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
function uniqueStrings(values: Array<string | undefined | null>) {
|
||||
return Array.from(
|
||||
new Set(values.map((value) => String(value || "").trim()).filter(Boolean))
|
||||
@ -40,11 +48,51 @@ function getNodeBounds(node?: CanvasAgentPlacementNode | null) {
|
||||
};
|
||||
}
|
||||
|
||||
function getViewportCenter(viewport?: { x: number; y: number; zoom: number }) {
|
||||
const zoom = Math.max(0.1, Number(viewport?.zoom || 1) || 1);
|
||||
function toFiniteNumber(value: unknown) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function normalizePlacementViewport(
|
||||
value: unknown
|
||||
): CanvasAgentPlacementViewport | undefined {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
||||
const raw = value as Record<string, unknown>;
|
||||
const x = toFiniteNumber(raw.x);
|
||||
const y = toFiniteNumber(raw.y);
|
||||
const zoom = toFiniteNumber(raw.zoom);
|
||||
if (x === undefined || y === undefined || zoom === undefined) return undefined;
|
||||
return {
|
||||
x: -Number(viewport?.x || 0) / zoom,
|
||||
y: -Number(viewport?.y || 0) / zoom,
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
width: toFiniteNumber(raw.width),
|
||||
height: toFiniteNumber(raw.height),
|
||||
};
|
||||
}
|
||||
|
||||
function getPlanViewport(plan: CanvasOperationPlan) {
|
||||
const metadata =
|
||||
plan.metadata && typeof plan.metadata === "object" && !Array.isArray(plan.metadata)
|
||||
? (plan.metadata as Record<string, unknown>)
|
||||
: {};
|
||||
return normalizePlacementViewport(metadata.canvasAgentViewport);
|
||||
}
|
||||
|
||||
function getEffectiveViewport(params: {
|
||||
plan: CanvasOperationPlan;
|
||||
viewport?: CanvasAgentPlacementViewport;
|
||||
}) {
|
||||
return getPlanViewport(params.plan) || params.viewport;
|
||||
}
|
||||
|
||||
function getViewportCenter(viewport?: CanvasAgentPlacementViewport) {
|
||||
const zoom = Math.max(0.1, Number(viewport?.zoom || 1) || 1);
|
||||
const width = toFiniteNumber(viewport?.width);
|
||||
const height = toFiniteNumber(viewport?.height);
|
||||
return {
|
||||
x: ((width && width > 0 ? width / 2 : 0) - Number(viewport?.x || 0)) / zoom,
|
||||
y: ((height && height > 0 ? height / 2 : 0) - Number(viewport?.y || 0)) / zoom,
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,7 +102,7 @@ function getViewportCenteredPosition(params: {
|
||||
groupIndex: number;
|
||||
size: { width: number; height: number };
|
||||
strategy: CanvasOperationPlan["placementStrategy"];
|
||||
viewport?: { x: number; y: number; zoom: number };
|
||||
viewport?: CanvasAgentPlacementViewport;
|
||||
}) {
|
||||
const stepGap = 24;
|
||||
const center = getViewportCenter(params.viewport);
|
||||
@ -155,7 +203,7 @@ function resolvePosition(params: {
|
||||
size: { width: number; height: number };
|
||||
anchorNode?: CanvasAgentPlacementNode | null;
|
||||
targetSlot?: CanvasAgentPlacementNode | null;
|
||||
viewport?: { x: number; y: number; zoom: number };
|
||||
viewport?: CanvasAgentPlacementViewport;
|
||||
}) {
|
||||
const gap = 40;
|
||||
const stepGap = 24;
|
||||
@ -258,9 +306,13 @@ export function resolveCanvasAgentMediaPlacements(params: {
|
||||
mediaItems: CanvasMediaItem[];
|
||||
nodes: CanvasAgentPlacementNode[];
|
||||
targetSlot?: CanvasAgentPlacementNode | null;
|
||||
viewport?: { x: number; y: number; zoom: number };
|
||||
viewport?: CanvasAgentPlacementViewport;
|
||||
}): ResolvedCanvasAgentMediaPlacement[] {
|
||||
const groupCounts = new Map<string, number>();
|
||||
const viewport = getEffectiveViewport({
|
||||
plan: params.plan,
|
||||
viewport: params.viewport,
|
||||
});
|
||||
|
||||
return params.mediaItems.map((item, index) => {
|
||||
const sourceNodeIds = getItemSourceNodeIds(item, params.plan);
|
||||
@ -289,7 +341,7 @@ export function resolveCanvasAgentMediaPlacements(params: {
|
||||
size,
|
||||
anchorNode,
|
||||
targetSlot: params.targetSlot,
|
||||
viewport: params.viewport,
|
||||
viewport,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -676,6 +676,53 @@ test.describe("canvas chat pure helpers", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("plans direct generations into the current viewport when no anchor exists", () => {
|
||||
const context: CanvasAgentContext = {
|
||||
...plannerContext,
|
||||
selectedNodeIds: [],
|
||||
selectedNodes: [],
|
||||
candidateNodes: [],
|
||||
slots: [],
|
||||
annotations: [],
|
||||
viewport: { x: -200, y: -100, zoom: 1, width: 1000, height: 800 },
|
||||
focusPolicy: "auto",
|
||||
};
|
||||
|
||||
const result = buildCanvasAgentDirectWritebackPlan({
|
||||
projectId: plannerContext.projectId || "",
|
||||
chatId: "chat-1",
|
||||
assistantMessageId: "assistant-no-anchor",
|
||||
operationLabel: "agent-generate-image",
|
||||
resultType: "image",
|
||||
resultUrls: ["https://cdn.example.com/no-anchor.png"],
|
||||
prompt: "生成一张主图",
|
||||
mode: "generate",
|
||||
context,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
operation: "insert_beside",
|
||||
shouldFillSlot: false,
|
||||
plan: {
|
||||
operation: "insert_beside",
|
||||
anchorNodeId: undefined,
|
||||
placement: "viewport-center",
|
||||
placementStrategy: "single",
|
||||
sourceNodeIds: [],
|
||||
metadata: {
|
||||
canvasAgentViewport: {
|
||||
x: -200,
|
||||
y: -100,
|
||||
zoom: 1,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result?.mediaItems[0]?.sourceNodeIds).toEqual([]);
|
||||
});
|
||||
|
||||
test("does not build direct writeback plans without valid result urls", () => {
|
||||
expect(
|
||||
buildCanvasAgentDirectWritebackPlan({
|
||||
@ -2244,6 +2291,37 @@ test.describe("canvas chat pure helpers", () => {
|
||||
{ x: 88, y: 60 },
|
||||
{ x: 212, y: 60 },
|
||||
]);
|
||||
|
||||
const viewportAwarePlacements = resolveCanvasAgentMediaPlacements({
|
||||
plan: {
|
||||
version: 1,
|
||||
operationId: "op-center-with-stage",
|
||||
operation: "insert_beside",
|
||||
projectId: "project-1",
|
||||
placement: "viewport-center",
|
||||
placementStrategy: "grid",
|
||||
metadata: {
|
||||
canvasAgentViewport: {
|
||||
x: -200,
|
||||
y: -100,
|
||||
zoom: 1,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
},
|
||||
},
|
||||
},
|
||||
mediaItems: [
|
||||
{ mediaItemId: "media-1", type: "image", url: "https://cdn.example.com/1.png", width: 100, height: 80 },
|
||||
{ mediaItemId: "media-2", type: "image", url: "https://cdn.example.com/2.png", width: 100, height: 80 },
|
||||
],
|
||||
nodes: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
});
|
||||
|
||||
expect(viewportAwarePlacements.map((placement) => placement.position)).toEqual([
|
||||
{ x: 588, y: 460 },
|
||||
{ x: 712, y: 460 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("normalizes slot-only canvas agent context without image nodes", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user