feat: add PSD force clipping mask option
This commit is contained in:
parent
4d2093ef44
commit
db1fc8c3df
@ -1798,6 +1798,9 @@ export function PsdTemplatesSettingsClient({
|
|||||||
allowUserUploadOverride: checked
|
allowUserUploadOverride: checked
|
||||||
? false
|
? false
|
||||||
: current.allowUserUploadOverride,
|
: current.allowUserUploadOverride,
|
||||||
|
forceClipMask: checked
|
||||||
|
? false
|
||||||
|
: current.forceClipMask,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1834,6 +1837,9 @@ export function PsdTemplatesSettingsClient({
|
|||||||
allowUserUploadOverride: checked
|
allowUserUploadOverride: checked
|
||||||
? current.allowUserUploadOverride
|
? current.allowUserUploadOverride
|
||||||
: false,
|
: false,
|
||||||
|
forceClipMask: checked
|
||||||
|
? current.forceClipMask
|
||||||
|
: false,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
...current,
|
...current,
|
||||||
@ -1949,6 +1955,31 @@ export function PsdTemplatesSettingsClient({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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="space-y-2 rounded-xl border p-3">
|
||||||
<div className="text-sm font-medium">填充模式</div>
|
<div className="text-sm font-medium">填充模式</div>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@ -1476,16 +1476,27 @@ function renderTextLayerPreviewContent(
|
|||||||
function getAssignedImageMaskStyle(
|
function getAssignedImageMaskStyle(
|
||||||
layer: AgentPsdTemplateImageLayerSlot
|
layer: AgentPsdTemplateImageLayerSlot
|
||||||
): CSSProperties {
|
): 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 {
|
return {
|
||||||
WebkitMaskImage: hasMask ? `url(${layer.maskUrl})` : undefined,
|
WebkitMaskImage: maskImageUrl,
|
||||||
maskImage: hasMask ? `url(${layer.maskUrl})` : undefined,
|
maskImage: maskImageUrl,
|
||||||
WebkitMaskSize: hasMask ? "100% 100%" : undefined,
|
WebkitMaskSize: hasMask ? "100% 100%" : undefined,
|
||||||
maskSize: hasMask ? "100% 100%" : undefined,
|
maskSize: hasMask ? "100% 100%" : undefined,
|
||||||
WebkitMaskRepeat: hasMask ? "no-repeat" : undefined,
|
WebkitMaskRepeat: hasMask ? "no-repeat" : undefined,
|
||||||
maskRepeat: hasMask ? "no-repeat" : undefined,
|
maskRepeat: hasMask ? "no-repeat" : undefined,
|
||||||
WebkitMaskPosition: hasMask ? "center" : undefined,
|
WebkitMaskPosition: hasMask ? "center" : undefined,
|
||||||
maskPosition: hasMask ? "center" : undefined,
|
maskPosition: hasMask ? "center" : undefined,
|
||||||
|
maskMode: hasMask ? "alpha" : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4152,6 +4163,9 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
...getAssignedImageMaskStyle(layer),
|
...getAssignedImageMaskStyle(layer),
|
||||||
}
|
}
|
||||||
: layerBoxStyle;
|
: layerBoxStyle;
|
||||||
|
const previewZIndex = assignedUrl
|
||||||
|
? Math.max(baseZIndex, renderBaseZIndex + 1)
|
||||||
|
: baseZIndex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -4176,7 +4190,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
...assignedImageContainerStyle,
|
...assignedImageContainerStyle,
|
||||||
zIndex: baseZIndex,
|
zIndex: previewZIndex,
|
||||||
cursor:
|
cursor:
|
||||||
isLayerSuperLocked
|
isLayerSuperLocked
|
||||||
? "default"
|
? "default"
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export interface AgentPsdTemplateImageLayerSlot
|
|||||||
slotType: "image";
|
slotType: "image";
|
||||||
isDefaultResultSource: boolean;
|
isDefaultResultSource: boolean;
|
||||||
allowUserUploadOverride: boolean;
|
allowUserUploadOverride: boolean;
|
||||||
|
forceClipMask?: boolean;
|
||||||
replaceMode: AgentPsdTemplateReplaceMode;
|
replaceMode: AgentPsdTemplateReplaceMode;
|
||||||
contentBounds?: AgentPsdTemplateImageContentBounds;
|
contentBounds?: AgentPsdTemplateImageContentBounds;
|
||||||
maskUrl?: string;
|
maskUrl?: string;
|
||||||
@ -415,6 +416,7 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
|
|||||||
isEditable && !isSuperLocked && record.isDefaultResultSource === true,
|
isEditable && !isSuperLocked && record.isDefaultResultSource === true,
|
||||||
allowUserUploadOverride:
|
allowUserUploadOverride:
|
||||||
isEditable && !isSuperLocked ? record.allowUserUploadOverride !== false : false,
|
isEditable && !isSuperLocked ? record.allowUserUploadOverride !== false : false,
|
||||||
|
forceClipMask: isEditable && !isSuperLocked && record.forceClipMask === true,
|
||||||
replaceMode: normalizeAgentPsdTemplateReplaceMode(record.replaceMode),
|
replaceMode: normalizeAgentPsdTemplateReplaceMode(record.replaceMode),
|
||||||
contentBounds: normalizeAgentPsdTemplateImageContentBounds(
|
contentBounds: normalizeAgentPsdTemplateImageContentBounds(
|
||||||
record.contentBounds,
|
record.contentBounds,
|
||||||
|
|||||||
@ -576,13 +576,13 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
for (const layer of orderedTemplateLayers) {
|
for (const layer of orderedTemplateLayers) {
|
||||||
if (hiddenLayerIdSet.has(layer.id)) {
|
if (hiddenLayerIdSet.has(layer.id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedLayerSourceUrl = getStoredLayerSourceUrl(layer);
|
const storedLayerSourceUrl = getStoredLayerSourceUrl(layer);
|
||||||
if (shouldRenderAsStaticLayer(layer)) {
|
if (shouldRenderAsStaticLayer(layer)) {
|
||||||
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
|
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
pushAsset(
|
pushAsset(
|
||||||
storedLayerSourceUrl,
|
storedLayerSourceUrl,
|
||||||
"PSD 图层素材",
|
"PSD 图层素材",
|
||||||
@ -1032,11 +1032,16 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
.png(PSD_RENDER_PNG_OPTIONS)
|
.png(PSD_RENDER_PNG_OPTIONS)
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
if (layer.maskUrl) {
|
const shouldUseLayerSourceAsMask = !layer.maskUrl && layer.forceClipMask;
|
||||||
|
if (layer.maskUrl || shouldUseLayerSourceAsMask) {
|
||||||
try {
|
try {
|
||||||
const maskBuffer = await sharp(
|
const rawMaskBuffer = layer.maskUrl
|
||||||
await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
|
? await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
|
||||||
)
|
: await readReusableLayerBuffer(layer);
|
||||||
|
if (!rawMaskBuffer) {
|
||||||
|
throw new Error("Missing PSD layer mask source");
|
||||||
|
}
|
||||||
|
const maskBuffer = await sharp(rawMaskBuffer)
|
||||||
.resize({
|
.resize({
|
||||||
width: Math.max(1, Math.round(layer.width)),
|
width: Math.max(1, Math.round(layer.width)),
|
||||||
height: Math.max(1, Math.round(layer.height)),
|
height: Math.max(1, Math.round(layer.height)),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user