feat: add PSD force clipping mask option

This commit is contained in:
Your Name 2026-06-01 18:43:42 +08:00
parent 4d2093ef44
commit db1fc8c3df
4 changed files with 67 additions and 15 deletions

View File

@ -1798,6 +1798,9 @@ export function PsdTemplatesSettingsClient({
allowUserUploadOverride: checked
? false
: current.allowUserUploadOverride,
forceClipMask: checked
? false
: current.forceClipMask,
};
}
@ -1834,6 +1837,9 @@ export function PsdTemplatesSettingsClient({
allowUserUploadOverride: checked
? current.allowUserUploadOverride
: false,
forceClipMask: checked
? current.forceClipMask
: false,
}
: {
...current,
@ -1949,6 +1955,31 @@ export function PsdTemplatesSettingsClient({
/>
</div>
<div className="flex items-center justify-between rounded-xl border p-3">
<div className="space-y-0.5">
<div className="text-sm font-medium">强制剪切蒙版</div>
<div className="text-xs text-muted-foreground">
开启后替换图会按当前 PSD 图层原始透明区域剪切,适合需要贴合固定形状的图片槽位。
</div>
</div>
<Switch
checked={layer.isEditable && layer.forceClipMask === true}
disabled={!layer.isEditable || layer.isSuperLocked === true}
onCheckedChange={(checked) =>
updateDraftLayer(layer.id, (current) =>
current.slotType === "image"
? {
...current,
forceClipMask: current.isEditable
? checked
: false,
}
: current
)
}
/>
</div>
<div className="space-y-2 rounded-xl border p-3">
<div className="text-sm font-medium">填充模式</div>
<Select

View File

@ -1476,16 +1476,27 @@ function renderTextLayerPreviewContent(
function getAssignedImageMaskStyle(
layer: AgentPsdTemplateImageLayerSlot
): CSSProperties {
const hasMask = Boolean(layer.maskUrl);
const fallbackMaskUrl = layer.forceClipMask
? getPsdLayerPreviewSourceUrl(layer)
: "";
const normalizedMaskUrl = String(layer.maskUrl || fallbackMaskUrl || "").trim();
const hasMask = Boolean(normalizedMaskUrl);
const maskImageUrl = hasMask
? `url(${buildPsdLayerPreviewUrl(normalizedMaskUrl, {
width: layer.width,
quality: 72,
})})`
: undefined;
return {
WebkitMaskImage: hasMask ? `url(${layer.maskUrl})` : undefined,
maskImage: hasMask ? `url(${layer.maskUrl})` : undefined,
WebkitMaskImage: maskImageUrl,
maskImage: maskImageUrl,
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,
maskMode: hasMask ? "alpha" : undefined,
};
}
@ -4152,6 +4163,9 @@ export function AgentPsdTemplateEditorDialog({
...getAssignedImageMaskStyle(layer),
}
: layerBoxStyle;
const previewZIndex = assignedUrl
? Math.max(baseZIndex, renderBaseZIndex + 1)
: baseZIndex;
return (
<div
@ -4176,7 +4190,7 @@ export function AgentPsdTemplateEditorDialog({
}
style={{
...assignedImageContainerStyle,
zIndex: baseZIndex,
zIndex: previewZIndex,
cursor:
isLayerSuperLocked
? "default"

View File

@ -49,6 +49,7 @@ export interface AgentPsdTemplateImageLayerSlot
slotType: "image";
isDefaultResultSource: boolean;
allowUserUploadOverride: boolean;
forceClipMask?: boolean;
replaceMode: AgentPsdTemplateReplaceMode;
contentBounds?: AgentPsdTemplateImageContentBounds;
maskUrl?: string;
@ -415,6 +416,7 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
isEditable && !isSuperLocked && record.isDefaultResultSource === true,
allowUserUploadOverride:
isEditable && !isSuperLocked ? record.allowUserUploadOverride !== false : false,
forceClipMask: isEditable && !isSuperLocked && record.forceClipMask === true,
replaceMode: normalizeAgentPsdTemplateReplaceMode(record.replaceMode),
contentBounds: normalizeAgentPsdTemplateImageContentBounds(
record.contentBounds,

View File

@ -576,13 +576,13 @@ export async function renderAgentPsdTemplate(params: {
for (const layer of orderedTemplateLayers) {
if (hiddenLayerIdSet.has(layer.id)) {
continue;
}
const storedLayerSourceUrl = getStoredLayerSourceUrl(layer);
if (shouldRenderAsStaticLayer(layer)) {
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
continue;
}
}
const storedLayerSourceUrl = getStoredLayerSourceUrl(layer);
if (shouldRenderAsStaticLayer(layer)) {
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
continue;
}
pushAsset(
storedLayerSourceUrl,
"PSD 图层素材",
@ -1032,11 +1032,16 @@ export async function renderAgentPsdTemplate(params: {
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
if (layer.maskUrl) {
const shouldUseLayerSourceAsMask = !layer.maskUrl && layer.forceClipMask;
if (layer.maskUrl || shouldUseLayerSourceAsMask) {
try {
const maskBuffer = await sharp(
await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
)
const rawMaskBuffer = layer.maskUrl
? await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
: await readReusableLayerBuffer(layer);
if (!rawMaskBuffer) {
throw new Error("Missing PSD layer mask source");
}
const maskBuffer = await sharp(rawMaskBuffer)
.resize({
width: Math.max(1, Math.round(layer.width)),
height: Math.max(1, Math.round(layer.height)),