fix: crop layer split overlays by bounds
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-24 03:26:45 +08:00
parent 60c9c57263
commit d2b1fd22d0

View File

@ -63,6 +63,16 @@ interface ImageDisplayMetrics {
naturalHeight: number; naturalHeight: number;
} }
interface LayerSplitBounds {
x: number;
y: number;
width: number;
height: number;
originalWidth: number;
originalHeight: number;
coverage: number;
}
interface BoxCutoutPoint { interface BoxCutoutPoint {
x: number; x: number;
y: number; y: number;
@ -399,6 +409,56 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
}, [detailSubjectAnalysis, detailUiDslDocumentFromMetadata, detailUiScreenOverlay]); }, [detailSubjectAnalysis, detailUiDslDocumentFromMetadata, detailUiScreenOverlay]);
const shouldRenderDetailOverlay = const shouldRenderDetailOverlay =
data.metadata?.detailGenerationMode === "agent_layout" && Boolean(detailUiScreenOverlay); data.metadata?.detailGenerationMode === "agent_layout" && Boolean(detailUiScreenOverlay);
const layerSplitBounds = useMemo<LayerSplitBounds | null>(() => {
const raw = data.metadata?.bounds;
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
const x = Number((raw as any).x);
const y = Number((raw as any).y);
const width = Number((raw as any).width);
const height = Number((raw as any).height);
const originalWidth = Number((raw as any).originalWidth);
const originalHeight = Number((raw as any).originalHeight);
const coverage = Number((raw as any).coverage);
if (
!Number.isFinite(x) ||
!Number.isFinite(y) ||
!Number.isFinite(width) ||
!Number.isFinite(height) ||
!Number.isFinite(originalWidth) ||
!Number.isFinite(originalHeight) ||
width <= 0 ||
height <= 0 ||
originalWidth <= 0 ||
originalHeight <= 0
) {
return null;
}
return {
x,
y,
width,
height,
originalWidth,
originalHeight,
coverage: Number.isFinite(coverage) ? coverage : 0,
};
}, [data.metadata]);
const layerSplitCropStyle = useMemo<React.CSSProperties | undefined>(() => {
if (!layerSplitBounds) return undefined;
return {
position: "absolute",
width: `${(layerSplitBounds.originalWidth / layerSplitBounds.width) * 100}%`,
height: `${(layerSplitBounds.originalHeight / layerSplitBounds.height) * 100}%`,
maxWidth: "none",
maxHeight: "none",
left: `${-(layerSplitBounds.x / layerSplitBounds.width) * 100}%`,
top: `${-(layerSplitBounds.y / layerSplitBounds.height) * 100}%`,
};
}, [layerSplitBounds]);
const shortAgentExecutionSnapshotId = agentExecutionSnapshotId const shortAgentExecutionSnapshotId = agentExecutionSnapshotId
? agentExecutionSnapshotId.slice(0, 8) ? agentExecutionSnapshotId.slice(0, 8)
: ""; : "";
@ -1978,7 +2038,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
return; return;
} }
handleLayerSplitComplete( const addedLayerCount = handleLayerSplitComplete(
layers.map((layer) => ({ layers.map((layer) => ({
originalUrl: layer.originalUrl, originalUrl: layer.originalUrl,
title: layer.title, title: layer.title,
@ -1987,7 +2047,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
bounds: layer.bounds ?? null, bounds: layer.bounds ?? null,
})) }))
); );
toast.success(`图层拆分完成,已新增 ${layers.length} 个图层结果`); toast.success(`图层拆分完成,已新增 ${addedLayerCount} 个图层结果`);
} catch (error) { } catch (error) {
console.error("图层拆分失败:", error); console.error("图层拆分失败:", error);
toast.error("图层拆分失败"); toast.error("图层拆分失败");
@ -2208,12 +2268,20 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
throw new Error("没有可添加到画布的图层结果"); throw new Error("没有可添加到画布的图层结果");
} }
const orderedLayers = [...validLayers].sort((left, right) => { const isFullFrameLayer = (bounds?: LayerSplitBounds | null) => {
const leftArea = (left.bounds?.width || node.size.width) * (left.bounds?.height || node.size.height); if (!bounds) return false;
const rightArea = return (
(right.bounds?.width || node.size.width) * (right.bounds?.height || node.size.height); bounds.coverage >= 0.92 &&
return rightArea - leftArea; bounds.width >= bounds.originalWidth - 2 &&
}); bounds.height >= bounds.originalHeight - 2
);
};
const drawableLayers = validLayers.filter((layer) => !isFullFrameLayer(layer.bounds));
const orderedLayers =
(drawableLayers.length > 0 ? drawableLayers : validLayers).sort(
(left, right) => (left.layerIndex || 0) - (right.layerIndex || 0)
);
orderedLayers.forEach((layer, index) => { orderedLayers.forEach((layer, index) => {
const layerTitle = `${displayTitle}-${layer.title || `图层${layer.layerIndex || index + 1}`}`; const layerTitle = `${displayTitle}-${layer.title || `图层${layer.layerIndex || index + 1}`}`;
@ -2263,6 +2331,8 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
connections: [node.id], connections: [node.id],
}); });
}); });
return orderedLayers.length;
}, },
[addNode, data.src, displayTitle, node.id, node.position.x, node.position.y, node.size.height, node.size.width] [addNode, data.src, displayTitle, node.id, node.position.x, node.position.y, node.size.height, node.size.width]
); );
@ -2655,9 +2725,12 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
src={displayImageSrc} src={displayImageSrc}
alt={displayTitle} alt={displayTitle}
className={cn( className={cn(
"w-full h-full object-contain bg-muted/30", layerSplitCropStyle
? "absolute bg-transparent"
: "w-full h-full object-contain bg-muted/30",
boxCutoutMode ? "cursor-crosshair" : isThisNodePicking ? "cursor-crosshair" : "cursor-pointer" boxCutoutMode ? "cursor-crosshair" : isThisNodePicking ? "cursor-crosshair" : "cursor-pointer"
)} )}
style={layerSplitCropStyle}
draggable={false} draggable={false}
loading="lazy" loading="lazy"
decoding="async" decoding="async"