107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import type { AgentPsdTemplateImageContentBounds } from "@/lib/agents/psd-template-config";
|
|
|
|
type PsdImageLayerCandidate = {
|
|
id: string;
|
|
layerName: string;
|
|
layerPath: string;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
contentBounds?: AgentPsdTemplateImageContentBounds;
|
|
};
|
|
|
|
const BACKGROUND_LAYER_PATTERN = /(背景|底图|bg|background)/i;
|
|
const ADJUSTMENT_LAYER_PATTERN =
|
|
/(色相\s*\/?\s*饱和度|曲线|色阶|亮度\s*\/?\s*对比度|可选颜色|色彩平衡|自然饱和度|曝光|黑白|阈值|反相|海报化|通道混合器|渐变映射|纯色|渐变填充|图案填充|hue\s*\/?\s*saturation|curves?|levels?|brightness\s*\/?\s*contrast|selective color|color balance|vibrance|exposure|black\s*&?\s*white|threshold|invert|posterize|channel mixer|gradient map|solid color|gradient fill|pattern fill)/i;
|
|
const PRIMARY_IMAGE_HINT_PATTERN =
|
|
/(主图|产品|商品|主体|sku|result|photo|image|product|subject|scene|素材)/i;
|
|
|
|
function getCanvasOverlapArea(params: {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
canvasWidth: number;
|
|
canvasHeight: number;
|
|
}) {
|
|
const left = Math.max(0, params.x);
|
|
const top = Math.max(0, params.y);
|
|
const right = Math.min(params.canvasWidth, params.x + params.width);
|
|
const bottom = Math.min(params.canvasHeight, params.y + params.height);
|
|
return Math.max(0, right - left) * Math.max(0, bottom - top);
|
|
}
|
|
|
|
export function isPsdAdjustmentLikeImageLayer(layer: {
|
|
layerName: string;
|
|
layerPath: string;
|
|
}) {
|
|
return (
|
|
ADJUSTMENT_LAYER_PATTERN.test(layer.layerName) ||
|
|
ADJUSTMENT_LAYER_PATTERN.test(layer.layerPath)
|
|
);
|
|
}
|
|
|
|
export function pickPrimaryPsdImageLayerId(params: {
|
|
imageLayers: PsdImageLayerCandidate[];
|
|
canvasWidth: number;
|
|
canvasHeight: number;
|
|
}) {
|
|
const { imageLayers, canvasWidth, canvasHeight } = params;
|
|
if (imageLayers.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const scored = imageLayers.map((layer) => {
|
|
const overlapArea = getCanvasOverlapArea({
|
|
x: layer.x,
|
|
y: layer.y,
|
|
width: layer.width,
|
|
height: layer.height,
|
|
canvasWidth,
|
|
canvasHeight,
|
|
});
|
|
const layerArea = Math.max(1, layer.width * layer.height);
|
|
const visibleArea =
|
|
layer.contentBounds && layer.contentBounds.width > 0 && layer.contentBounds.height > 0
|
|
? layer.contentBounds.width * layer.contentBounds.height
|
|
: overlapArea;
|
|
const coversWholeCanvas =
|
|
overlapArea >= canvasWidth * canvasHeight * 0.96 &&
|
|
layer.x <= 0 &&
|
|
layer.y <= 0 &&
|
|
layer.width >= canvasWidth * 0.96 &&
|
|
layer.height >= canvasHeight * 0.96;
|
|
const isAdjustmentLike = isPsdAdjustmentLikeImageLayer(layer);
|
|
const hasPrimaryImageHint =
|
|
PRIMARY_IMAGE_HINT_PATTERN.test(layer.layerName) ||
|
|
PRIMARY_IMAGE_HINT_PATTERN.test(layer.layerPath);
|
|
const backgroundPenalty =
|
|
BACKGROUND_LAYER_PATTERN.test(layer.layerName) ||
|
|
BACKGROUND_LAYER_PATTERN.test(layer.layerPath) ||
|
|
coversWholeCanvas
|
|
? 0.45
|
|
: 1;
|
|
const adjustmentPenalty = isAdjustmentLike ? 0.05 : 1;
|
|
const oversizePenalty =
|
|
layer.width > canvasWidth * 1.2 || layer.height > canvasHeight * 1.2 ? 0.9 : 1;
|
|
const primaryHintBonus = hasPrimaryImageHint ? 1.2 : 1;
|
|
|
|
return {
|
|
layer,
|
|
score:
|
|
(visibleArea * 1.15 + overlapArea + layerArea * 0.2) *
|
|
backgroundPenalty *
|
|
adjustmentPenalty *
|
|
oversizePenalty *
|
|
primaryHintBonus,
|
|
isAdjustmentLike,
|
|
};
|
|
});
|
|
|
|
const preferredCandidates = scored.filter((entry) => !entry.isAdjustmentLike);
|
|
const candidates = preferredCandidates.length > 0 ? preferredCandidates : scored;
|
|
candidates.sort((left, right) => right.score - left.score);
|
|
return candidates[0]?.layer.id || null;
|
|
}
|