diff --git a/app/api/admin/psd-templates/[id]/parse/route.ts b/app/api/admin/psd-templates/[id]/parse/route.ts index 47d94effc..e24b328a4 100644 --- a/app/api/admin/psd-templates/[id]/parse/route.ts +++ b/app/api/admin/psd-templates/[id]/parse/route.ts @@ -10,6 +10,7 @@ import { type AgentPsdTemplateMutationResponse, } from "@/lib/agents/psd-template-config"; import { + type ParsedPsdTemplateLayer, parsePsdTemplateFile, preparePsdTemplateRenderAssets, } from "@/lib/agents/psd-template-parser"; @@ -40,22 +41,105 @@ function getErrorMessage(error: unknown) { return "PSD 解析失败"; } +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); +} + +function pickPrimaryImageLayerId(params: { + parsedLayers: Awaited>["layers"]; + canvasWidth: number; + canvasHeight: number; +}) { + const { parsedLayers, canvasWidth, canvasHeight } = params; + const imageLayers = parsedLayers.filter((layer) => layer.slotType === "image"); + if (imageLayers.length === 0) { + return null; + } + + const backgroundPattern = /(背景|底图|bg|background)/i; + 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 coversWholeCanvas = + overlapArea >= canvasWidth * canvasHeight * 0.96 && + layer.x <= 0 && + layer.y <= 0 && + layer.width >= canvasWidth * 0.96 && + layer.height >= canvasHeight * 0.96; + const backgroundPenalty = + backgroundPattern.test(layer.layerName) || backgroundPattern.test(layer.layerPath) || coversWholeCanvas + ? 0.45 + : 1; + + return { + layer, + score: Math.max(overlapArea, layerArea * 0.5) * backgroundPenalty, + }; + }); + + scored.sort((left, right) => right.score - left.score); + return scored[0]?.layer.id || null; +} + +function shouldAutoEditableTextLayer( + layer: ParsedPsdTemplateLayer +) { + const text = String(layer.defaultText || "").replace(/\s+/g, ""); + if (!text) { + return false; + } + + return ( + layer.width >= 80 || + layer.height >= 30 || + text.length >= 4 || + /[A-Za-z0-9]{3,}/.test(text) + ); +} + function buildInitialTemplateLayers(params: { parsedLayers: Awaited>["layers"]; fonts: Awaited>; + canvasWidth: number; + canvasHeight: number; }) { - const { parsedLayers, fonts } = params; + const { parsedLayers, fonts, canvasWidth, canvasHeight } = params; + const primaryImageLayerId = pickPrimaryImageLayerId({ + parsedLayers, + canvasWidth, + canvasHeight, + }); + return parsedLayers.map((layer): AgentPsdTemplateLayerSlot => { if (layer.slotType === "text") { const matchedFont = findBestMatchingPsdFont({ detectedFontNames: layer.detectedFontNames || [], fonts, }); + const isEditable = shouldAutoEditableTextLayer(layer); return { ...layer, slotType: "text", - isEditable: false, + isEditable, isRequired: false, placeholder: layer.defaultText || layer.layerName, detectedFontName: layer.detectedFontName, @@ -75,13 +159,14 @@ function buildInitialTemplateLayers(params: { }; } + const isPrimaryImageLayer = layer.id === primaryImageLayerId; return { ...layer, slotType: "image", - isEditable: false, + isEditable: isPrimaryImageLayer, isRequired: false, - isDefaultResultSource: false, - allowUserUploadOverride: true, + isDefaultResultSource: isPrimaryImageLayer, + allowUserUploadOverride: isPrimaryImageLayer, replaceMode: "cover", }; }); @@ -207,6 +292,8 @@ export async function POST( const initialLayers = buildInitialTemplateLayers({ parsedLayers: parsedTemplate.layers, fonts, + canvasWidth: parsedTemplate.width, + canvasHeight: parsedTemplate.height, }); await updateParseProgress({ @@ -236,6 +323,13 @@ export async function POST( toArrayBuffer(preparedAssets.renderBaseBuffer) ); + const hasEditableLayers = preparedAssets.layers.some((layer) => layer.isEditable); + const nextStatus = + currentTemplate.status === "active" || + (currentTemplate.layers.length === 0 && hasEditableLayers) + ? "active" + : currentTemplate.status; + const updatedTemplate = await updateTemplate(id, session.user.id, (template) => ({ ...template, previewUrl: uploadedPreview.url, @@ -246,6 +340,7 @@ export async function POST( height: parsedTemplate.height, layerCount: parsedTemplate.layers.length, layers: preparedAssets.layers, + status: nextStatus, parseStatus: "completed", parseProgress: 100, parseStage: "解析完成", diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index 1ec7933fd..047fc5028 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -174,8 +174,11 @@ export function AgentPsdTemplateEditorDialog({ return ( - - + + 套用 PSD 模板 @@ -186,7 +189,7 @@ export function AgentPsdTemplateEditorDialog({
-
+
@@ -266,7 +269,7 @@ export function AgentPsdTemplateEditorDialog({
-
+
槽位内容
@@ -430,7 +433,7 @@ export function AgentPsdTemplateEditorDialog({
-
+
套版结果