fix: preserve PSD super lock mask editing

This commit is contained in:
Your Name 2026-06-01 17:59:37 +08:00
parent f050c807a4
commit 6d60d53910
3 changed files with 169 additions and 59 deletions

View File

@ -1780,7 +1780,7 @@ export function PsdTemplatesSettingsClient({
超级锁 超级锁
</div> </div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
开启后用户前台无法选中、编辑、拖动或临时删除该图层,鼠标点击会自动穿透到下方图层。 开启后锁定目标图层的位置、层级和删除操作;图片槽位仍可替换图片,并只能在蒙版内拖动替换图。
</div> </div>
</div> </div>
<Switch <Switch
@ -1792,11 +1792,8 @@ export function PsdTemplatesSettingsClient({
...current, ...current,
isSuperLocked: checked, isSuperLocked: checked,
isRequired: checked ? false : current.isRequired, isRequired: checked ? false : current.isRequired,
isDefaultResultSource: checked
? false
: current.isDefaultResultSource,
allowUserUploadOverride: checked allowUserUploadOverride: checked
? false ? true
: current.allowUserUploadOverride, : current.allowUserUploadOverride,
}; };
} }

View File

@ -872,9 +872,7 @@ function getDefaultSourceImageLayer(
): AgentPsdTemplateImageLayerSlot | null { ): AgentPsdTemplateImageLayerSlot | null {
const editableImageLayers = layers.filter( const editableImageLayers = layers.filter(
(layer): layer is AgentPsdTemplateImageLayerSlot => (layer): layer is AgentPsdTemplateImageLayerSlot =>
layer.isEditable && layer.isEditable && isAgentPsdImageLayerSlot(layer)
!isAgentPsdLayerSuperLocked(layer) &&
isAgentPsdImageLayerSlot(layer)
); );
return ( return (
@ -912,9 +910,6 @@ function createInitialState(
for (const layer of template.layers) { for (const layer of template.layers) {
layerOrderIds.push(layer.id); layerOrderIds.push(layer.id);
if (isAgentPsdLayerSuperLocked(layer)) {
continue;
}
if ( if (
isAgentPsdImageLayerSlot(layer) && isAgentPsdImageLayerSlot(layer) &&
layer.id === defaultSourceImageLayerId && layer.id === defaultSourceImageLayerId &&
@ -923,6 +918,9 @@ function createInitialState(
imageAssignments[layer.id] = sourceImageUrl; imageAssignments[layer.id] = sourceImageUrl;
imagePlacements[layer.id] = getDefaultAgentPsdImagePlacement(); imagePlacements[layer.id] = getDefaultAgentPsdImagePlacement();
} }
if (isAgentPsdLayerSuperLocked(layer)) {
continue;
}
if (isAgentPsdTextLayerSlot(layer) && layer.defaultText) { if (isAgentPsdTextLayerSlot(layer) && layer.defaultText) {
textAssignments[layer.id] = layer.defaultText; textAssignments[layer.id] = layer.defaultText;
} }
@ -1732,6 +1730,13 @@ export function AgentPsdTemplateEditorDialog({
startClientY: number; startClientY: number;
startOffset: AgentPsdLayerPositionOffset; startOffset: AgentPsdLayerPositionOffset;
} | null>(null); } | null>(null);
const imagePlacementDragStateRef = useRef<{
layerId: string;
pointerId: number;
startClientX: number;
startClientY: number;
startPlacement: AgentPsdImagePlacement;
} | null>(null);
const textDragStateRef = useRef<{ const textDragStateRef = useRef<{
layerId: string; layerId: string;
pointerId: number; pointerId: number;
@ -1938,9 +1943,7 @@ export function AgentPsdTemplateEditorDialog({
() => () =>
selectedTemplateLayers.filter( selectedTemplateLayers.filter(
(layer): layer is AgentPsdTemplateImageLayerSlot => (layer): layer is AgentPsdTemplateImageLayerSlot =>
layer.isEditable && layer.isEditable && isAgentPsdImageLayerSlot(layer)
!isAgentPsdLayerSuperLocked(layer) &&
isAgentPsdImageLayerSlot(layer)
), ),
[selectedTemplateLayers] [selectedTemplateLayers]
); );
@ -2035,11 +2038,11 @@ export function AgentPsdTemplateEditorDialog({
); );
const orderedEditableImageLayers = useMemo( const orderedEditableImageLayers = useMemo(
() => () =>
orderedEditableLayers.filter( orderedPreviewLayers.filter(
(layer): layer is AgentPsdTemplateImageLayerSlot => (layer): layer is AgentPsdTemplateImageLayerSlot =>
isAgentPsdImageLayerSlot(layer) layer.isEditable && isAgentPsdImageLayerSlot(layer)
), ),
[orderedEditableLayers] [orderedPreviewLayers]
); );
const orderedEditableTextLayers = useMemo( const orderedEditableTextLayers = useMemo(
() => () =>
@ -2190,17 +2193,24 @@ export function AgentPsdTemplateEditorDialog({
const layer = const layer =
selectedTemplateLayers.find((entry) => entry.id === selectedLayerId) || selectedTemplateLayers.find((entry) => entry.id === selectedLayerId) ||
null; null;
return layer && !isAgentPsdLayerSuperLocked(layer) ? layer : null; return layer;
}, [selectedLayerId, selectedTemplateLayers]); }, [selectedLayerId, selectedTemplateLayers]);
const isSelectedLayerSuperLocked = Boolean(
selectedLayer && isAgentPsdLayerSuperLocked(selectedLayer)
);
const isSelectedLayerHidden = Boolean( const isSelectedLayerHidden = Boolean(
selectedLayer && hiddenLayerIdSet.has(selectedLayer.id) selectedLayer && hiddenLayerIdSet.has(selectedLayer.id)
); );
const selectedImageLayer = const selectedImageLayer =
selectedLayer && isAgentPsdImageLayerSlot(selectedLayer) selectedLayer &&
selectedLayer.isEditable &&
isAgentPsdImageLayerSlot(selectedLayer)
? selectedLayer ? selectedLayer
: null; : null;
const selectedTextLayer = const selectedTextLayer =
selectedLayer && isAgentPsdTextLayerSlot(selectedLayer) selectedLayer &&
!isSelectedLayerSuperLocked &&
isAgentPsdTextLayerSlot(selectedLayer)
? selectedLayer ? selectedLayer
: null; : null;
const selectedImageAssignmentUrl = selectedImageLayer const selectedImageAssignmentUrl = selectedImageLayer
@ -2252,8 +2262,9 @@ export function AgentPsdTemplateEditorDialog({
current.filter((currentLayerId) => currentLayerId !== normalizedLayerId) current.filter((currentLayerId) => currentLayerId !== normalizedLayerId)
); );
} }
const selectedLayerPositionPanel = selectedLayer ? ( const selectedLayerPositionPanel =
<div className="space-y-4 rounded-2xl border border-white/10 bg-white/[0.03] p-5 shadow-inner"> selectedLayer && !isSelectedLayerSuperLocked ? (
<div className="space-y-4 rounded-2xl border border-white/10 bg-white/[0.03] p-5 shadow-inner">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div> <div>
<div className="font-semibold text-sm text-white">图层层级</div> <div className="font-semibold text-sm text-white">图层层级</div>
@ -2477,7 +2488,7 @@ export function AgentPsdTemplateEditorDialog({
槽位内图片位置 槽位内图片位置
</div> </div>
<div className="mt-1.5 text-white/50 text-xs"> <div className="mt-1.5 text-white/50 text-xs">
图层整体拖动已经改到中间预览区;这里保留槽位内图片的细调。 目标图层保持固定;拖动预览中的替换图可在蒙版内微调位置。
</div> </div>
</div> </div>
<ImageIcon className="h-5 w-5 shrink-0 text-orange-300" /> <ImageIcon className="h-5 w-5 shrink-0 text-orange-300" />
@ -3445,6 +3456,72 @@ export function AgentPsdTemplateEditorDialog({
window.addEventListener("pointercancel", handlePointerUp); window.addEventListener("pointercancel", handlePointerUp);
} }
function handleAssignedImagePlacementDragStart(
layer: AgentPsdTemplateImageLayerSlot,
event: ReactPointerEvent<HTMLElement>
) {
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( function handleTextDragStart(
layer: AgentPsdTemplateTextLayerSlot, layer: AgentPsdTemplateTextLayerSlot,
event: ReactPointerEvent<HTMLDivElement> event: ReactPointerEvent<HTMLDivElement>
@ -4140,6 +4217,11 @@ export function AgentPsdTemplateEditorDialog({
if (isAgentPsdImageLayerSlot(layer)) { if (isAgentPsdImageLayerSlot(layer)) {
const assignedUrl = const assignedUrl =
effectiveImageAssignments[layer.id]; effectiveImageAssignments[layer.id];
const hasAssignedImage = Boolean(
String(assignedUrl || "").trim()
);
const canDragAssignedImage =
isLayerSuperLocked && hasAssignedImage;
const layerImageUrl = const layerImageUrl =
assignedUrl || assignedUrl ||
getPsdLayerPreviewSourceUrl(layer); getPsdLayerPreviewSourceUrl(layer);
@ -4156,39 +4238,51 @@ export function AgentPsdTemplateEditorDialog({
return ( return (
<div <div
className={`absolute overflow-hidden ${ className={`absolute overflow-hidden ${
isLayerSuperLocked isLayerSuperLocked && !hasAssignedImage
? "pointer-events-none" ? "pointer-events-none"
: "pointer-events-auto" : "pointer-events-auto"
}`} }`}
key={layer.id} key={layer.id}
onClick={ onClick={(event) => {
isLayerSuperLocked event.stopPropagation();
? undefined setSelectedLayerId(layer.id);
: (event) => { }}
event.stopPropagation(); onPointerDown={(event) => {
setSelectedLayerId(layer.id); if (canDragAssignedImage) {
} handleAssignedImagePlacementDragStart(
} layer,
onPointerDown={(event) => event
isLayerSuperLocked );
? undefined return;
: handleLayerDragStart(layer, event) }
} if (!isLayerSuperLocked) {
handleLayerDragStart(layer, event);
}
}}
style={{ style={{
...assignedImageContainerStyle, ...assignedImageContainerStyle,
zIndex: baseZIndex, zIndex: baseZIndex,
cursor: cursor: canDragAssignedImage
isLayerSuperLocked ? imagePlacementDragStateRef.current
?.layerId === layer.id
? "grabbing"
: "grab"
: isLayerSuperLocked
? "default" ? "default"
: layerDragStateRef.current?.layerId === : layerDragStateRef.current?.layerId ===
layer.id layer.id
? "grabbing" ? "grabbing"
: "grab", : "grab",
touchAction: isLayerSuperLocked touchAction:
? "auto" isLayerSuperLocked && !hasAssignedImage
: "none", ? "auto"
: "none",
}} }}
title={`${getLayerLabel(layer)}:${layer.layerName}`} title={
canDragAssignedImage
? `${getLayerLabel(layer)}:${layer.layerName}(目标已锁定,可拖动替换图)`
: `${getLayerLabel(layer)}:${layer.layerName}`
}
> >
<img <img
alt={layer.layerName} alt={layer.layerName}
@ -4429,6 +4523,8 @@ export function AgentPsdTemplateEditorDialog({
effectiveImageAssignments[layer.id] || "" effectiveImageAssignments[layer.id] || ""
).trim() ).trim()
); );
const isLayerSuperLocked =
isAgentPsdLayerSuperLocked(layer);
return isSelected && hasAssignedImage ? ( return isSelected && hasAssignedImage ? (
<div <div
@ -4456,9 +4552,18 @@ export function AgentPsdTemplateEditorDialog({
className="absolute overflow-hidden rounded-lg transition-all" className="absolute overflow-hidden rounded-lg transition-all"
key={layer.id} key={layer.id}
onClick={() => setSelectedLayerId(layer.id)} onClick={() => setSelectedLayerId(layer.id)}
onPointerDown={(event) => onPointerDown={(event) => {
handleLayerDragStart(layer, event) if (isLayerSuperLocked && hasAssignedImage) {
} handleAssignedImagePlacementDragStart(
layer,
event
);
return;
}
if (!isLayerSuperLocked) {
handleLayerDragStart(layer, event);
}
}}
style={{ style={{
...frameStyle, ...frameStyle,
zIndex: zIndex:
@ -4470,10 +4575,15 @@ export function AgentPsdTemplateEditorDialog({
? "rgba(249,115,22,0.08)" ? "rgba(249,115,22,0.08)"
: "transparent", : "transparent",
cursor: cursor:
layerDragStateRef.current?.layerId === isLayerSuperLocked && hasAssignedImage
layer.id ? imagePlacementDragStateRef.current
? "grabbing" ?.layerId === layer.id
: "grab", ? "grabbing"
: "grab"
: layerDragStateRef.current?.layerId ===
layer.id
? "grabbing"
: "grab",
touchAction: "none", touchAction: "none",
}} }}
title={`${getLayerLabel(layer)}:${layer.layerName}`} title={`${getLayerLabel(layer)}:${layer.layerName}`}

View File

@ -576,13 +576,13 @@ export async function renderAgentPsdTemplate(params: {
for (const layer of orderedTemplateLayers) { for (const layer of orderedTemplateLayers) {
if (hiddenLayerIdSet.has(layer.id)) { if (hiddenLayerIdSet.has(layer.id)) {
continue; continue;
} }
const storedLayerSourceUrl = getStoredLayerSourceUrl(layer); const storedLayerSourceUrl = getStoredLayerSourceUrl(layer);
if (shouldRenderAsStaticLayer(layer)) { if (shouldRenderAsStaticLayer(layer)) {
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) { if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
continue; continue;
} }
pushAsset( pushAsset(
storedLayerSourceUrl, storedLayerSourceUrl,
"PSD 图层素材", "PSD 图层素材",
@ -913,7 +913,10 @@ export async function renderAgentPsdTemplate(params: {
} }
for (const layer of [...orderedTemplateLayers].reverse()) { for (const layer of [...orderedTemplateLayers].reverse()) {
if (shouldUseRenderSegments && isAgentPsdTemplateLayerUserEditable(layer)) { if (
shouldUseRenderSegments &&
(isAgentPsdTemplateLayerUserEditable(layer) || hasImageAssignment(layer))
) {
await appendRenderSegmentBeforeLayer(layer.id); await appendRenderSegmentBeforeLayer(layer.id);
} }
@ -921,7 +924,7 @@ export async function renderAgentPsdTemplate(params: {
continue; continue;
} }
if (!isAgentPsdTemplateLayerUserEditable(layer)) { if (shouldRenderAsStaticLayer(layer)) {
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) { if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
continue; continue;
} }