ai-chatbot/lib/agents/psd-template-image-slot-selection.ts
Your Name 11ab9a8e03
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: organize psd assets by brand and harden template editor
2026-04-02 18:29:41 +08:00

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;
}