fix: keep PSD clipped replacements in target layer order
This commit is contained in:
parent
87be5499d5
commit
a78fd8a0d4
@ -4210,11 +4210,6 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
if (isAgentPsdImageLayerSlot(layer)) {
|
if (isAgentPsdImageLayerSlot(layer)) {
|
||||||
const assignedUrl =
|
const assignedUrl =
|
||||||
effectiveImageAssignments[layer.id];
|
effectiveImageAssignments[layer.id];
|
||||||
const isDefaultAssignedImageLayer =
|
|
||||||
layer.id ===
|
|
||||||
getDefaultSourceImageLayer(
|
|
||||||
selectedTemplateLayers
|
|
||||||
)?.id;
|
|
||||||
const layerImageUrl =
|
const layerImageUrl =
|
||||||
assignedUrl ||
|
assignedUrl ||
|
||||||
getPsdLayerPreviewSourceUrl(layer);
|
getPsdLayerPreviewSourceUrl(layer);
|
||||||
@ -4251,15 +4246,14 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
: layerBoxStyle;
|
: layerBoxStyle;
|
||||||
|
const assignedFrameBaseZIndex =
|
||||||
|
layerStackZIndexById.get(
|
||||||
|
assignedFrameLayer.id
|
||||||
|
) || baseZIndex;
|
||||||
const previewZIndex = assignedUrl
|
const previewZIndex = assignedUrl
|
||||||
? Math.max(
|
? assignedFrameLayer.id === layer.id
|
||||||
baseZIndex,
|
? Math.max(baseZIndex, renderBaseZIndex + 1)
|
||||||
renderBaseZIndex + 1,
|
: assignedFrameBaseZIndex + 1
|
||||||
isDefaultAssignedImageLayer
|
|
||||||
? (orderedPreviewLayers.length + 1) *
|
|
||||||
PSD_LAYER_Z_INDEX_STEP
|
|
||||||
: 0
|
|
||||||
)
|
|
||||||
: baseZIndex;
|
: baseZIndex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -912,6 +912,195 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAssignedImageRenderFrameLayer(layer: AgentPsdTemplateImageLayerSlot) {
|
||||||
|
if (!layer.forceClipMask || !layer.clipMaskLayerId) {
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return layerMap.get(layer.clipMaskLayerId) || layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignedImageLayersByFrameId = new Map<string, AgentPsdTemplateImageLayerSlot[]>();
|
||||||
|
for (const layer of orderedTemplateLayers) {
|
||||||
|
if (
|
||||||
|
hiddenLayerIdSet.has(layer.id) ||
|
||||||
|
!isAgentPsdImageLayerSlot(layer) ||
|
||||||
|
!String(imageAssignments[layer.id] || "").trim()
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameLayer = getAssignedImageRenderFrameLayer(layer);
|
||||||
|
const currentLayers = assignedImageLayersByFrameId.get(frameLayer.id) || [];
|
||||||
|
currentLayers.push(layer);
|
||||||
|
assignedImageLayersByFrameId.set(frameLayer.id, currentLayers);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendedImageAssignmentLayerIds = new Set<string>();
|
||||||
|
async function appendAssignedImageLayer(layer: AgentPsdTemplateImageLayerSlot) {
|
||||||
|
if (appendedImageAssignmentLayerIds.has(layer.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDefaultSourceImageLayer = layer.id === defaultSourceImageLayerId;
|
||||||
|
const assignmentUrl = String(imageAssignments[layer.id] || "").trim();
|
||||||
|
if (!assignmentUrl || hiddenLayerIdSet.has(layer.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendedImageAssignmentLayerIds.add(layer.id);
|
||||||
|
const sourceBuffer = await readCachedBuffer(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: 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: renderFrameContentBounds,
|
||||||
|
placement: imagePlacements[layer.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const resizedOverlayBuffer = await sourceImage
|
||||||
|
.clone()
|
||||||
|
.resize({
|
||||||
|
width: Math.max(1, Math.round(placementFrame.width)),
|
||||||
|
height: Math.max(1, Math.round(placementFrame.height)),
|
||||||
|
fit:
|
||||||
|
layer.replaceMode === "contain"
|
||||||
|
? "contain"
|
||||||
|
: layer.replaceMode === "stretch"
|
||||||
|
? "fill"
|
||||||
|
: "cover",
|
||||||
|
background: {
|
||||||
|
r: 0,
|
||||||
|
g: 0,
|
||||||
|
b: 0,
|
||||||
|
alpha: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.png(PSD_RENDER_PNG_OPTIONS)
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const slotOverlay = await fitOverlayBufferIntoCanvas({
|
||||||
|
input: resizedOverlayBuffer,
|
||||||
|
left: placementFrame.left,
|
||||||
|
top: placementFrame.top,
|
||||||
|
width: placementFrame.width,
|
||||||
|
height: placementFrame.height,
|
||||||
|
canvasWidth: renderFrameLayer.width,
|
||||||
|
canvasHeight: renderFrameLayer.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!slotOverlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerPositionOffset = getLayerPositionOffset(renderFrameLayer.id);
|
||||||
|
const shouldApplyMask = Boolean(layer.maskUrl || clipMaskLayer);
|
||||||
|
if (!shouldApplyMask) {
|
||||||
|
if (isDefaultSourceImageLayer) {
|
||||||
|
await appendRenderBaseOverlay();
|
||||||
|
}
|
||||||
|
await appendBufferOverlay({
|
||||||
|
input: slotOverlay.input,
|
||||||
|
left: renderFrameLayer.x + layerPositionOffset.offsetX + slotOverlay.left,
|
||||||
|
top: renderFrameLayer.y + layerPositionOffset.offsetY + slotOverlay.top,
|
||||||
|
width: slotOverlay.width,
|
||||||
|
height: slotOverlay.height,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let overlayBuffer = await sharp({
|
||||||
|
create: {
|
||||||
|
width: Math.max(1, Math.round(renderFrameLayer.width)),
|
||||||
|
height: Math.max(1, Math.round(renderFrameLayer.height)),
|
||||||
|
channels: 4,
|
||||||
|
background: {
|
||||||
|
r: 0,
|
||||||
|
g: 0,
|
||||||
|
b: 0,
|
||||||
|
alpha: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite([slotOverlay])
|
||||||
|
.png(PSD_RENDER_PNG_OPTIONS)
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawMaskBuffer = layer.maskUrl
|
||||||
|
? await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
|
||||||
|
: await readReusableLayerBuffer(clipMaskLayer!);
|
||||||
|
if (!rawMaskBuffer) {
|
||||||
|
throw new Error("Missing PSD layer mask source");
|
||||||
|
}
|
||||||
|
const maskBuffer = await sharp(rawMaskBuffer)
|
||||||
|
.resize({
|
||||||
|
width: Math.max(1, Math.round(renderFrameLayer.width)),
|
||||||
|
height: Math.max(1, Math.round(renderFrameLayer.height)),
|
||||||
|
fit: "fill",
|
||||||
|
background: {
|
||||||
|
r: 0,
|
||||||
|
g: 0,
|
||||||
|
b: 0,
|
||||||
|
alpha: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.png(PSD_RENDER_PNG_OPTIONS)
|
||||||
|
.toBuffer();
|
||||||
|
overlayBuffer = await sharp(overlayBuffer)
|
||||||
|
.composite([
|
||||||
|
{
|
||||||
|
input: maskBuffer,
|
||||||
|
blend: "dest-in",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.png(PSD_RENDER_PNG_OPTIONS)
|
||||||
|
.toBuffer();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"[PSD Template] failed to apply layer mask during render:",
|
||||||
|
{
|
||||||
|
templateId: template.id,
|
||||||
|
layerId: layer.layerId,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefaultSourceImageLayer) {
|
||||||
|
await appendRenderBaseOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
await appendBufferOverlay({
|
||||||
|
input: overlayBuffer,
|
||||||
|
left: renderFrameLayer.x + layerPositionOffset.offsetX,
|
||||||
|
top: renderFrameLayer.y + layerPositionOffset.offsetY,
|
||||||
|
width: renderFrameLayer.width,
|
||||||
|
height: renderFrameLayer.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appendAssignedImagesForFrameLayer(frameLayerId: string) {
|
||||||
|
for (const assignedLayer of assignedImageLayersByFrameId.get(frameLayerId) || []) {
|
||||||
|
if (assignedLayer.id !== frameLayerId) {
|
||||||
|
await appendAssignedImageLayer(assignedLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldUseRenderBaseFallback &&
|
shouldUseRenderBaseFallback &&
|
||||||
(!defaultSourceImageLayerId || hiddenLayerIdSet.has(defaultSourceImageLayerId))
|
(!defaultSourceImageLayerId || hiddenLayerIdSet.has(defaultSourceImageLayerId))
|
||||||
@ -930,9 +1119,11 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
|
|
||||||
if (!isAgentPsdTemplateLayerUserEditable(layer)) {
|
if (!isAgentPsdTemplateLayerUserEditable(layer)) {
|
||||||
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
|
if (shouldUseRenderBaseFallback || shouldUseRenderSegments) {
|
||||||
|
await appendAssignedImagesForFrameLayer(layer.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await appendStoredLayer(layer, { optional: shouldUseRenderBaseFallback });
|
await appendStoredLayer(layer, { optional: shouldUseRenderBaseFallback });
|
||||||
|
await appendAssignedImagesForFrameLayer(layer.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -946,164 +1137,15 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
if (isDefaultSourceImageLayer) {
|
if (isDefaultSourceImageLayer) {
|
||||||
await appendRenderBaseOverlay();
|
await appendRenderBaseOverlay();
|
||||||
}
|
}
|
||||||
|
await appendAssignedImagesForFrameLayer(layer.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceBuffer = await readCachedBuffer(
|
const renderFrameLayer = getAssignedImageRenderFrameLayer(layer);
|
||||||
assignmentUrl,
|
if (renderFrameLayer.id === layer.id) {
|
||||||
"PSD 套版素材"
|
await appendAssignedImageLayer(layer);
|
||||||
);
|
|
||||||
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: 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: renderFrameContentBounds,
|
|
||||||
placement: imagePlacements[layer.id],
|
|
||||||
});
|
|
||||||
|
|
||||||
const resizedOverlayBuffer = await sourceImage
|
|
||||||
.clone()
|
|
||||||
.resize({
|
|
||||||
width: Math.max(1, Math.round(placementFrame.width)),
|
|
||||||
height: Math.max(1, Math.round(placementFrame.height)),
|
|
||||||
fit:
|
|
||||||
layer.replaceMode === "contain"
|
|
||||||
? "contain"
|
|
||||||
: layer.replaceMode === "stretch"
|
|
||||||
? "fill"
|
|
||||||
: "cover",
|
|
||||||
background: {
|
|
||||||
r: 0,
|
|
||||||
g: 0,
|
|
||||||
b: 0,
|
|
||||||
alpha: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.png(PSD_RENDER_PNG_OPTIONS)
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const slotOverlay = await fitOverlayBufferIntoCanvas({
|
|
||||||
input: resizedOverlayBuffer,
|
|
||||||
left: placementFrame.left,
|
|
||||||
top: placementFrame.top,
|
|
||||||
width: placementFrame.width,
|
|
||||||
height: placementFrame.height,
|
|
||||||
canvasWidth: renderFrameLayer.width,
|
|
||||||
canvasHeight: renderFrameLayer.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!slotOverlay) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
await appendAssignedImagesForFrameLayer(layer.id);
|
||||||
const layerPositionOffset = getLayerPositionOffset(renderFrameLayer.id);
|
|
||||||
const shouldApplyMask = Boolean(layer.maskUrl || clipMaskLayer);
|
|
||||||
if (!shouldApplyMask) {
|
|
||||||
if (isDefaultSourceImageLayer) {
|
|
||||||
await appendRenderBaseOverlay();
|
|
||||||
}
|
|
||||||
const didAppend = await appendBufferOverlay({
|
|
||||||
input: slotOverlay.input,
|
|
||||||
left:
|
|
||||||
renderFrameLayer.x +
|
|
||||||
layerPositionOffset.offsetX +
|
|
||||||
slotOverlay.left,
|
|
||||||
top:
|
|
||||||
renderFrameLayer.y +
|
|
||||||
layerPositionOffset.offsetY +
|
|
||||||
slotOverlay.top,
|
|
||||||
width: slotOverlay.width,
|
|
||||||
height: slotOverlay.height,
|
|
||||||
});
|
|
||||||
if (!didAppend) continue;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let overlayBuffer = await sharp({
|
|
||||||
create: {
|
|
||||||
width: Math.max(1, Math.round(renderFrameLayer.width)),
|
|
||||||
height: Math.max(1, Math.round(renderFrameLayer.height)),
|
|
||||||
channels: 4,
|
|
||||||
background: {
|
|
||||||
r: 0,
|
|
||||||
g: 0,
|
|
||||||
b: 0,
|
|
||||||
alpha: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.composite([slotOverlay])
|
|
||||||
.png(PSD_RENDER_PNG_OPTIONS)
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
if (shouldApplyMask) {
|
|
||||||
try {
|
|
||||||
const rawMaskBuffer = layer.maskUrl
|
|
||||||
? await readCachedBuffer(layer.maskUrl, "PSD 蒙版资源")
|
|
||||||
: await readReusableLayerBuffer(clipMaskLayer!);
|
|
||||||
if (!rawMaskBuffer) {
|
|
||||||
throw new Error("Missing PSD layer mask source");
|
|
||||||
}
|
|
||||||
const maskBuffer = await sharp(rawMaskBuffer)
|
|
||||||
.resize({
|
|
||||||
width: Math.max(1, Math.round(renderFrameLayer.width)),
|
|
||||||
height: Math.max(1, Math.round(renderFrameLayer.height)),
|
|
||||||
fit: "fill",
|
|
||||||
background: {
|
|
||||||
r: 0,
|
|
||||||
g: 0,
|
|
||||||
b: 0,
|
|
||||||
alpha: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.png(PSD_RENDER_PNG_OPTIONS)
|
|
||||||
.toBuffer();
|
|
||||||
overlayBuffer = await sharp(overlayBuffer)
|
|
||||||
.composite([
|
|
||||||
{
|
|
||||||
input: maskBuffer,
|
|
||||||
blend: "dest-in",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.png(PSD_RENDER_PNG_OPTIONS)
|
|
||||||
.toBuffer();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
"[PSD Template] failed to apply layer mask during render:",
|
|
||||||
{
|
|
||||||
templateId: template.id,
|
|
||||||
layerId: layer.layerId,
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDefaultSourceImageLayer) {
|
|
||||||
await appendRenderBaseOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
const didAppend = await appendBufferOverlay({
|
|
||||||
input: overlayBuffer,
|
|
||||||
left: renderFrameLayer.x + layerPositionOffset.offsetX,
|
|
||||||
top: renderFrameLayer.y + layerPositionOffset.offsetY,
|
|
||||||
width: renderFrameLayer.width,
|
|
||||||
height: renderFrameLayer.height,
|
|
||||||
});
|
|
||||||
if (!didAppend) continue;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1127,6 +1169,7 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
optional: shouldUseRenderBaseFallback,
|
optional: shouldUseRenderBaseFallback,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await appendAssignedImagesForFrameLayer(layer.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const textAlign = resolveAgentPsdTextLayerEffectiveAlign(layer);
|
const textAlign = resolveAgentPsdTextLayerEffectiveAlign(layer);
|
||||||
@ -1236,6 +1279,7 @@ export async function renderAgentPsdTemplate(params: {
|
|||||||
height: overlayFrame.height,
|
height: overlayFrame.height,
|
||||||
});
|
});
|
||||||
if (!didAppend) continue;
|
if (!didAppend) continue;
|
||||||
|
await appendAssignedImagesForFrameLayer(layer.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user