From 6d60d53910cf8cb3d65aade75496cf514d3df617 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 17:59:37 +0800 Subject: [PATCH] fix: preserve PSD super lock mask editing --- .../psd-templates-settings-client.tsx | 7 +- .../agents/AgentPsdTemplateEditorDialog.tsx | 200 ++++++++++++++---- lib/agents/psd-template-renderer.ts | 21 +- 3 files changed, 169 insertions(+), 59 deletions(-) diff --git a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx index 1128ba03c..d248af3b3 100644 --- a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx @@ -1780,7 +1780,7 @@ export function PsdTemplatesSettingsClient({ 超级锁
- 开启后用户前台无法选中、编辑、拖动或临时删除该图层,鼠标点击会自动穿透到下方图层。 + 开启后锁定目标图层的位置、层级和删除操作;图片槽位仍可替换图片,并只能在蒙版内拖动替换图。
- layer.isEditable && - !isAgentPsdLayerSuperLocked(layer) && - isAgentPsdImageLayerSlot(layer) + layer.isEditable && isAgentPsdImageLayerSlot(layer) ); return ( @@ -912,9 +910,6 @@ function createInitialState( for (const layer of template.layers) { layerOrderIds.push(layer.id); - if (isAgentPsdLayerSuperLocked(layer)) { - continue; - } if ( isAgentPsdImageLayerSlot(layer) && layer.id === defaultSourceImageLayerId && @@ -923,6 +918,9 @@ function createInitialState( imageAssignments[layer.id] = sourceImageUrl; imagePlacements[layer.id] = getDefaultAgentPsdImagePlacement(); } + if (isAgentPsdLayerSuperLocked(layer)) { + continue; + } if (isAgentPsdTextLayerSlot(layer) && layer.defaultText) { textAssignments[layer.id] = layer.defaultText; } @@ -1732,6 +1730,13 @@ export function AgentPsdTemplateEditorDialog({ startClientY: number; startOffset: AgentPsdLayerPositionOffset; } | null>(null); + const imagePlacementDragStateRef = useRef<{ + layerId: string; + pointerId: number; + startClientX: number; + startClientY: number; + startPlacement: AgentPsdImagePlacement; + } | null>(null); const textDragStateRef = useRef<{ layerId: string; pointerId: number; @@ -1938,9 +1943,7 @@ export function AgentPsdTemplateEditorDialog({ () => selectedTemplateLayers.filter( (layer): layer is AgentPsdTemplateImageLayerSlot => - layer.isEditable && - !isAgentPsdLayerSuperLocked(layer) && - isAgentPsdImageLayerSlot(layer) + layer.isEditable && isAgentPsdImageLayerSlot(layer) ), [selectedTemplateLayers] ); @@ -2035,11 +2038,11 @@ export function AgentPsdTemplateEditorDialog({ ); const orderedEditableImageLayers = useMemo( () => - orderedEditableLayers.filter( + orderedPreviewLayers.filter( (layer): layer is AgentPsdTemplateImageLayerSlot => - isAgentPsdImageLayerSlot(layer) + layer.isEditable && isAgentPsdImageLayerSlot(layer) ), - [orderedEditableLayers] + [orderedPreviewLayers] ); const orderedEditableTextLayers = useMemo( () => @@ -2190,17 +2193,24 @@ export function AgentPsdTemplateEditorDialog({ const layer = selectedTemplateLayers.find((entry) => entry.id === selectedLayerId) || null; - return layer && !isAgentPsdLayerSuperLocked(layer) ? layer : null; + return layer; }, [selectedLayerId, selectedTemplateLayers]); + const isSelectedLayerSuperLocked = Boolean( + selectedLayer && isAgentPsdLayerSuperLocked(selectedLayer) + ); const isSelectedLayerHidden = Boolean( selectedLayer && hiddenLayerIdSet.has(selectedLayer.id) ); const selectedImageLayer = - selectedLayer && isAgentPsdImageLayerSlot(selectedLayer) + selectedLayer && + selectedLayer.isEditable && + isAgentPsdImageLayerSlot(selectedLayer) ? selectedLayer : null; const selectedTextLayer = - selectedLayer && isAgentPsdTextLayerSlot(selectedLayer) + selectedLayer && + !isSelectedLayerSuperLocked && + isAgentPsdTextLayerSlot(selectedLayer) ? selectedLayer : null; const selectedImageAssignmentUrl = selectedImageLayer @@ -2252,8 +2262,9 @@ export function AgentPsdTemplateEditorDialog({ current.filter((currentLayerId) => currentLayerId !== normalizedLayerId) ); } - const selectedLayerPositionPanel = selectedLayer ? ( -
+ const selectedLayerPositionPanel = + selectedLayer && !isSelectedLayerSuperLocked ? ( +
图层层级
@@ -2477,7 +2488,7 @@ export function AgentPsdTemplateEditorDialog({ 槽位内图片位置
- 图层整体拖动已经改到中间预览区;这里保留槽位内图片的细调。 + 目标图层保持固定;拖动预览中的替换图可在蒙版内微调位置。
@@ -3445,6 +3456,72 @@ export function AgentPsdTemplateEditorDialog({ window.addEventListener("pointercancel", handlePointerUp); } + function handleAssignedImagePlacementDragStart( + layer: AgentPsdTemplateImageLayerSlot, + event: ReactPointerEvent + ) { + const assignmentUrl = String( + effectiveImageAssignments[layer.id] || "" + ).trim(); + if (!assignmentUrl) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + setSelectedLayerId(layer.id); + + imagePlacementDragStateRef.current = { + layerId: layer.id, + pointerId: event.pointerId, + startClientX: event.clientX, + startClientY: event.clientY, + startPlacement: normalizeAgentPsdImagePlacement( + effectiveImagePlacements[layer.id] + ), + }; + + const nextTarget = event.currentTarget; + try { + nextTarget.setPointerCapture?.(event.pointerId); + } catch (error) { + console.warn( + "[AgentPsdTemplateEditorDialog] setPointerCapture failed:", + error + ); + } + + const handlePointerMove = (moveEvent: PointerEvent) => { + const dragState = imagePlacementDragStateRef.current; + if (!dragState || dragState.layerId !== layer.id) { + return; + } + + updateImagePlacement(layer.id, () => ({ + ...dragState.startPlacement, + offsetX: + dragState.startPlacement.offsetX + + (moveEvent.clientX - dragState.startClientX) / stageScale, + offsetY: + dragState.startPlacement.offsetY + + (moveEvent.clientY - dragState.startClientY) / stageScale, + })); + }; + + const handlePointerUp = (upEvent: PointerEvent) => { + if (imagePlacementDragStateRef.current?.pointerId === upEvent.pointerId) { + imagePlacementDragStateRef.current = null; + } + window.removeEventListener("pointermove", handlePointerMove); + window.removeEventListener("pointerup", handlePointerUp); + window.removeEventListener("pointercancel", handlePointerUp); + }; + + window.addEventListener("pointermove", handlePointerMove); + window.addEventListener("pointerup", handlePointerUp); + window.addEventListener("pointercancel", handlePointerUp); + } + function handleTextDragStart( layer: AgentPsdTemplateTextLayerSlot, event: ReactPointerEvent @@ -4140,6 +4217,11 @@ export function AgentPsdTemplateEditorDialog({ if (isAgentPsdImageLayerSlot(layer)) { const assignedUrl = effectiveImageAssignments[layer.id]; + const hasAssignedImage = Boolean( + String(assignedUrl || "").trim() + ); + const canDragAssignedImage = + isLayerSuperLocked && hasAssignedImage; const layerImageUrl = assignedUrl || getPsdLayerPreviewSourceUrl(layer); @@ -4156,39 +4238,51 @@ export function AgentPsdTemplateEditorDialog({ return (
{ - event.stopPropagation(); - setSelectedLayerId(layer.id); - } - } - onPointerDown={(event) => - isLayerSuperLocked - ? undefined - : handleLayerDragStart(layer, event) - } + onClick={(event) => { + event.stopPropagation(); + setSelectedLayerId(layer.id); + }} + onPointerDown={(event) => { + if (canDragAssignedImage) { + handleAssignedImagePlacementDragStart( + layer, + event + ); + return; + } + if (!isLayerSuperLocked) { + handleLayerDragStart(layer, event); + } + }} style={{ ...assignedImageContainerStyle, zIndex: baseZIndex, - cursor: - isLayerSuperLocked + cursor: canDragAssignedImage + ? imagePlacementDragStateRef.current + ?.layerId === layer.id + ? "grabbing" + : "grab" + : isLayerSuperLocked ? "default" : layerDragStateRef.current?.layerId === layer.id ? "grabbing" : "grab", - touchAction: isLayerSuperLocked - ? "auto" - : "none", + touchAction: + isLayerSuperLocked && !hasAssignedImage + ? "auto" + : "none", }} - title={`${getLayerLabel(layer)}:${layer.layerName}`} + title={ + canDragAssignedImage + ? `${getLayerLabel(layer)}:${layer.layerName}(目标已锁定,可拖动替换图)` + : `${getLayerLabel(layer)}:${layer.layerName}` + } > {layer.layerName} setSelectedLayerId(layer.id)} - onPointerDown={(event) => - handleLayerDragStart(layer, event) - } + onPointerDown={(event) => { + if (isLayerSuperLocked && hasAssignedImage) { + handleAssignedImagePlacementDragStart( + layer, + event + ); + return; + } + if (!isLayerSuperLocked) { + handleLayerDragStart(layer, event); + } + }} style={{ ...frameStyle, zIndex: @@ -4470,10 +4575,15 @@ export function AgentPsdTemplateEditorDialog({ ? "rgba(249,115,22,0.08)" : "transparent", cursor: - layerDragStateRef.current?.layerId === - layer.id - ? "grabbing" - : "grab", + isLayerSuperLocked && hasAssignedImage + ? imagePlacementDragStateRef.current + ?.layerId === layer.id + ? "grabbing" + : "grab" + : layerDragStateRef.current?.layerId === + layer.id + ? "grabbing" + : "grab", touchAction: "none", }} title={`${getLayerLabel(layer)}:${layer.layerName}`} diff --git a/lib/agents/psd-template-renderer.ts b/lib/agents/psd-template-renderer.ts index 28d2e1fbf..d9a45880c 100644 --- a/lib/agents/psd-template-renderer.ts +++ b/lib/agents/psd-template-renderer.ts @@ -576,13 +576,13 @@ export async function renderAgentPsdTemplate(params: { for (const layer of orderedTemplateLayers) { if (hiddenLayerIdSet.has(layer.id)) { continue; - } - - const storedLayerSourceUrl = getStoredLayerSourceUrl(layer); - if (shouldRenderAsStaticLayer(layer)) { - if (shouldUseRenderBaseFallback || shouldUseRenderSegments) { - continue; - } + } + + const storedLayerSourceUrl = getStoredLayerSourceUrl(layer); + if (shouldRenderAsStaticLayer(layer)) { + if (shouldUseRenderBaseFallback || shouldUseRenderSegments) { + continue; + } pushAsset( storedLayerSourceUrl, "PSD 图层素材", @@ -913,7 +913,10 @@ export async function renderAgentPsdTemplate(params: { } for (const layer of [...orderedTemplateLayers].reverse()) { - if (shouldUseRenderSegments && isAgentPsdTemplateLayerUserEditable(layer)) { + if ( + shouldUseRenderSegments && + (isAgentPsdTemplateLayerUserEditable(layer) || hasImageAssignment(layer)) + ) { await appendRenderSegmentBeforeLayer(layer.id); } @@ -921,7 +924,7 @@ export async function renderAgentPsdTemplate(params: { continue; } - if (!isAgentPsdTemplateLayerUserEditable(layer)) { + if (shouldRenderAsStaticLayer(layer)) { if (shouldUseRenderBaseFallback || shouldUseRenderSegments) { continue; }