fix: preserve layer split overlay positions
This commit is contained in:
parent
365eabead1
commit
15c57afdfa
@ -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],
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user