Compare commits
3 Commits
5d7e8ce773
...
db1fc8c3df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db1fc8c3df | ||
|
|
4d2093ef44 | ||
|
|
0baa13f7de |
@ -1780,7 +1780,7 @@ export function PsdTemplatesSettingsClient({
|
||||
超级锁
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
开启后锁定目标图层的位置、层级和删除操作;图片槽位仍可替换图片,并只能在蒙版内拖动替换图。
|
||||
开启后用户前台无法选中、编辑、拖动或临时删除该图层,鼠标点击会自动穿透到下方图层。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@ -1792,9 +1792,15 @@ export function PsdTemplatesSettingsClient({
|
||||
...current,
|
||||
isSuperLocked: checked,
|
||||
isRequired: checked ? false : current.isRequired,
|
||||
isDefaultResultSource: checked
|
||||
? false
|
||||
: current.isDefaultResultSource,
|
||||
allowUserUploadOverride: checked
|
||||
? true
|
||||
? false
|
||||
: current.allowUserUploadOverride,
|
||||
forceClipMask: checked
|
||||
? false
|
||||
: current.forceClipMask,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1831,6 +1837,9 @@ export function PsdTemplatesSettingsClient({
|
||||
allowUserUploadOverride: checked
|
||||
? current.allowUserUploadOverride
|
||||
: false,
|
||||
forceClipMask: checked
|
||||
? current.forceClipMask
|
||||
: false,
|
||||
}
|
||||
: {
|
||||
...current,
|
||||
@ -1946,6 +1955,31 @@ export function PsdTemplatesSettingsClient({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-sm font-medium">强制剪切蒙版</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
开启后替换图会按当前 PSD 图层原始透明区域剪切,适合需要贴合固定形状的图片槽位。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={layer.isEditable && layer.forceClipMask === true}
|
||||
disabled={!layer.isEditable || layer.isSuperLocked === true}
|
||||
onCheckedChange={(checked) =>
|
||||
updateDraftLayer(layer.id, (current) =>
|
||||
current.slotType === "image"
|
||||
? {
|
||||
...current,
|
||||
forceClipMask: current.isEditable
|
||||
? checked
|
||||
: false,
|
||||
}
|
||||
: current
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 rounded-xl border p-3">
|
||||
<div className="text-sm font-medium">填充模式</div>
|
||||
<Select
|
||||
|
||||
@ -872,7 +872,9 @@ function getDefaultSourceImageLayer(
|
||||
): AgentPsdTemplateImageLayerSlot | null {
|
||||
const editableImageLayers = layers.filter(
|
||||
(layer): layer is AgentPsdTemplateImageLayerSlot =>
|
||||
layer.isEditable && isAgentPsdImageLayerSlot(layer)
|
||||
layer.isEditable &&
|
||||
!isAgentPsdLayerSuperLocked(layer) &&
|
||||
isAgentPsdImageLayerSlot(layer)
|
||||
);
|
||||
|
||||
return (
|
||||
@ -910,6 +912,9 @@ function createInitialState(
|
||||
|
||||
for (const layer of template.layers) {
|
||||
layerOrderIds.push(layer.id);
|
||||
if (isAgentPsdLayerSuperLocked(layer)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
isAgentPsdImageLayerSlot(layer) &&
|
||||
layer.id === defaultSourceImageLayerId &&
|
||||
@ -918,9 +923,6 @@ function createInitialState(
|
||||
imageAssignments[layer.id] = sourceImageUrl;
|
||||
imagePlacements[layer.id] = getDefaultAgentPsdImagePlacement();
|
||||
}
|
||||
if (isAgentPsdLayerSuperLocked(layer)) {
|
||||
continue;
|
||||
}
|
||||
if (isAgentPsdTextLayerSlot(layer) && layer.defaultText) {
|
||||
textAssignments[layer.id] = layer.defaultText;
|
||||
}
|
||||
@ -1471,17 +1473,17 @@ function renderTextLayerPreviewContent(
|
||||
);
|
||||
}
|
||||
|
||||
function getAssignedImageMaskStyle(params: {
|
||||
layer: AgentPsdTemplateImageLayerSlot;
|
||||
maskSourceUrl?: string;
|
||||
}): CSSProperties {
|
||||
const normalizedMaskUrl = String(
|
||||
params.layer.maskUrl || params.maskSourceUrl || ""
|
||||
).trim();
|
||||
function getAssignedImageMaskStyle(
|
||||
layer: AgentPsdTemplateImageLayerSlot
|
||||
): CSSProperties {
|
||||
const fallbackMaskUrl = layer.forceClipMask
|
||||
? getPsdLayerPreviewSourceUrl(layer)
|
||||
: "";
|
||||
const normalizedMaskUrl = String(layer.maskUrl || fallbackMaskUrl || "").trim();
|
||||
const hasMask = Boolean(normalizedMaskUrl);
|
||||
const maskImageUrl = hasMask
|
||||
? `url(${buildPsdLayerPreviewUrl(normalizedMaskUrl, {
|
||||
width: params.layer.width,
|
||||
width: layer.width,
|
||||
quality: 72,
|
||||
})})`
|
||||
: undefined;
|
||||
@ -1741,13 +1743,6 @@ 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;
|
||||
@ -1954,7 +1949,9 @@ export function AgentPsdTemplateEditorDialog({
|
||||
() =>
|
||||
selectedTemplateLayers.filter(
|
||||
(layer): layer is AgentPsdTemplateImageLayerSlot =>
|
||||
layer.isEditable && isAgentPsdImageLayerSlot(layer)
|
||||
layer.isEditable &&
|
||||
!isAgentPsdLayerSuperLocked(layer) &&
|
||||
isAgentPsdImageLayerSlot(layer)
|
||||
),
|
||||
[selectedTemplateLayers]
|
||||
);
|
||||
@ -2049,11 +2046,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
);
|
||||
const orderedEditableImageLayers = useMemo(
|
||||
() =>
|
||||
orderedPreviewLayers.filter(
|
||||
orderedEditableLayers.filter(
|
||||
(layer): layer is AgentPsdTemplateImageLayerSlot =>
|
||||
layer.isEditable && isAgentPsdImageLayerSlot(layer)
|
||||
isAgentPsdImageLayerSlot(layer)
|
||||
),
|
||||
[orderedPreviewLayers]
|
||||
[orderedEditableLayers]
|
||||
);
|
||||
const orderedEditableTextLayers = useMemo(
|
||||
() =>
|
||||
@ -2204,24 +2201,17 @@ export function AgentPsdTemplateEditorDialog({
|
||||
const layer =
|
||||
selectedTemplateLayers.find((entry) => entry.id === selectedLayerId) ||
|
||||
null;
|
||||
return layer;
|
||||
return layer && !isAgentPsdLayerSuperLocked(layer) ? layer : null;
|
||||
}, [selectedLayerId, selectedTemplateLayers]);
|
||||
const isSelectedLayerSuperLocked = Boolean(
|
||||
selectedLayer && isAgentPsdLayerSuperLocked(selectedLayer)
|
||||
);
|
||||
const isSelectedLayerHidden = Boolean(
|
||||
selectedLayer && hiddenLayerIdSet.has(selectedLayer.id)
|
||||
);
|
||||
const selectedImageLayer =
|
||||
selectedLayer &&
|
||||
selectedLayer.isEditable &&
|
||||
isAgentPsdImageLayerSlot(selectedLayer)
|
||||
selectedLayer && isAgentPsdImageLayerSlot(selectedLayer)
|
||||
? selectedLayer
|
||||
: null;
|
||||
const selectedTextLayer =
|
||||
selectedLayer &&
|
||||
!isSelectedLayerSuperLocked &&
|
||||
isAgentPsdTextLayerSlot(selectedLayer)
|
||||
selectedLayer && isAgentPsdTextLayerSlot(selectedLayer)
|
||||
? selectedLayer
|
||||
: null;
|
||||
const selectedImageAssignmentUrl = selectedImageLayer
|
||||
@ -2273,9 +2263,8 @@ export function AgentPsdTemplateEditorDialog({
|
||||
current.filter((currentLayerId) => currentLayerId !== normalizedLayerId)
|
||||
);
|
||||
}
|
||||
const selectedLayerPositionPanel =
|
||||
selectedLayer && !isSelectedLayerSuperLocked ? (
|
||||
<div className="space-y-4 rounded-2xl border border-white/10 bg-white/[0.03] p-5 shadow-inner">
|
||||
const selectedLayerPositionPanel = selectedLayer ? (
|
||||
<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>
|
||||
<div className="font-semibold text-sm text-white">图层层级</div>
|
||||
@ -2499,7 +2488,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
槽位内图片位置
|
||||
</div>
|
||||
<div className="mt-1.5 text-white/50 text-xs">
|
||||
目标图层保持固定;拖动预览中的替换图可在蒙版内微调位置。
|
||||
图层整体拖动已经改到中间预览区;这里保留槽位内图片的细调。
|
||||
</div>
|
||||
</div>
|
||||
<ImageIcon className="h-5 w-5 shrink-0 text-orange-300" />
|
||||
@ -3467,72 +3456,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
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(
|
||||
layer: AgentPsdTemplateTextLayerSlot,
|
||||
event: ReactPointerEvent<HTMLDivElement>
|
||||
@ -4228,80 +4151,58 @@ 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);
|
||||
if (!layerImageUrl) {
|
||||
return null;
|
||||
}
|
||||
const layerPreviewSourceUrl =
|
||||
getPsdLayerPreviewSourceUrl(layer);
|
||||
const assignedImageContainerStyle = assignedUrl
|
||||
? {
|
||||
...layerBoxStyle,
|
||||
...getAssignedImageMaskStyle({
|
||||
layer,
|
||||
maskSourceUrl: layerPreviewSourceUrl,
|
||||
}),
|
||||
...getAssignedImageMaskStyle(layer),
|
||||
}
|
||||
: layerBoxStyle;
|
||||
const previewZIndex = hasAssignedImage
|
||||
const previewZIndex = assignedUrl
|
||||
? Math.max(baseZIndex, renderBaseZIndex + 1)
|
||||
: baseZIndex;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute overflow-hidden ${
|
||||
isLayerSuperLocked && !hasAssignedImage
|
||||
isLayerSuperLocked
|
||||
? "pointer-events-none"
|
||||
: "pointer-events-auto"
|
||||
}`}
|
||||
key={layer.id}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedLayerId(layer.id);
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
if (canDragAssignedImage) {
|
||||
handleAssignedImagePlacementDragStart(
|
||||
layer,
|
||||
event
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!isLayerSuperLocked) {
|
||||
handleLayerDragStart(layer, event);
|
||||
}
|
||||
}}
|
||||
onClick={
|
||||
isLayerSuperLocked
|
||||
? undefined
|
||||
: (event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedLayerId(layer.id);
|
||||
}
|
||||
}
|
||||
onPointerDown={(event) =>
|
||||
isLayerSuperLocked
|
||||
? undefined
|
||||
: handleLayerDragStart(layer, event)
|
||||
}
|
||||
style={{
|
||||
...assignedImageContainerStyle,
|
||||
zIndex: previewZIndex,
|
||||
cursor: canDragAssignedImage
|
||||
? imagePlacementDragStateRef.current
|
||||
?.layerId === layer.id
|
||||
? "grabbing"
|
||||
: "grab"
|
||||
: isLayerSuperLocked
|
||||
cursor:
|
||||
isLayerSuperLocked
|
||||
? "default"
|
||||
: layerDragStateRef.current?.layerId ===
|
||||
layer.id
|
||||
? "grabbing"
|
||||
: "grab",
|
||||
touchAction:
|
||||
isLayerSuperLocked && !hasAssignedImage
|
||||
? "auto"
|
||||
: "none",
|
||||
touchAction: isLayerSuperLocked
|
||||
? "auto"
|
||||
: "none",
|
||||
}}
|
||||
title={
|
||||
canDragAssignedImage
|
||||
? `${getLayerLabel(layer)}:${layer.layerName}(目标已锁定,可拖动替换图)`
|
||||
: `${getLayerLabel(layer)}:${layer.layerName}`
|
||||
}
|
||||
title={`${getLayerLabel(layer)}:${layer.layerName}`}
|
||||
>
|
||||
<img
|
||||
alt={layer.layerName}
|
||||
@ -4542,11 +4443,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
effectiveImageAssignments[layer.id] || ""
|
||||
).trim()
|
||||
);
|
||||
const effectiveBaseZIndex = hasAssignedImage
|
||||
? Math.max(baseZIndex, renderBaseZIndex + 1)
|
||||
: baseZIndex;
|
||||
const isLayerSuperLocked =
|
||||
isAgentPsdLayerSuperLocked(layer);
|
||||
|
||||
return isSelected && hasAssignedImage ? (
|
||||
<div
|
||||
@ -4556,7 +4452,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
style={{
|
||||
...frameStyle,
|
||||
zIndex:
|
||||
effectiveBaseZIndex +
|
||||
baseZIndex +
|
||||
SELECTED_LAYER_INTERACTION_Z_BOOST,
|
||||
background: "rgba(249,115,22,0.08)",
|
||||
cursor:
|
||||
@ -4574,22 +4470,13 @@ export function AgentPsdTemplateEditorDialog({
|
||||
className="absolute overflow-hidden rounded-lg transition-all"
|
||||
key={layer.id}
|
||||
onClick={() => setSelectedLayerId(layer.id)}
|
||||
onPointerDown={(event) => {
|
||||
if (isLayerSuperLocked && hasAssignedImage) {
|
||||
handleAssignedImagePlacementDragStart(
|
||||
layer,
|
||||
event
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!isLayerSuperLocked) {
|
||||
handleLayerDragStart(layer, event);
|
||||
}
|
||||
}}
|
||||
onPointerDown={(event) =>
|
||||
handleLayerDragStart(layer, event)
|
||||
}
|
||||
style={{
|
||||
...frameStyle,
|
||||
zIndex:
|
||||
effectiveBaseZIndex +
|
||||
baseZIndex +
|
||||
(isSelected
|
||||
? SELECTED_LAYER_INTERACTION_Z_BOOST
|
||||
: 0),
|
||||
@ -4597,15 +4484,10 @@ export function AgentPsdTemplateEditorDialog({
|
||||
? "rgba(249,115,22,0.08)"
|
||||
: "transparent",
|
||||
cursor:
|
||||
isLayerSuperLocked && hasAssignedImage
|
||||
? imagePlacementDragStateRef.current
|
||||
?.layerId === layer.id
|
||||
? "grabbing"
|
||||
: "grab"
|
||||
: layerDragStateRef.current?.layerId ===
|
||||
layer.id
|
||||
? "grabbing"
|
||||
: "grab",
|
||||
layerDragStateRef.current?.layerId ===
|
||||
layer.id
|
||||
? "grabbing"
|
||||
: "grab",
|
||||
touchAction: "none",
|
||||
}}
|
||||
title={`${getLayerLabel(layer)}:${layer.layerName}`}
|
||||
@ -4626,9 +4508,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
);
|
||||
const baseZIndex =
|
||||
layerStackZIndexById.get(layer.id) || 1;
|
||||
const effectiveBaseZIndex = hasAssignedImage
|
||||
? Math.max(baseZIndex, renderBaseZIndex + 1)
|
||||
: baseZIndex;
|
||||
if (!isSelected || !hasAssignedImage) {
|
||||
return null;
|
||||
}
|
||||
@ -4656,7 +4535,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
44
|
||||
),
|
||||
zIndex:
|
||||
effectiveBaseZIndex +
|
||||
baseZIndex +
|
||||
SELECTED_LAYER_CONTROLS_Z_BOOST,
|
||||
}}
|
||||
>
|
||||
|
||||
@ -49,6 +49,7 @@ export interface AgentPsdTemplateImageLayerSlot
|
||||
slotType: "image";
|
||||
isDefaultResultSource: boolean;
|
||||
allowUserUploadOverride: boolean;
|
||||
forceClipMask?: boolean;
|
||||
replaceMode: AgentPsdTemplateReplaceMode;
|
||||
contentBounds?: AgentPsdTemplateImageContentBounds;
|
||||
maskUrl?: string;
|
||||
@ -415,6 +416,7 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
|
||||
isEditable && !isSuperLocked && record.isDefaultResultSource === true,
|
||||
allowUserUploadOverride:
|
||||
isEditable && !isSuperLocked ? record.allowUserUploadOverride !== false : false,
|
||||
forceClipMask: isEditable && !isSuperLocked && record.forceClipMask === true,
|
||||
replaceMode: normalizeAgentPsdTemplateReplaceMode(record.replaceMode),
|
||||
contentBounds: normalizeAgentPsdTemplateImageContentBounds(
|
||||
record.contentBounds,
|
||||
|
||||
@ -913,10 +913,7 @@ export async function renderAgentPsdTemplate(params: {
|
||||
}
|
||||
|
||||
for (const layer of [...orderedTemplateLayers].reverse()) {
|
||||
if (
|
||||
shouldUseRenderSegments &&
|
||||
(isAgentPsdTemplateLayerUserEditable(layer) || hasImageAssignment(layer))
|
||||
) {
|
||||
if (shouldUseRenderSegments && isAgentPsdTemplateLayerUserEditable(layer)) {
|
||||
await appendRenderSegmentBeforeLayer(layer.id);
|
||||
}
|
||||
|
||||
@ -924,7 +921,7 @@ export async function renderAgentPsdTemplate(params: {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldRenderAsStaticLayer(layer)) {
|
||||
if (!isAgentPsdTemplateLayerUserEditable(layer)) {
|
||||
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
|
||||
continue;
|
||||
}
|
||||
@ -1035,8 +1032,7 @@ export async function renderAgentPsdTemplate(params: {
|
||||
.png(PSD_RENDER_PNG_OPTIONS)
|
||||
.toBuffer();
|
||||
|
||||
const shouldUseLayerSourceAsMask =
|
||||
!layer.maskUrl && isAgentPsdLayerSuperLocked(layer);
|
||||
const shouldUseLayerSourceAsMask = !layer.maskUrl && layer.forceClipMask;
|
||||
if (layer.maskUrl || shouldUseLayerSourceAsMask) {
|
||||
try {
|
||||
const rawMaskBuffer = layer.maskUrl
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user