fix: support PSD clipping mask target layers
This commit is contained in:
parent
c00b9e333d
commit
cffed9ab50
@ -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({
|
||||
: "当前筛选条件下没有图层,请换个关键词试试。"}
|
||||
</div>
|
||||
) : (
|
||||
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 (
|
||||
<div key={layer.id} className="rounded-2xl border p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
@ -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({
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-sm font-medium">强制剪切蒙版</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
开启后替换图会按当前 PSD 图层原始透明区域剪切,适合需要贴合固定形状的图片槽位。
|
||||
开启后替换图会按指定目标图层的透明区域剪切,适合替换图需要贴合超级锁或固定形状的图片槽位。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@ -2008,6 +2030,10 @@ export function PsdTemplatesSettingsClient({
|
||||
forceClipMask: current.isEditable
|
||||
? checked
|
||||
: false,
|
||||
clipMaskLayerId:
|
||||
current.isEditable && checked
|
||||
? current.clipMaskLayerId
|
||||
: undefined,
|
||||
}
|
||||
: current
|
||||
)
|
||||
@ -2015,6 +2041,55 @@ export function PsdTemplatesSettingsClient({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{layer.forceClipMask ? (
|
||||
<div className="space-y-2 rounded-xl border p-3 md:col-span-2 xl:col-span-3">
|
||||
<div className="text-sm font-medium">剪切蒙版目标图层</div>
|
||||
<Select
|
||||
value={
|
||||
layer.clipMaskLayerId ||
|
||||
SELF_CLIP_MASK_SELECT_VALUE
|
||||
}
|
||||
disabled={
|
||||
!layer.isEditable ||
|
||||
layer.isSuperLocked === true
|
||||
}
|
||||
onValueChange={(value) =>
|
||||
updateDraftLayer(layer.id, (current) =>
|
||||
current.slotType === "image"
|
||||
? {
|
||||
...current,
|
||||
clipMaskLayerId:
|
||||
value === SELF_CLIP_MASK_SELECT_VALUE
|
||||
? undefined
|
||||
: value,
|
||||
}
|
||||
: current
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择用于裁切的目标图层" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={SELF_CLIP_MASK_SELECT_VALUE}>
|
||||
使用当前替换图层自身透明区域
|
||||
</SelectItem>
|
||||
{clipMaskLayerOptions.map((candidate) => (
|
||||
<SelectItem
|
||||
key={`${layer.id}-clip-mask-${candidate.id}`}
|
||||
value={candidate.id}
|
||||
>
|
||||
{candidate.layerName} · {candidate.width} x {candidate.height}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
如果 PSD 里目标形状是超级锁图层,请在这里选择那个目标图层;前台只能移动替换图,目标图仍保持锁定不可选中。
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2 rounded-xl border p-3">
|
||||
<div className="text-sm font-medium">填充模式</div>
|
||||
<Select
|
||||
@ -2327,7 +2402,8 @@ export function PsdTemplatesSettingsClient({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user