fix: surface parsed PSD templates in agent dialog
This commit is contained in:
parent
e23912d48f
commit
e562e48c13
@ -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<ReturnType<typeof parsePsdTemplateFile>>["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<ReturnType<typeof parsePsdTemplateFile>>["layers"];
|
||||
fonts: Awaited<ReturnType<typeof readAgentPsdFonts>>;
|
||||
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: "解析完成",
|
||||
|
||||
@ -174,8 +174,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden p-0">
|
||||
<DialogHeader className="border-b px-6 py-5">
|
||||
<DialogContent
|
||||
overlayClassName="bg-black/85 backdrop-blur-sm"
|
||||
className="max-h-[90vh] max-w-6xl overflow-hidden border border-white/10 bg-[#09090b] p-0 text-white shadow-[0_30px_120px_rgba(0,0,0,0.65)]"
|
||||
>
|
||||
<DialogHeader className="border-b border-white/10 bg-[#09090b] px-6 py-5">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-orange-500" />
|
||||
套用 PSD 模板
|
||||
@ -186,7 +189,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid min-h-0 gap-0 lg:grid-cols-[360px,minmax(0,1fr),minmax(0,1fr)]">
|
||||
<div className="border-r p-5">
|
||||
<div className="border-r border-white/10 bg-[#09090b] p-5">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>选择模板</Label>
|
||||
@ -266,7 +269,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-r p-5">
|
||||
<div className="border-r border-white/10 bg-[#09090b] p-5">
|
||||
<div className="mb-4">
|
||||
<div className="text-sm font-medium">槽位内容</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
@ -430,7 +433,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="p-5">
|
||||
<div className="bg-[#09090b] p-5">
|
||||
<div className="mb-4">
|
||||
<div className="text-sm font-medium">套版结果</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user