diff --git a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx
index ba95ed259..d56af75c6 100644
--- a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx
+++ b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx
@@ -96,6 +96,7 @@ const SYSTEM_FONT_OPTIONS = [
const EMPTY_CATEGORY_SELECT_VALUE = "__none__";
const EMPTY_BRAND_SELECT_VALUE = "__unassigned__";
const ALL_TEMPLATE_BRANDS_SELECT_VALUE = "__all_template_brands__";
+const SELF_CLIP_MASK_SELECT_VALUE = "__self_clip_mask__";
const UNASSIGNED_BRAND_LABEL = "未分类";
const MAX_PSD_TEMPLATE_FILE_SIZE = 80 * 1024 * 1024;
const PSD_TEMPLATE_MODE_OPTIONS: Array<{
@@ -1679,7 +1680,22 @@ export function PsdTemplatesSettingsClient({
: "当前筛选条件下没有图层,请换个关键词试试。"}
) : (
- filteredLayers.map((layer, index) => (
+ filteredLayers.map((layer, index) => {
+ const clipMaskLayerOptions =
+ layer.slotType === "image"
+ ? draftTemplate.layers.filter(
+ (candidate) =>
+ candidate.id !== layer.id &&
+ (candidate.previewUrl || candidate.sourceUrl) &&
+ !(
+ candidate.slotType === "text" &&
+ !candidate.previewUrl &&
+ !candidate.sourceUrl
+ )
+ )
+ : [];
+
+ return (
@@ -1836,6 +1852,9 @@ export function PsdTemplatesSettingsClient({
forceClipMask: checked
? false
: current.forceClipMask,
+ clipMaskLayerId: checked
+ ? undefined
+ : current.clipMaskLayerId,
};
}
@@ -1875,6 +1894,9 @@ export function PsdTemplatesSettingsClient({
forceClipMask: checked
? current.forceClipMask
: false,
+ clipMaskLayerId: checked
+ ? current.clipMaskLayerId
+ : undefined,
}
: {
...current,
@@ -1994,7 +2016,7 @@ export function PsdTemplatesSettingsClient({
强制剪切蒙版
- 开启后替换图会按当前 PSD 图层原始透明区域剪切,适合需要贴合固定形状的图片槽位。
+ 开启后替换图会按指定目标图层的透明区域剪切,适合替换图需要贴合超级锁或固定形状的图片槽位。
+ {layer.forceClipMask ? (
+
+
剪切蒙版目标图层
+
+
+ 如果 PSD 里目标形状是超级锁图层,请在这里选择那个目标图层;前台只能移动替换图,目标图仍保持锁定不可选中。
+
+
+ ) : null}
+
- ))
+ );
+ })
)}
diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx
index 31eb87906..bb9e4dd28 100644
--- a/components/agents/AgentPsdTemplateEditorDialog.tsx
+++ b/components/agents/AgentPsdTemplateEditorDialog.tsx
@@ -1488,12 +1488,18 @@ function renderTextLayerPreviewContent(
}
function getAssignedImageMaskStyle(
- layer: AgentPsdTemplateImageLayerSlot
+ layer: AgentPsdTemplateImageLayerSlot,
+ layers: AgentPsdTemplateLayerSlot[]
): CSSProperties {
+ const clipMaskLayer = layer.clipMaskLayerId
+ ? layers.find((item) => item.id === layer.clipMaskLayerId)
+ : null;
const fallbackMaskUrl = layer.forceClipMask
- ? getPsdLayerPreviewSourceUrl(layer)
+ ? getPsdLayerPreviewSourceUrl(clipMaskLayer || layer)
: "";
- const normalizedMaskUrl = String(layer.maskUrl || fallbackMaskUrl || "").trim();
+ const normalizedMaskUrl = String(
+ layer.maskUrl || fallbackMaskUrl || ""
+ ).trim();
const hasMask = Boolean(normalizedMaskUrl);
const maskImageUrl = hasMask
? `url(${buildPsdLayerMaskUrl(normalizedMaskUrl, layer.width)})`
@@ -4162,6 +4168,11 @@ export function AgentPsdTemplateEditorDialog({
if (isAgentPsdImageLayerSlot(layer)) {
const assignedUrl =
effectiveImageAssignments[layer.id];
+ const isDefaultAssignedImageLayer =
+ layer.id ===
+ getDefaultSourceImageLayer(
+ selectedTemplateLayers
+ )?.id;
const layerImageUrl =
assignedUrl ||
getPsdLayerPreviewSourceUrl(layer);
@@ -4171,11 +4182,21 @@ export function AgentPsdTemplateEditorDialog({
const assignedImageContainerStyle = assignedUrl
? {
...layerBoxStyle,
- ...getAssignedImageMaskStyle(layer),
+ ...getAssignedImageMaskStyle(
+ layer,
+ selectedTemplateLayers
+ ),
}
: layerBoxStyle;
const previewZIndex = assignedUrl
- ? Math.max(baseZIndex, renderBaseZIndex + 1)
+ ? Math.max(
+ baseZIndex,
+ renderBaseZIndex + 1,
+ isDefaultAssignedImageLayer
+ ? (orderedPreviewLayers.length + 1) *
+ PSD_LAYER_Z_INDEX_STEP
+ : 0
+ )
: baseZIndex;
return (
diff --git a/lib/agents/psd-template-config.ts b/lib/agents/psd-template-config.ts
index 0825e5caa..6b7ddec65 100644
--- a/lib/agents/psd-template-config.ts
+++ b/lib/agents/psd-template-config.ts
@@ -50,6 +50,7 @@ export interface AgentPsdTemplateImageLayerSlot
isDefaultResultSource: boolean;
allowUserUploadOverride: boolean;
forceClipMask?: boolean;
+ clipMaskLayerId?: string;
replaceMode: AgentPsdTemplateReplaceMode;
contentBounds?: AgentPsdTemplateImageContentBounds;
maskUrl?: string;
@@ -417,6 +418,10 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
allowUserUploadOverride:
isEditable && !isSuperLocked ? record.allowUserUploadOverride !== false : false,
forceClipMask: isEditable && !isSuperLocked && record.forceClipMask === true,
+ clipMaskLayerId:
+ isEditable && !isSuperLocked && record.forceClipMask === true
+ ? normalizeText(record.clipMaskLayerId) || undefined
+ : undefined,
replaceMode: normalizeAgentPsdTemplateReplaceMode(record.replaceMode),
contentBounds: normalizeAgentPsdTemplateImageContentBounds(
record.contentBounds,
diff --git a/lib/agents/psd-template-renderer.ts b/lib/agents/psd-template-renderer.ts
index 324e353af..da9df60ed 100644
--- a/lib/agents/psd-template-renderer.ts
+++ b/lib/agents/psd-template-renderer.ts
@@ -596,6 +596,13 @@ export async function renderAgentPsdTemplate(params: {
if (assignmentUrl) {
pushAsset(assignmentUrl, "PSD 套版素材");
pushAsset(layer.maskUrl, "PSD 蒙版资源", true);
+ if (layer.forceClipMask && layer.clipMaskLayerId) {
+ pushAsset(
+ getStoredLayerSourceUrl(layerMap.get(layer.clipMaskLayerId) || layer),
+ "PSD 剪切蒙版目标资源",
+ true
+ );
+ }
} else {
pushAsset(
storedLayerSourceUrl,
@@ -995,6 +1002,9 @@ export async function renderAgentPsdTemplate(params: {
const layerPositionOffset = getLayerPositionOffset(layer.id);
if (!layer.maskUrl && !layer.forceClipMask) {
+ if (isDefaultSourceImageLayer) {
+ await appendRenderBaseOverlay();
+ }
const didAppend = await appendBufferOverlay({
input: slotOverlay.input,
left:
@@ -1008,9 +1018,6 @@ export async function renderAgentPsdTemplate(params: {
width: slotOverlay.width,
height: slotOverlay.height,
});
- if (isDefaultSourceImageLayer) {
- await appendRenderBaseOverlay();
- }
if (!didAppend) continue;
continue;
}
@@ -1035,9 +1042,12 @@ export async function renderAgentPsdTemplate(params: {
const shouldUseLayerSourceAsMask = !layer.maskUrl && layer.forceClipMask;
if (layer.maskUrl || shouldUseLayerSourceAsMask) {
try {
+ const clipMaskLayer = layer.clipMaskLayerId
+ ? layerMap.get(layer.clipMaskLayerId)
+ : null;
const rawMaskBuffer = layer.maskUrl
? await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
- : await readReusableLayerBuffer(layer);
+ : await readReusableLayerBuffer(clipMaskLayer || layer);
if (!rawMaskBuffer) {
throw new Error("Missing PSD layer mask source");
}
@@ -1076,6 +1086,10 @@ export async function renderAgentPsdTemplate(params: {
}
}
+ if (isDefaultSourceImageLayer) {
+ await appendRenderBaseOverlay();
+ }
+
const didAppend = await appendBufferOverlay({
input: overlayBuffer,
left: layer.x + layerPositionOffset.offsetX,
@@ -1083,9 +1097,6 @@ export async function renderAgentPsdTemplate(params: {
width: layer.width,
height: layer.height,
});
- if (isDefaultSourceImageLayer) {
- await appendRenderBaseOverlay();
- }
if (!didAppend) continue;
continue;
}