From 80db6cd48d4da0491a0380f6d4c1f5d998b2e7cc Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 28 Mar 2026 20:53:00 +0800 Subject: [PATCH] fix: preserve psd layer stack in editor preview --- app/api/admin/psd-templates/[id]/route.ts | 5 +- app/api/admin/psd-templates/route.ts | 5 +- app/api/psd-templates/route.ts | 6 +- .../agents/AgentPsdTemplateEditorDialog.tsx | 369 ++++++++++++------ lib/agents/psd-template-config.ts | 4 + lib/agents/psd-template-parser.ts | 19 + 6 files changed, 280 insertions(+), 128 deletions(-) diff --git a/app/api/admin/psd-templates/[id]/route.ts b/app/api/admin/psd-templates/[id]/route.ts index 8e65d1299..922cab51e 100644 --- a/app/api/admin/psd-templates/[id]/route.ts +++ b/app/api/admin/psd-templates/[id]/route.ts @@ -45,7 +45,10 @@ async function repairTemplateLayerPreviewsIfNeeded(template: NonNullable !String(layer.previewUrl || "").trim()); + const needsRepair = template.layers.some( + (layer) => + !String(layer.previewUrl || "").trim() || !String(layer.sourceUrl || "").trim() + ); const fonts = await readAgentPsdFonts().catch(() => []); const needsFontDetection = template.layers.some( (layer) => diff --git a/app/api/admin/psd-templates/route.ts b/app/api/admin/psd-templates/route.ts index 98eea7ddb..e4684f3a4 100644 --- a/app/api/admin/psd-templates/route.ts +++ b/app/api/admin/psd-templates/route.ts @@ -70,7 +70,10 @@ async function repairTemplateLayerPreviewsIfNeeded(params: { return template; } - const needsRepair = template.layers.some((layer) => !String(layer.previewUrl || "").trim()); + const needsRepair = template.layers.some( + (layer) => + !String(layer.previewUrl || "").trim() || !String(layer.sourceUrl || "").trim() + ); const fonts = await readAgentPsdFonts().catch(() => []); const needsFontDetection = template.layers.some( (layer) => diff --git a/app/api/psd-templates/route.ts b/app/api/psd-templates/route.ts index 8ca5a2907..9e0b5c41a 100644 --- a/app/api/psd-templates/route.ts +++ b/app/api/psd-templates/route.ts @@ -37,9 +37,10 @@ export async function GET(request: NextRequest) { .filter((template) => requestedBrand ? template.brandName === requestedBrand : true ) + .filter((template) => getEditableAgentPsdTemplateLayers(template).length > 0) .map((template) => ({ ...template, - layers: getEditableAgentPsdTemplateLayers(template).map((layer) => { + layers: template.layers.map((layer) => { if (!isAgentPsdTextLayerSlot(layer) || !layer.fontId) { return layer; } @@ -55,8 +56,7 @@ export async function GET(request: NextRequest) { fontContentType: font.contentType, }; }), - })) - .filter((template) => template.layers.length > 0); + })); return NextResponse.json({ success: true, diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index b85ce6f96..c16864f58 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; +import { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { @@ -95,14 +95,76 @@ function getPreferredSelectedLayerId(template: AgentPsdTemplate | null) { return ""; } - const preferredImage = template.layers.find( + const editableLayers = template.layers.filter((layer) => layer.isEditable); + if (editableLayers.length === 0) { + return ""; + } + + const preferredImage = editableLayers.find( (layer) => isAgentPsdImageLayerSlot(layer) && layer.isDefaultResultSource ); if (preferredImage) { return preferredImage.id; } - return template.layers[0]?.id || ""; + return editableLayers[0]?.id || ""; +} + +function getImageFit(replaceMode: AgentPsdTemplateImageLayerSlot["replaceMode"]): CSSProperties["objectFit"] { + if (replaceMode === "contain") return "contain"; + if (replaceMode === "stretch") return "fill"; + return "cover"; +} + +function getLayerFrameStyle( + layer: AgentPsdTemplateLayerSlot, + isSelected: boolean +): CSSProperties { + return { + left: layer.x, + top: layer.y, + width: layer.width, + height: layer.height, + border: `2px solid ${getLayerOutlineColor(layer)}`, + boxShadow: isSelected + ? `0 0 0 3px rgba(255,255,255,0.16), 0 0 0 6px ${getLayerOutlineColor(layer)}55` + : `0 0 0 1px ${getLayerOutlineColor(layer)}66 inset`, + }; +} + +function getTextLayerContentStyle(layer: AgentPsdTemplateTextLayerSlot): CSSProperties { + return { + width: "100%", + height: "100%", + fontFamily: layer.fontFamily || "sans-serif", + fontSize: layer.fontSize, + lineHeight: layer.lineHeight, + letterSpacing: `${layer.letterSpacing}px`, + color: layer.textColor, + fontWeight: layer.fontWeight, + textAlign: layer.textAlign, + paddingLeft: layer.paddingX, + paddingRight: layer.paddingX, + paddingTop: layer.paddingY, + paddingBottom: layer.paddingY, + }; +} + +function getAssignedImageStyle(layer: AgentPsdTemplateImageLayerSlot): CSSProperties { + const hasMask = Boolean(layer.maskUrl); + return { + width: "100%", + height: "100%", + objectFit: getImageFit(layer.replaceMode), + WebkitMaskImage: hasMask ? `url(${layer.maskUrl})` : undefined, + maskImage: hasMask ? `url(${layer.maskUrl})` : undefined, + WebkitMaskSize: hasMask ? "100% 100%" : undefined, + maskSize: hasMask ? "100% 100%" : undefined, + WebkitMaskRepeat: hasMask ? "no-repeat" : undefined, + maskRepeat: hasMask ? "no-repeat" : undefined, + WebkitMaskPosition: hasMask ? "center" : undefined, + maskPosition: hasMask ? "center" : undefined, + }; } export function AgentPsdTemplateEditorDialog({ @@ -144,22 +206,36 @@ export function AgentPsdTemplateEditorDialog({ const editableImageLayers = useMemo( () => - (selectedTemplate?.layers || []).filter((layer) => - isAgentPsdImageLayerSlot(layer) + (selectedTemplate?.layers || []).filter( + (layer): layer is AgentPsdTemplateImageLayerSlot => + layer.isEditable && isAgentPsdImageLayerSlot(layer) ), [selectedTemplate] ); const editableTextLayers = useMemo( () => - (selectedTemplate?.layers || []).filter((layer) => - isAgentPsdTextLayerSlot(layer) + (selectedTemplate?.layers || []).filter( + (layer): layer is AgentPsdTemplateTextLayerSlot => + layer.isEditable && isAgentPsdTextLayerSlot(layer) ), [selectedTemplate] ); const fixedLayerCount = useMemo(() => { if (!selectedTemplate) return 0; - return Math.max(0, selectedTemplate.layerCount - selectedTemplate.layers.length); + return selectedTemplate.layers.filter((layer) => !layer.isEditable).length; }, [selectedTemplate]); + const orderedPreviewLayers = useMemo( + () => (selectedTemplate ? [...selectedTemplate.layers].reverse() : []), + [selectedTemplate] + ); + const orderedEditableLayers = useMemo( + () => orderedPreviewLayers.filter((layer) => layer.isEditable), + [orderedPreviewLayers] + ); + const shouldShowCompositeFallback = useMemo( + () => orderedPreviewLayers.some((layer) => !String(layer.sourceUrl || "").trim()), + [orderedPreviewLayers] + ); const effectiveImageAssignments = useMemo(() => { const nextAssignments: Record = {}; @@ -459,129 +535,176 @@ export function AgentPsdTemplateEditorDialog({ transformOrigin: "top left", }} > - {`${selectedTemplate.name} + {shouldShowCompositeFallback ? ( + {`${selectedTemplate.name} + ) : null} - {editableImageLayers.map((layer) => { - const assignedUrl = effectiveImageAssignments[layer.id]; - const isSelected = selectedLayerId === layer.id; +
+ {orderedPreviewLayers.map((layer) => { + const layerBoxStyle = { + left: layer.x, + top: layer.y, + width: layer.width, + height: layer.height, + } satisfies CSSProperties; - return ( - - ); - })} - - {editableTextLayers.map((layer) => { - const isSelected = selectedLayerId === layer.id; - const value = effectiveTextAssignments[layer.id] || ""; - const sharedStyle = { - left: layer.x, - top: layer.y, - width: layer.width, - height: layer.height, - fontFamily: layer.fontFamily || "sans-serif", - fontSize: layer.fontSize, - lineHeight: layer.lineHeight, - letterSpacing: `${layer.letterSpacing}px`, - color: layer.textColor, - fontWeight: layer.fontWeight, - textAlign: layer.textAlign, - paddingLeft: layer.paddingX, - paddingRight: layer.paddingX, - paddingTop: layer.paddingY, - paddingBottom: layer.paddingY, - } as const; - - return ( -
setSelectedLayerId(layer.id)} - > - {isSelected ? ( -