From 87be5499d5e1237da06478df2c98e056002decf5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 20:38:45 +0800 Subject: [PATCH] fix: anchor PSD replacements to clip targets --- .../agents/AgentPsdTemplateEditorDialog.tsx | 70 ++++++++++++++++--- lib/agents/psd-template-renderer.ts | 45 ++++++------ 2 files changed, 87 insertions(+), 28 deletions(-) diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index 3b3809ffe..d98f2e3f1 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -1498,6 +1498,7 @@ function renderTextLayerPreviewContent( function getAssignedImageMaskStyle( layer: AgentPsdTemplateImageLayerSlot, + frameLayer: AgentPsdTemplateLayerSlot, layers: AgentPsdTemplateLayerSlot[] ): CSSProperties { const clipMaskLayer = layer.clipMaskLayerId @@ -1512,7 +1513,7 @@ function getAssignedImageMaskStyle( ).trim(); const hasMask = Boolean(normalizedMaskUrl); const maskImageUrl = hasMask - ? `url(${buildPsdLayerMaskUrl(normalizedMaskUrl, layer.width)})` + ? `url(${buildPsdLayerMaskUrl(normalizedMaskUrl, frameLayer.width)})` : undefined; return { WebkitMaskImage: maskImageUrl, @@ -1529,18 +1530,22 @@ function getAssignedImageMaskStyle( function getPlacedAssignedImageStyle(params: { layer: AgentPsdTemplateImageLayerSlot; + frameLayer?: AgentPsdTemplateLayerSlot | null; imageSize: AgentPsdImageSize; placement: AgentPsdImagePlacement; }): CSSProperties { const { layer, imageSize, placement } = params; + const frameLayer = params.frameLayer || layer; const normalizedPlacement = normalizeAgentPsdImagePlacement(placement); + const frameContentBounds = + isAgentPsdImageLayerSlot(frameLayer) ? frameLayer.contentBounds : undefined; const baseFrame = resolveAgentPsdImageBaseFrame({ - containerWidth: layer.width, - containerHeight: layer.height, + containerWidth: frameLayer.width, + containerHeight: frameLayer.height, imageWidth: imageSize.width, imageHeight: imageSize.height, replaceMode: layer.replaceMode, - contentBounds: layer.contentBounds, + contentBounds: frameContentBounds, }); return { @@ -1558,6 +1563,17 @@ function getPlacedAssignedImageStyle(params: { }; } +function getAssignedImageFrameLayer( + layer: AgentPsdTemplateImageLayerSlot, + layers: AgentPsdTemplateLayerSlot[] +) { + if (!layer.forceClipMask || !layer.clipMaskLayerId) { + return layer; + } + + return layers.find((item) => item.id === layer.clipMaskLayerId) || layer; +} + function hasEditedTextValue( layer: AgentPsdTemplateTextLayerSlot, value: string @@ -3432,15 +3448,18 @@ export function AgentPsdTemplateEditorDialog({ event.preventDefault(); event.stopPropagation(); setSelectedLayerId(layer.id); + const shouldDragAssignedImageContent = + isAgentPsdImageLayerSlot(layer) && + Boolean(String(effectiveImageAssignments[layer.id] || "").trim()); layerDragStateRef.current = { layerId: layer.id, pointerId: event.pointerId, startClientX: event.clientX, startClientY: event.clientY, - startOffset: normalizeLayerPositionOffset( - effectiveLayerPositionOffsets[layer.id] - ), + startOffset: shouldDragAssignedImageContent + ? normalizeLayerPositionOffset(effectiveImagePlacements[layer.id]) + : normalizeLayerPositionOffset(effectiveLayerPositionOffsets[layer.id]), }; const nextTarget = event.currentTarget; @@ -3459,6 +3478,19 @@ export function AgentPsdTemplateEditorDialog({ return; } + if (shouldDragAssignedImageContent) { + updateImagePlacement(layer.id, (current) => ({ + ...current, + offsetX: + dragState.startOffset.offsetX + + (moveEvent.clientX - dragState.startClientX) / stageScale, + offsetY: + dragState.startOffset.offsetY + + (moveEvent.clientY - dragState.startClientY) / stageScale, + })); + return; + } + updateLayerPositionOffset(layer.id, () => ({ offsetX: dragState.startOffset.offsetX + @@ -4189,11 +4221,32 @@ export function AgentPsdTemplateEditorDialog({ if (!layerImageUrl) { return null; } + const assignedFrameLayer = assignedUrl + ? getAssignedImageFrameLayer( + layer, + selectedTemplateLayers + ) + : layer; + const assignedFrameOffset = + effectiveLayerPositionOffsets[ + assignedFrameLayer.id + ] || normalizeLayerPositionOffset(); + const assignedFrameBoxStyle = { + left: + assignedFrameLayer.x + + assignedFrameOffset.offsetX, + top: + assignedFrameLayer.y + + assignedFrameOffset.offsetY, + width: assignedFrameLayer.width, + height: assignedFrameLayer.height, + } satisfies CSSProperties; const assignedImageContainerStyle = assignedUrl ? { - ...layerBoxStyle, + ...assignedFrameBoxStyle, ...getAssignedImageMaskStyle( layer, + assignedFrameLayer, selectedTemplateLayers ), } @@ -4263,6 +4316,7 @@ export function AgentPsdTemplateEditorDialog({ assignedUrl && imageSizes[layer.id] ? getPlacedAssignedImageStyle({ layer, + frameLayer: assignedFrameLayer, imageSize: imageSizes[layer.id], placement: effectiveImagePlacements[ diff --git a/lib/agents/psd-template-renderer.ts b/lib/agents/psd-template-renderer.ts index fd764a476..ead38dcb6 100644 --- a/lib/agents/psd-template-renderer.ts +++ b/lib/agents/psd-template-renderer.ts @@ -953,15 +953,24 @@ export async function renderAgentPsdTemplate(params: { 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: layer.width, - containerHeight: layer.height, + 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: layer.contentBounds, + contentBounds: renderFrameContentBounds, placement: imagePlacements[layer.id], }); @@ -992,19 +1001,15 @@ export async function renderAgentPsdTemplate(params: { top: placementFrame.top, width: placementFrame.width, height: placementFrame.height, - canvasWidth: layer.width, - canvasHeight: layer.height, + canvasWidth: renderFrameLayer.width, + canvasHeight: renderFrameLayer.height, }); if (!slotOverlay) { continue; } - const layerPositionOffset = getLayerPositionOffset(layer.id); - const clipMaskLayer = - layer.forceClipMask && layer.clipMaskLayerId - ? layerMap.get(layer.clipMaskLayerId) - : null; + const layerPositionOffset = getLayerPositionOffset(renderFrameLayer.id); const shouldApplyMask = Boolean(layer.maskUrl || clipMaskLayer); if (!shouldApplyMask) { if (isDefaultSourceImageLayer) { @@ -1013,11 +1018,11 @@ export async function renderAgentPsdTemplate(params: { const didAppend = await appendBufferOverlay({ input: slotOverlay.input, left: - layer.x + + renderFrameLayer.x + layerPositionOffset.offsetX + slotOverlay.left, top: - layer.y + + renderFrameLayer.y + layerPositionOffset.offsetY + slotOverlay.top, width: slotOverlay.width, @@ -1029,8 +1034,8 @@ export async function renderAgentPsdTemplate(params: { let overlayBuffer = await sharp({ create: { - width: Math.max(1, Math.round(layer.width)), - height: Math.max(1, Math.round(layer.height)), + width: Math.max(1, Math.round(renderFrameLayer.width)), + height: Math.max(1, Math.round(renderFrameLayer.height)), channels: 4, background: { r: 0, @@ -1054,8 +1059,8 @@ export async function renderAgentPsdTemplate(params: { } const maskBuffer = await sharp(rawMaskBuffer) .resize({ - width: Math.max(1, Math.round(layer.width)), - height: Math.max(1, Math.round(layer.height)), + width: Math.max(1, Math.round(renderFrameLayer.width)), + height: Math.max(1, Math.round(renderFrameLayer.height)), fit: "fill", background: { r: 0, @@ -1093,10 +1098,10 @@ export async function renderAgentPsdTemplate(params: { const didAppend = await appendBufferOverlay({ input: overlayBuffer, - left: layer.x + layerPositionOffset.offsetX, - top: layer.y + layerPositionOffset.offsetY, - width: layer.width, - height: layer.height, + left: renderFrameLayer.x + layerPositionOffset.offsetX, + top: renderFrameLayer.y + layerPositionOffset.offsetY, + width: renderFrameLayer.width, + height: renderFrameLayer.height, }); if (!didAppend) continue; continue;