fix: anchor PSD replacements to clip targets

This commit is contained in:
Your Name 2026-06-01 20:38:45 +08:00
parent c247093ec9
commit 87be5499d5
2 changed files with 87 additions and 28 deletions

View File

@ -1498,6 +1498,7 @@ function renderTextLayerPreviewContent(
function getAssignedImageMaskStyle( function getAssignedImageMaskStyle(
layer: AgentPsdTemplateImageLayerSlot, layer: AgentPsdTemplateImageLayerSlot,
frameLayer: AgentPsdTemplateLayerSlot,
layers: AgentPsdTemplateLayerSlot[] layers: AgentPsdTemplateLayerSlot[]
): CSSProperties { ): CSSProperties {
const clipMaskLayer = layer.clipMaskLayerId const clipMaskLayer = layer.clipMaskLayerId
@ -1512,7 +1513,7 @@ function getAssignedImageMaskStyle(
).trim(); ).trim();
const hasMask = Boolean(normalizedMaskUrl); const hasMask = Boolean(normalizedMaskUrl);
const maskImageUrl = hasMask const maskImageUrl = hasMask
? `url(${buildPsdLayerMaskUrl(normalizedMaskUrl, layer.width)})` ? `url(${buildPsdLayerMaskUrl(normalizedMaskUrl, frameLayer.width)})`
: undefined; : undefined;
return { return {
WebkitMaskImage: maskImageUrl, WebkitMaskImage: maskImageUrl,
@ -1529,18 +1530,22 @@ function getAssignedImageMaskStyle(
function getPlacedAssignedImageStyle(params: { function getPlacedAssignedImageStyle(params: {
layer: AgentPsdTemplateImageLayerSlot; layer: AgentPsdTemplateImageLayerSlot;
frameLayer?: AgentPsdTemplateLayerSlot | null;
imageSize: AgentPsdImageSize; imageSize: AgentPsdImageSize;
placement: AgentPsdImagePlacement; placement: AgentPsdImagePlacement;
}): CSSProperties { }): CSSProperties {
const { layer, imageSize, placement } = params; const { layer, imageSize, placement } = params;
const frameLayer = params.frameLayer || layer;
const normalizedPlacement = normalizeAgentPsdImagePlacement(placement); const normalizedPlacement = normalizeAgentPsdImagePlacement(placement);
const frameContentBounds =
isAgentPsdImageLayerSlot(frameLayer) ? frameLayer.contentBounds : undefined;
const baseFrame = resolveAgentPsdImageBaseFrame({ const baseFrame = resolveAgentPsdImageBaseFrame({
containerWidth: layer.width, containerWidth: frameLayer.width,
containerHeight: layer.height, containerHeight: frameLayer.height,
imageWidth: imageSize.width, imageWidth: imageSize.width,
imageHeight: imageSize.height, imageHeight: imageSize.height,
replaceMode: layer.replaceMode, replaceMode: layer.replaceMode,
contentBounds: layer.contentBounds, contentBounds: frameContentBounds,
}); });
return { 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( function hasEditedTextValue(
layer: AgentPsdTemplateTextLayerSlot, layer: AgentPsdTemplateTextLayerSlot,
value: string value: string
@ -3432,15 +3448,18 @@ export function AgentPsdTemplateEditorDialog({
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setSelectedLayerId(layer.id); setSelectedLayerId(layer.id);
const shouldDragAssignedImageContent =
isAgentPsdImageLayerSlot(layer) &&
Boolean(String(effectiveImageAssignments[layer.id] || "").trim());
layerDragStateRef.current = { layerDragStateRef.current = {
layerId: layer.id, layerId: layer.id,
pointerId: event.pointerId, pointerId: event.pointerId,
startClientX: event.clientX, startClientX: event.clientX,
startClientY: event.clientY, startClientY: event.clientY,
startOffset: normalizeLayerPositionOffset( startOffset: shouldDragAssignedImageContent
effectiveLayerPositionOffsets[layer.id] ? normalizeLayerPositionOffset(effectiveImagePlacements[layer.id])
), : normalizeLayerPositionOffset(effectiveLayerPositionOffsets[layer.id]),
}; };
const nextTarget = event.currentTarget; const nextTarget = event.currentTarget;
@ -3459,6 +3478,19 @@ export function AgentPsdTemplateEditorDialog({
return; 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, () => ({ updateLayerPositionOffset(layer.id, () => ({
offsetX: offsetX:
dragState.startOffset.offsetX + dragState.startOffset.offsetX +
@ -4189,11 +4221,32 @@ export function AgentPsdTemplateEditorDialog({
if (!layerImageUrl) { if (!layerImageUrl) {
return null; 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 const assignedImageContainerStyle = assignedUrl
? { ? {
...layerBoxStyle, ...assignedFrameBoxStyle,
...getAssignedImageMaskStyle( ...getAssignedImageMaskStyle(
layer, layer,
assignedFrameLayer,
selectedTemplateLayers selectedTemplateLayers
), ),
} }
@ -4263,6 +4316,7 @@ export function AgentPsdTemplateEditorDialog({
assignedUrl && imageSizes[layer.id] assignedUrl && imageSizes[layer.id]
? getPlacedAssignedImageStyle({ ? getPlacedAssignedImageStyle({
layer, layer,
frameLayer: assignedFrameLayer,
imageSize: imageSizes[layer.id], imageSize: imageSizes[layer.id],
placement: placement:
effectiveImagePlacements[ effectiveImagePlacements[

View File

@ -953,15 +953,24 @@ export async function renderAgentPsdTemplate(params: {
assignmentUrl, assignmentUrl,
"PSD 套版素材" "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 sourceImage = sharp(sourceBuffer, { failOn: "none" }).rotate();
const metadata = await sourceImage.clone().metadata(); const metadata = await sourceImage.clone().metadata();
const placementFrame = resolveAgentPsdImagePlacementFrame({ const placementFrame = resolveAgentPsdImagePlacementFrame({
containerWidth: layer.width, containerWidth: renderFrameLayer.width,
containerHeight: layer.height, containerHeight: renderFrameLayer.height,
imageWidth: Math.max(1, Number(metadata.width || 1)), imageWidth: Math.max(1, Number(metadata.width || 1)),
imageHeight: Math.max(1, Number(metadata.height || 1)), imageHeight: Math.max(1, Number(metadata.height || 1)),
replaceMode: layer.replaceMode, replaceMode: layer.replaceMode,
contentBounds: layer.contentBounds, contentBounds: renderFrameContentBounds,
placement: imagePlacements[layer.id], placement: imagePlacements[layer.id],
}); });
@ -992,19 +1001,15 @@ export async function renderAgentPsdTemplate(params: {
top: placementFrame.top, top: placementFrame.top,
width: placementFrame.width, width: placementFrame.width,
height: placementFrame.height, height: placementFrame.height,
canvasWidth: layer.width, canvasWidth: renderFrameLayer.width,
canvasHeight: layer.height, canvasHeight: renderFrameLayer.height,
}); });
if (!slotOverlay) { if (!slotOverlay) {
continue; continue;
} }
const layerPositionOffset = getLayerPositionOffset(layer.id); const layerPositionOffset = getLayerPositionOffset(renderFrameLayer.id);
const clipMaskLayer =
layer.forceClipMask && layer.clipMaskLayerId
? layerMap.get(layer.clipMaskLayerId)
: null;
const shouldApplyMask = Boolean(layer.maskUrl || clipMaskLayer); const shouldApplyMask = Boolean(layer.maskUrl || clipMaskLayer);
if (!shouldApplyMask) { if (!shouldApplyMask) {
if (isDefaultSourceImageLayer) { if (isDefaultSourceImageLayer) {
@ -1013,11 +1018,11 @@ export async function renderAgentPsdTemplate(params: {
const didAppend = await appendBufferOverlay({ const didAppend = await appendBufferOverlay({
input: slotOverlay.input, input: slotOverlay.input,
left: left:
layer.x + renderFrameLayer.x +
layerPositionOffset.offsetX + layerPositionOffset.offsetX +
slotOverlay.left, slotOverlay.left,
top: top:
layer.y + renderFrameLayer.y +
layerPositionOffset.offsetY + layerPositionOffset.offsetY +
slotOverlay.top, slotOverlay.top,
width: slotOverlay.width, width: slotOverlay.width,
@ -1029,8 +1034,8 @@ export async function renderAgentPsdTemplate(params: {
let overlayBuffer = await sharp({ let overlayBuffer = await sharp({
create: { create: {
width: Math.max(1, Math.round(layer.width)), width: Math.max(1, Math.round(renderFrameLayer.width)),
height: Math.max(1, Math.round(layer.height)), height: Math.max(1, Math.round(renderFrameLayer.height)),
channels: 4, channels: 4,
background: { background: {
r: 0, r: 0,
@ -1054,8 +1059,8 @@ export async function renderAgentPsdTemplate(params: {
} }
const maskBuffer = await sharp(rawMaskBuffer) const maskBuffer = await sharp(rawMaskBuffer)
.resize({ .resize({
width: Math.max(1, Math.round(layer.width)), width: Math.max(1, Math.round(renderFrameLayer.width)),
height: Math.max(1, Math.round(layer.height)), height: Math.max(1, Math.round(renderFrameLayer.height)),
fit: "fill", fit: "fill",
background: { background: {
r: 0, r: 0,
@ -1093,10 +1098,10 @@ export async function renderAgentPsdTemplate(params: {
const didAppend = await appendBufferOverlay({ const didAppend = await appendBufferOverlay({
input: overlayBuffer, input: overlayBuffer,
left: layer.x + layerPositionOffset.offsetX, left: renderFrameLayer.x + layerPositionOffset.offsetX,
top: layer.y + layerPositionOffset.offsetY, top: renderFrameLayer.y + layerPositionOffset.offsetY,
width: layer.width, width: renderFrameLayer.width,
height: layer.height, height: renderFrameLayer.height,
}); });
if (!didAppend) continue; if (!didAppend) continue;
continue; continue;