fix: preserve layer split overlay positions
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:07:40 +08:00
parent 365eabead1
commit 15c57afdfa
4 changed files with 128 additions and 13 deletions

View File

@ -2188,6 +2188,15 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
title?: string;
layerIndex: number;
totalLayers: number;
bounds?: {
x: number;
y: number;
width: number;
height: number;
originalWidth: number;
originalHeight: number;
coverage: number;
} | null;
}>
) => {
const validLayers = layers.filter(
@ -2198,23 +2207,44 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
throw new Error("没有可添加到画布的图层结果");
}
const maxColumns = 3;
const gap = 20;
const baseX = node.position.x + node.size.width + gap;
const baseY = node.position.y;
const orderedLayers = [...validLayers].sort((left, right) => {
const leftArea = (left.bounds?.width || node.size.width) * (left.bounds?.height || node.size.height);
const rightArea =
(right.bounds?.width || node.size.width) * (right.bounds?.height || node.size.height);
return rightArea - leftArea;
});
validLayers.forEach((layer, index) => {
const column = index % maxColumns;
const row = Math.floor(index / maxColumns);
orderedLayers.forEach((layer, index) => {
const layerTitle = `${displayTitle}-${layer.title || `图层${layer.layerIndex || index + 1}`}`;
const bounds = layer.bounds;
const hasUsableBounds =
bounds &&
bounds.originalWidth > 0 &&
bounds.originalHeight > 0 &&
bounds.width > 0 &&
bounds.height > 0;
const scaleX = hasUsableBounds ? node.size.width / bounds.originalWidth : 1;
const scaleY = hasUsableBounds ? node.size.height / bounds.originalHeight : 1;
const targetPosition = hasUsableBounds
? {
x: node.position.x + bounds.x * scaleX,
y: node.position.y + bounds.y * scaleY,
}
: {
x: node.position.x,
y: node.position.y,
};
const targetSize = hasUsableBounds
? {
width: Math.max(1, bounds.width * scaleX),
height: Math.max(1, bounds.height * scaleY),
}
: { width: node.size.width, height: node.size.height };
addNode({
type: "image",
position: {
x: baseX + column * (node.size.width + gap),
y: baseY + row * (node.size.height + gap),
},
size: { width: node.size.width, height: node.size.height },
position: targetPosition,
size: targetSize,
data: {
src: layer.originalUrl,
status: "completed",
@ -2224,6 +2254,9 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
metadata: {
layerIndex: layer.layerIndex,
totalLayers: layer.totalLayers,
bounds: layer.bounds,
allowOverlap: true,
placementMode: "overlay",
},
},
connections: [node.id],

View File

@ -60,6 +60,15 @@ export type LayerSplitResultItem = {
title: string;
layerIndex: number;
totalLayers: number;
bounds?: {
x: number;
y: number;
width: number;
height: number;
originalWidth: number;
originalHeight: number;
coverage: number;
} | null;
};
export type ExecuteLayerSplitJobResult = {
@ -438,6 +447,60 @@ async function createThumbnail(buffer: Buffer, mimeType: string): Promise<Buffer
}
}
async function detectVisibleBounds(buffer: Buffer) {
try {
const sharp = await import("sharp");
const { data, info } = await sharp.default(buffer)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const width = Number(info.width || 0);
const height = Number(info.height || 0);
const channels = Number(info.channels || 4);
if (width <= 0 || height <= 0 || channels < 4) {
return null;
}
let minX = width;
let minY = height;
let maxX = -1;
let maxY = -1;
let visiblePixels = 0;
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
const offset = (y * width + x) * channels;
const alpha = data[offset + 3];
if (alpha <= 8) continue;
visiblePixels += 1;
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
}
}
if (maxX < minX || maxY < minY || visiblePixels <= 0) {
return null;
}
return {
x: minX,
y: minY,
width: maxX - minX + 1,
height: maxY - minY + 1,
originalWidth: width,
originalHeight: height,
coverage: visiblePixels / (width * height),
};
} catch (error) {
console.warn("[LayerSplit] Failed to detect visible bounds:", error);
return null;
}
}
async function uploadBufferPair(buffer: Buffer, mimeType: string, prefix: string) {
const ext = extFromMimeType(mimeType);
const timestamp = Date.now();
@ -571,6 +634,7 @@ export async function executeLayerSplitJob(params: LayerSplitParams): Promise<Ex
for (let index = 0; index < layerBuffers.length; index += 1) {
const layer = layerBuffers[index];
const visibleBounds = await detectVisibleBounds(layer.buffer);
const uploadResult = await uploadBufferPair(layer.buffer, layer.mimeType, `layer-split-${index + 1}`);
let generatedImageId: string | null = null;
let mediaAssetId: string | null = null;
@ -615,6 +679,7 @@ export async function executeLayerSplitJob(params: LayerSplitParams): Promise<Ex
model: config.replicateModel,
sourceImageUrl: params.imageUrl,
sourceFilename: layer.filename,
bounds: visibleBounds,
},
});
mediaAssetId = media?.id || null;
@ -647,6 +712,7 @@ export async function executeLayerSplitJob(params: LayerSplitParams): Promise<Ex
title: `图层${index + 1}`,
layerIndex: index + 1,
totalLayers,
bounds: visibleBounds,
});
}

View File

@ -316,6 +316,15 @@ export const imageGenerationService = {
title: string;
layerIndex: number;
totalLayers: number;
bounds?: {
x: number;
y: number;
width: number;
height: number;
originalWidth: number;
originalHeight: number;
coverage: number;
} | null;
}>;
model?: string;
provider?: string;

View File

@ -209,12 +209,19 @@ export const useCanvasStore = create<CanvasStore>()(
// 节点操作
addNode: (nodeData) => {
const imageMetadata =
nodeData.type === 'image' && nodeData.data && typeof nodeData.data === 'object'
? (((nodeData.data as any).metadata ?? {}) as Record<string, unknown>)
: {};
const shouldPreserveImagePosition =
imageMetadata.allowOverlap === true || imageMetadata.placementMode === 'overlay';
const resolveImagePosition = (
basePosition: Position,
size: Size,
nodes: CanvasNode[]
): Position => {
if (nodeData.type !== 'image') return basePosition;
if (nodeData.type !== 'image' || shouldPreserveImagePosition) return basePosition;
const padding = 20;
const width = size?.width || 300;
const height = size?.height || 300;