fix: preserve psd layer stack in editor preview
This commit is contained in:
parent
1a82296133
commit
80db6cd48d
@ -45,7 +45,10 @@ async function repairTemplateLayerPreviewsIfNeeded(template: NonNullable<Awaited
|
|||||||
return template;
|
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 fonts = await readAgentPsdFonts().catch(() => []);
|
||||||
const needsFontDetection = template.layers.some(
|
const needsFontDetection = template.layers.some(
|
||||||
(layer) =>
|
(layer) =>
|
||||||
|
|||||||
@ -70,7 +70,10 @@ async function repairTemplateLayerPreviewsIfNeeded(params: {
|
|||||||
return template;
|
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 fonts = await readAgentPsdFonts().catch(() => []);
|
||||||
const needsFontDetection = template.layers.some(
|
const needsFontDetection = template.layers.some(
|
||||||
(layer) =>
|
(layer) =>
|
||||||
|
|||||||
@ -37,9 +37,10 @@ export async function GET(request: NextRequest) {
|
|||||||
.filter((template) =>
|
.filter((template) =>
|
||||||
requestedBrand ? template.brandName === requestedBrand : true
|
requestedBrand ? template.brandName === requestedBrand : true
|
||||||
)
|
)
|
||||||
|
.filter((template) => getEditableAgentPsdTemplateLayers(template).length > 0)
|
||||||
.map((template) => ({
|
.map((template) => ({
|
||||||
...template,
|
...template,
|
||||||
layers: getEditableAgentPsdTemplateLayers(template).map((layer) => {
|
layers: template.layers.map((layer) => {
|
||||||
if (!isAgentPsdTextLayerSlot(layer) || !layer.fontId) {
|
if (!isAgentPsdTextLayerSlot(layer) || !layer.fontId) {
|
||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
@ -55,8 +56,7 @@ export async function GET(request: NextRequest) {
|
|||||||
fontContentType: font.contentType,
|
fontContentType: font.contentType,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
}))
|
}));
|
||||||
.filter((template) => template.layers.length > 0);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"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 useSWR from "swr";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
@ -95,14 +95,76 @@ function getPreferredSelectedLayerId(template: AgentPsdTemplate | null) {
|
|||||||
return "";
|
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
|
(layer) => isAgentPsdImageLayerSlot(layer) && layer.isDefaultResultSource
|
||||||
);
|
);
|
||||||
if (preferredImage) {
|
if (preferredImage) {
|
||||||
return preferredImage.id;
|
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({
|
export function AgentPsdTemplateEditorDialog({
|
||||||
@ -144,22 +206,36 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
|
|
||||||
const editableImageLayers = useMemo(
|
const editableImageLayers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(selectedTemplate?.layers || []).filter((layer) =>
|
(selectedTemplate?.layers || []).filter(
|
||||||
isAgentPsdImageLayerSlot(layer)
|
(layer): layer is AgentPsdTemplateImageLayerSlot =>
|
||||||
|
layer.isEditable && isAgentPsdImageLayerSlot(layer)
|
||||||
),
|
),
|
||||||
[selectedTemplate]
|
[selectedTemplate]
|
||||||
);
|
);
|
||||||
const editableTextLayers = useMemo(
|
const editableTextLayers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(selectedTemplate?.layers || []).filter((layer) =>
|
(selectedTemplate?.layers || []).filter(
|
||||||
isAgentPsdTextLayerSlot(layer)
|
(layer): layer is AgentPsdTemplateTextLayerSlot =>
|
||||||
|
layer.isEditable && isAgentPsdTextLayerSlot(layer)
|
||||||
),
|
),
|
||||||
[selectedTemplate]
|
[selectedTemplate]
|
||||||
);
|
);
|
||||||
const fixedLayerCount = useMemo(() => {
|
const fixedLayerCount = useMemo(() => {
|
||||||
if (!selectedTemplate) return 0;
|
if (!selectedTemplate) return 0;
|
||||||
return Math.max(0, selectedTemplate.layerCount - selectedTemplate.layers.length);
|
return selectedTemplate.layers.filter((layer) => !layer.isEditable).length;
|
||||||
}, [selectedTemplate]);
|
}, [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 effectiveImageAssignments = useMemo(() => {
|
||||||
const nextAssignments: Record<string, string> = {};
|
const nextAssignments: Record<string, string> = {};
|
||||||
@ -459,129 +535,176 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
transformOrigin: "top left",
|
transformOrigin: "top left",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
{shouldShowCompositeFallback ? (
|
||||||
src={selectedTemplate.renderBaseUrl || selectedTemplate.previewUrl}
|
<img
|
||||||
alt={`${selectedTemplate.name} 编辑底图`}
|
src={selectedTemplate.renderBaseUrl || selectedTemplate.previewUrl}
|
||||||
className="absolute inset-0 h-full w-full object-cover"
|
alt={`${selectedTemplate.name} 编辑底图`}
|
||||||
/>
|
className="absolute inset-0 h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{editableImageLayers.map((layer) => {
|
<div className="pointer-events-none absolute inset-0">
|
||||||
const assignedUrl = effectiveImageAssignments[layer.id];
|
{orderedPreviewLayers.map((layer) => {
|
||||||
const isSelected = selectedLayerId === layer.id;
|
const layerBoxStyle = {
|
||||||
|
left: layer.x,
|
||||||
|
top: layer.y,
|
||||||
|
width: layer.width,
|
||||||
|
height: layer.height,
|
||||||
|
} satisfies CSSProperties;
|
||||||
|
|
||||||
return (
|
if (isAgentPsdImageLayerSlot(layer)) {
|
||||||
<button
|
const assignedUrl = effectiveImageAssignments[layer.id];
|
||||||
key={layer.id}
|
const layerImageUrl = assignedUrl || layer.sourceUrl;
|
||||||
type="button"
|
if (!layerImageUrl) {
|
||||||
onClick={() => setSelectedLayerId(layer.id)}
|
return null;
|
||||||
className="absolute overflow-hidden rounded-lg transition-all"
|
}
|
||||||
style={{
|
|
||||||
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`,
|
|
||||||
background: assignedUrl ? "rgba(255,255,255,0.02)" : "rgba(249,115,22,0.12)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{assignedUrl ? (
|
|
||||||
<img
|
|
||||||
src={assignedUrl}
|
|
||||||
alt={layer.layerName}
|
|
||||||
className="h-full w-full"
|
|
||||||
style={{
|
|
||||||
objectFit:
|
|
||||||
layer.replaceMode === "contain"
|
|
||||||
? "contain"
|
|
||||||
: layer.replaceMode === "stretch"
|
|
||||||
? "fill"
|
|
||||||
: "cover",
|
|
||||||
WebkitMaskImage: layer.maskUrl ? `url(${layer.maskUrl})` : undefined,
|
|
||||||
maskImage: layer.maskUrl ? `url(${layer.maskUrl})` : undefined,
|
|
||||||
WebkitMaskSize: layer.maskUrl ? "100% 100%" : undefined,
|
|
||||||
maskSize: layer.maskUrl ? "100% 100%" : undefined,
|
|
||||||
WebkitMaskRepeat: layer.maskUrl ? "no-repeat" : undefined,
|
|
||||||
maskRepeat: layer.maskUrl ? "no-repeat" : undefined,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center bg-orange-500/10 text-[13px] font-medium text-orange-100">
|
|
||||||
点击替换图片
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="absolute left-2 top-2 rounded-full bg-black/70 px-2 py-1 text-[11px] text-white">
|
return (
|
||||||
{layer.layerName}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{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 (
|
|
||||||
<div
|
|
||||||
key={layer.id}
|
|
||||||
className="absolute overflow-hidden rounded-lg"
|
|
||||||
style={{
|
|
||||||
...sharedStyle,
|
|
||||||
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`,
|
|
||||||
background: isSelected ? "rgba(56,189,248,0.09)" : "rgba(56,189,248,0.03)",
|
|
||||||
}}
|
|
||||||
onClick={() => setSelectedLayerId(layer.id)}
|
|
||||||
>
|
|
||||||
{isSelected ? (
|
|
||||||
<textarea
|
|
||||||
value={value}
|
|
||||||
onChange={(event) =>
|
|
||||||
setTextAssignments((current) => ({
|
|
||||||
...current,
|
|
||||||
[layer.id]: event.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="h-full w-full resize-none border-none bg-transparent outline-none"
|
|
||||||
style={sharedStyle}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none h-full w-full whitespace-pre-wrap break-words"
|
key={layer.id}
|
||||||
style={sharedStyle}
|
className="absolute overflow-hidden"
|
||||||
|
style={layerBoxStyle}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={layerImageUrl}
|
||||||
|
alt={layer.layerName}
|
||||||
|
className="h-full w-full"
|
||||||
|
style={
|
||||||
|
assignedUrl
|
||||||
|
? getAssignedImageStyle(layer)
|
||||||
|
: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "fill",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelected = selectedLayerId === layer.id;
|
||||||
|
if (isSelected) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = effectiveTextAssignments[layer.id] || "";
|
||||||
|
const defaultText = String(layer.defaultText || "");
|
||||||
|
const shouldUseEditedPreview =
|
||||||
|
value.trim().length > 0 && value.trim() !== defaultText.trim();
|
||||||
|
|
||||||
|
if (!shouldUseEditedPreview && layer.sourceUrl) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={layer.id}
|
||||||
|
className="absolute overflow-hidden"
|
||||||
|
style={layerBoxStyle}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={layer.sourceUrl}
|
||||||
|
alt={layer.layerName}
|
||||||
|
className="h-full w-full object-fill"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value && !layer.placeholder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={layer.id}
|
||||||
|
className="absolute overflow-hidden"
|
||||||
|
style={layerBoxStyle}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-full w-full whitespace-pre-wrap break-words"
|
||||||
|
style={getTextLayerContentStyle(layer)}
|
||||||
>
|
>
|
||||||
{value || layer.placeholder || layer.layerName}
|
{value || layer.placeholder || layer.layerName}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="absolute left-2 top-2 rounded-full bg-black/70 px-2 py-1 text-[11px] text-white">
|
|
||||||
{layer.layerName}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
{orderedEditableLayers.map((layer) => {
|
||||||
|
const isSelected = selectedLayerId === layer.id;
|
||||||
|
const frameStyle = getLayerFrameStyle(layer, isSelected);
|
||||||
|
|
||||||
|
if (isAgentPsdTextLayerSlot(layer) && isSelected) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={layer.id}
|
||||||
|
className="absolute overflow-hidden rounded-lg"
|
||||||
|
style={{
|
||||||
|
...frameStyle,
|
||||||
|
zIndex: 40,
|
||||||
|
background: "rgba(56,189,248,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
value={effectiveTextAssignments[layer.id] || ""}
|
||||||
|
onChange={(event) =>
|
||||||
|
setTextAssignments((current) => ({
|
||||||
|
...current,
|
||||||
|
[layer.id]: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="h-full w-full resize-none border-none bg-transparent outline-none"
|
||||||
|
style={getTextLayerContentStyle(layer)}
|
||||||
|
/>
|
||||||
|
<div className="absolute left-2 top-2 rounded-full bg-black/70 px-2 py-1 text-[11px] text-white">
|
||||||
|
{layer.layerName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAssignedImage =
|
||||||
|
isAgentPsdImageLayerSlot(layer) &&
|
||||||
|
Boolean(String(effectiveImageAssignments[layer.id] || "").trim());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={layer.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedLayerId(layer.id)}
|
||||||
|
className="absolute overflow-hidden rounded-lg transition-all"
|
||||||
|
style={{
|
||||||
|
...frameStyle,
|
||||||
|
zIndex: isSelected ? 35 : 30,
|
||||||
|
background: isSelected
|
||||||
|
? isAgentPsdImageLayerSlot(layer)
|
||||||
|
? "rgba(249,115,22,0.08)"
|
||||||
|
: "rgba(56,189,248,0.05)"
|
||||||
|
: "transparent",
|
||||||
|
}}
|
||||||
|
aria-label={`${getLayerLabel(layer)} ${layer.layerName}`}
|
||||||
|
title={`${getLayerLabel(layer)}:${layer.layerName}`}
|
||||||
|
>
|
||||||
|
<div className="absolute left-2 top-2 rounded-full bg-black/70 px-2 py-1 text-[11px] text-white">
|
||||||
|
{layer.layerName}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAgentPsdImageLayerSlot(layer) ? (
|
||||||
|
!hasAssignedImage ? (
|
||||||
|
<div className="pointer-events-none absolute bottom-2 right-2 rounded-full bg-orange-500/85 px-2 py-1 text-[11px] text-white">
|
||||||
|
点击替换图片
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
) : (
|
||||||
|
<div className="pointer-events-none absolute bottom-2 right-2 rounded-full bg-sky-500/85 px-2 py-1 text-[11px] text-white">
|
||||||
|
点击编辑文字
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -24,6 +24,8 @@ export interface AgentPsdTemplateBaseLayerSlot {
|
|||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
previewUrl?: string;
|
previewUrl?: string;
|
||||||
previewPath?: string;
|
previewPath?: string;
|
||||||
|
sourceUrl?: string;
|
||||||
|
sourcePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentPsdTemplateImageLayerSlot
|
export interface AgentPsdTemplateImageLayerSlot
|
||||||
@ -244,6 +246,8 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
|
|||||||
isRequired: isEditable && record.isRequired === true,
|
isRequired: isEditable && record.isRequired === true,
|
||||||
previewUrl: normalizeOptionalUrl(record.previewUrl),
|
previewUrl: normalizeOptionalUrl(record.previewUrl),
|
||||||
previewPath: normalizeText(record.previewPath) || undefined,
|
previewPath: normalizeText(record.previewPath) || undefined,
|
||||||
|
sourceUrl: normalizeOptionalUrl(record.sourceUrl),
|
||||||
|
sourcePath: normalizeText(record.sourcePath) || undefined,
|
||||||
} satisfies AgentPsdTemplateBaseLayerSlot;
|
} satisfies AgentPsdTemplateBaseLayerSlot;
|
||||||
|
|
||||||
if (slotType === "text") {
|
if (slotType === "text") {
|
||||||
|
|||||||
@ -508,6 +508,25 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!String(targetLayer.sourceUrl || "").trim()) {
|
||||||
|
const sourceBuffer = await rgbaToPngBuffer(rgba, entry.layer.width, entry.layer.height);
|
||||||
|
const uploadedSource = await uploadFile(
|
||||||
|
`agent-psd-layer-source-${params.templateId}-${sanitizeFileSegment(targetLayer.layerId, targetLayer.id)}.png`,
|
||||||
|
sourceBuffer.buffer.slice(
|
||||||
|
sourceBuffer.byteOffset,
|
||||||
|
sourceBuffer.byteOffset + sourceBuffer.byteLength
|
||||||
|
) as ArrayBuffer
|
||||||
|
);
|
||||||
|
const layerIndex = nextLayers.findIndex((layer) => layer.id === targetLayer.id);
|
||||||
|
if (layerIndex >= 0) {
|
||||||
|
nextLayers[layerIndex] = {
|
||||||
|
...nextLayers[layerIndex],
|
||||||
|
sourceUrl: uploadedSource.url,
|
||||||
|
sourcePath: uploadedSource.pathname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAgentPsdImageLayerSlot(targetLayer) || !targetLayer.isEditable || targetLayer.maskUrl) {
|
if (!isAgentPsdImageLayerSlot(targetLayer) || !targetLayer.isEditable || targetLayer.maskUrl) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user