From a78fd8a0d46850f34515ef75d4a8e7dff4e745e0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 20:57:25 +0800 Subject: [PATCH] fix: keep PSD clipped replacements in target layer order --- .../agents/AgentPsdTemplateEditorDialog.tsx | 20 +- lib/agents/psd-template-renderer.ts | 352 ++++++++++-------- 2 files changed, 205 insertions(+), 167 deletions(-) diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index d98f2e3f1..3ffde39f1 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -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 ( diff --git a/lib/agents/psd-template-renderer.ts b/lib/agents/psd-template-renderer.ts index ead38dcb6..71de72acf 100644 --- a/lib/agents/psd-template-renderer.ts +++ b/lib/agents/psd-template-renderer.ts @@ -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(); + 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(); + 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); } }