fix: keep PSD clipped replacements in target layer order

This commit is contained in:
Your Name 2026-06-01 20:57:25 +08:00
parent 87be5499d5
commit a78fd8a0d4
2 changed files with 205 additions and 167 deletions

View File

@ -4210,11 +4210,6 @@ export function AgentPsdTemplateEditorDialog({
if (isAgentPsdImageLayerSlot(layer)) {
const assignedUrl =
effectiveImageAssignments[layer.id];
const isDefaultAssignedImageLayer =
layer.id ===
getDefaultSourceImageLayer(
selectedTemplateLayers
)?.id;
const layerImageUrl =
assignedUrl ||
getPsdLayerPreviewSourceUrl(layer);
@ -4251,15 +4246,14 @@ export function AgentPsdTemplateEditorDialog({
),
}
: layerBoxStyle;
const assignedFrameBaseZIndex =
layerStackZIndexById.get(
assignedFrameLayer.id
) || baseZIndex;
const previewZIndex = assignedUrl
? Math.max(
baseZIndex,
renderBaseZIndex + 1,
isDefaultAssignedImageLayer
? (orderedPreviewLayers.length + 1) *
PSD_LAYER_Z_INDEX_STEP
: 0
)
? assignedFrameLayer.id === layer.id
? Math.max(baseZIndex, renderBaseZIndex + 1)
: assignedFrameBaseZIndex + 1
: baseZIndex;
return (

View File

@ -912,6 +912,195 @@ export async function renderAgentPsdTemplate(params: {
}
}
function getAssignedImageRenderFrameLayer(layer: AgentPsdTemplateImageLayerSlot) {
if (!layer.forceClipMask || !layer.clipMaskLayerId) {
return layer;
}
return layerMap.get(layer.clipMaskLayerId) || layer;
}
const assignedImageLayersByFrameId = new Map<string, AgentPsdTemplateImageLayerSlot[]>();
for (const layer of orderedTemplateLayers) {
if (
hiddenLayerIdSet.has(layer.id) ||
!isAgentPsdImageLayerSlot(layer) ||
!String(imageAssignments[layer.id] || "").trim()
) {
continue;
}
const frameLayer = getAssignedImageRenderFrameLayer(layer);
const currentLayers = assignedImageLayersByFrameId.get(frameLayer.id) || [];
currentLayers.push(layer);
assignedImageLayersByFrameId.set(frameLayer.id, currentLayers);
}
const appendedImageAssignmentLayerIds = new Set<string>();
async function appendAssignedImageLayer(layer: AgentPsdTemplateImageLayerSlot) {
if (appendedImageAssignmentLayerIds.has(layer.id)) {
return;
}
const isDefaultSourceImageLayer = layer.id === defaultSourceImageLayerId;
const assignmentUrl = String(imageAssignments[layer.id] || "").trim();
if (!assignmentUrl || hiddenLayerIdSet.has(layer.id)) {
return;
}
appendedImageAssignmentLayerIds.add(layer.id);
const sourceBuffer = await readCachedBuffer(assignmentUrl, "PSD 套版素材");
const clipMaskLayer =
layer.forceClipMask && layer.clipMaskLayerId
? layerMap.get(layer.clipMaskLayerId)
: null;
const renderFrameLayer = clipMaskLayer || layer;
const renderFrameContentBounds =
clipMaskLayer && isAgentPsdImageLayerSlot(clipMaskLayer)
? clipMaskLayer.contentBounds
: layer.contentBounds;
const sourceImage = sharp(sourceBuffer, { failOn: "none" }).rotate();
const metadata = await sourceImage.clone().metadata();
const placementFrame = resolveAgentPsdImagePlacementFrame({
containerWidth: renderFrameLayer.width,
containerHeight: renderFrameLayer.height,
imageWidth: Math.max(1, Number(metadata.width || 1)),
imageHeight: Math.max(1, Number(metadata.height || 1)),
replaceMode: layer.replaceMode,
contentBounds: renderFrameContentBounds,
placement: imagePlacements[layer.id],
});
const resizedOverlayBuffer = await sourceImage
.clone()
.resize({
width: Math.max(1, Math.round(placementFrame.width)),
height: Math.max(1, Math.round(placementFrame.height)),
fit:
layer.replaceMode === "contain"
? "contain"
: layer.replaceMode === "stretch"
? "fill"
: "cover",
background: {
r: 0,
g: 0,
b: 0,
alpha: 0,
},
})
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
const slotOverlay = await fitOverlayBufferIntoCanvas({
input: resizedOverlayBuffer,
left: placementFrame.left,
top: placementFrame.top,
width: placementFrame.width,
height: placementFrame.height,
canvasWidth: renderFrameLayer.width,
canvasHeight: renderFrameLayer.height,
});
if (!slotOverlay) {
return;
}
const layerPositionOffset = getLayerPositionOffset(renderFrameLayer.id);
const shouldApplyMask = Boolean(layer.maskUrl || clipMaskLayer);
if (!shouldApplyMask) {
if (isDefaultSourceImageLayer) {
await appendRenderBaseOverlay();
}
await appendBufferOverlay({
input: slotOverlay.input,
left: renderFrameLayer.x + layerPositionOffset.offsetX + slotOverlay.left,
top: renderFrameLayer.y + layerPositionOffset.offsetY + slotOverlay.top,
width: slotOverlay.width,
height: slotOverlay.height,
});
return;
}
let overlayBuffer = await sharp({
create: {
width: Math.max(1, Math.round(renderFrameLayer.width)),
height: Math.max(1, Math.round(renderFrameLayer.height)),
channels: 4,
background: {
r: 0,
g: 0,
b: 0,
alpha: 0,
},
},
})
.composite([slotOverlay])
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
try {
const rawMaskBuffer = layer.maskUrl
? await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
: await readReusableLayerBuffer(clipMaskLayer!);
if (!rawMaskBuffer) {
throw new Error("Missing PSD layer mask source");
}
const maskBuffer = await sharp(rawMaskBuffer)
.resize({
width: Math.max(1, Math.round(renderFrameLayer.width)),
height: Math.max(1, Math.round(renderFrameLayer.height)),
fit: "fill",
background: {
r: 0,
g: 0,
b: 0,
alpha: 0,
},
})
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
overlayBuffer = await sharp(overlayBuffer)
.composite([
{
input: maskBuffer,
blend: "dest-in",
},
])
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
} catch (error) {
console.warn(
"[PSD Template] failed to apply layer mask during render:",
{
templateId: template.id,
layerId: layer.layerId,
error,
}
);
}
if (isDefaultSourceImageLayer) {
await appendRenderBaseOverlay();
}
await appendBufferOverlay({
input: overlayBuffer,
left: renderFrameLayer.x + layerPositionOffset.offsetX,
top: renderFrameLayer.y + layerPositionOffset.offsetY,
width: renderFrameLayer.width,
height: renderFrameLayer.height,
});
}
async function appendAssignedImagesForFrameLayer(frameLayerId: string) {
for (const assignedLayer of assignedImageLayersByFrameId.get(frameLayerId) || []) {
if (assignedLayer.id !== frameLayerId) {
await appendAssignedImageLayer(assignedLayer);
}
}
}
if (
shouldUseRenderBaseFallback &&
(!defaultSourceImageLayerId || hiddenLayerIdSet.has(defaultSourceImageLayerId))
@ -930,9 +1119,11 @@ export async function renderAgentPsdTemplate(params: {
if (!isAgentPsdTemplateLayerUserEditable(layer)) {
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
await appendAssignedImagesForFrameLayer(layer.id);
continue;
}
await appendStoredLayer(layer, { optional: shouldUseRenderBaseFallback });
await appendAssignedImagesForFrameLayer(layer.id);
continue;
}
@ -946,164 +1137,15 @@ export async function renderAgentPsdTemplate(params: {
if (isDefaultSourceImageLayer) {
await appendRenderBaseOverlay();
}
await appendAssignedImagesForFrameLayer(layer.id);
continue;
}
const sourceBuffer = await readCachedBuffer(
assignmentUrl,
"PSD 套版素材"
);
const clipMaskLayer =
layer.forceClipMask && layer.clipMaskLayerId
? layerMap.get(layer.clipMaskLayerId)
: null;
const renderFrameLayer = clipMaskLayer || layer;
const renderFrameContentBounds =
clipMaskLayer && isAgentPsdImageLayerSlot(clipMaskLayer)
? clipMaskLayer.contentBounds
: layer.contentBounds;
const sourceImage = sharp(sourceBuffer, { failOn: "none" }).rotate();
const metadata = await sourceImage.clone().metadata();
const placementFrame = resolveAgentPsdImagePlacementFrame({
containerWidth: renderFrameLayer.width,
containerHeight: renderFrameLayer.height,
imageWidth: Math.max(1, Number(metadata.width || 1)),
imageHeight: Math.max(1, Number(metadata.height || 1)),
replaceMode: layer.replaceMode,
contentBounds: renderFrameContentBounds,
placement: imagePlacements[layer.id],
});
const resizedOverlayBuffer = await sourceImage
.clone()
.resize({
width: Math.max(1, Math.round(placementFrame.width)),
height: Math.max(1, Math.round(placementFrame.height)),
fit:
layer.replaceMode === "contain"
? "contain"
: layer.replaceMode === "stretch"
? "fill"
: "cover",
background: {
r: 0,
g: 0,
b: 0,
alpha: 0,
},
})
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
const slotOverlay = await fitOverlayBufferIntoCanvas({
input: resizedOverlayBuffer,
left: placementFrame.left,
top: placementFrame.top,
width: placementFrame.width,
height: placementFrame.height,
canvasWidth: renderFrameLayer.width,
canvasHeight: renderFrameLayer.height,
});
if (!slotOverlay) {
continue;
const renderFrameLayer = getAssignedImageRenderFrameLayer(layer);
if (renderFrameLayer.id === layer.id) {
await appendAssignedImageLayer(layer);
}
const layerPositionOffset = getLayerPositionOffset(renderFrameLayer.id);
const shouldApplyMask = Boolean(layer.maskUrl || clipMaskLayer);
if (!shouldApplyMask) {
if (isDefaultSourceImageLayer) {
await appendRenderBaseOverlay();
}
const didAppend = await appendBufferOverlay({
input: slotOverlay.input,
left:
renderFrameLayer.x +
layerPositionOffset.offsetX +
slotOverlay.left,
top:
renderFrameLayer.y +
layerPositionOffset.offsetY +
slotOverlay.top,
width: slotOverlay.width,
height: slotOverlay.height,
});
if (!didAppend) continue;
continue;
}
let overlayBuffer = await sharp({
create: {
width: Math.max(1, Math.round(renderFrameLayer.width)),
height: Math.max(1, Math.round(renderFrameLayer.height)),
channels: 4,
background: {
r: 0,
g: 0,
b: 0,
alpha: 0,
},
},
})
.composite([slotOverlay])
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
if (shouldApplyMask) {
try {
const rawMaskBuffer = layer.maskUrl
? await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
: await readReusableLayerBuffer(clipMaskLayer!);
if (!rawMaskBuffer) {
throw new Error("Missing PSD layer mask source");
}
const maskBuffer = await sharp(rawMaskBuffer)
.resize({
width: Math.max(1, Math.round(renderFrameLayer.width)),
height: Math.max(1, Math.round(renderFrameLayer.height)),
fit: "fill",
background: {
r: 0,
g: 0,
b: 0,
alpha: 0,
},
})
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
overlayBuffer = await sharp(overlayBuffer)
.composite([
{
input: maskBuffer,
blend: "dest-in",
},
])
.png(PSD_RENDER_PNG_OPTIONS)
.toBuffer();
} catch (error) {
console.warn(
"[PSD Template] failed to apply layer mask during render:",
{
templateId: template.id,
layerId: layer.layerId,
error,
}
);
}
}
if (isDefaultSourceImageLayer) {
await appendRenderBaseOverlay();
}
const didAppend = await appendBufferOverlay({
input: overlayBuffer,
left: renderFrameLayer.x + layerPositionOffset.offsetX,
top: renderFrameLayer.y + layerPositionOffset.offsetY,
width: renderFrameLayer.width,
height: renderFrameLayer.height,
});
if (!didAppend) continue;
await appendAssignedImagesForFrameLayer(layer.id);
continue;
}
@ -1127,6 +1169,7 @@ export async function renderAgentPsdTemplate(params: {
optional: shouldUseRenderBaseFallback,
});
}
await appendAssignedImagesForFrameLayer(layer.id);
continue;
}
const textAlign = resolveAgentPsdTextLayerEffectiveAlign(layer);
@ -1236,6 +1279,7 @@ export async function renderAgentPsdTemplate(params: {
height: overlayFrame.height,
});
if (!didAppend) continue;
await appendAssignedImagesForFrameLayer(layer.id);
}
}