fix: correct psd masked image placement
This commit is contained in:
parent
305dd21dd5
commit
8c0ce434dc
@ -350,6 +350,7 @@ function getPlacedAssignedImageStyle(params: {
|
||||
imageWidth: imageSize.width,
|
||||
imageHeight: imageSize.height,
|
||||
replaceMode: layer.replaceMode,
|
||||
contentBounds: layer.contentBounds,
|
||||
placement,
|
||||
});
|
||||
|
||||
@ -1559,7 +1560,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<div className="grid min-h-0 flex-1 gap-4 xl:grid-rows-[minmax(0,1fr)_minmax(0,0.78fr)]">
|
||||
<div className="flex min-h-0 flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">
|
||||
{selectedImageLayer ? (
|
||||
<div className="space-y-3">
|
||||
<div className="min-h-0 space-y-3 overflow-y-auto pr-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="default">可替换图片</Badge>
|
||||
<div className="text-sm font-medium">{selectedImageLayer.layerName}</div>
|
||||
@ -1727,7 +1728,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
) : selectedTextLayer ? (
|
||||
<div className="space-y-3">
|
||||
<div className="min-h-0 space-y-3 overflow-y-auto pr-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="default">可编辑文字</Badge>
|
||||
<div className="text-sm font-medium">{selectedTextLayer.layerName}</div>
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import type { AgentPsdTemplateReplaceMode } from "@/lib/agents/psd-template-config";
|
||||
import type {
|
||||
AgentPsdTemplateImageContentBounds,
|
||||
AgentPsdTemplateReplaceMode,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
|
||||
export interface AgentPsdImagePlacement {
|
||||
scale: number;
|
||||
@ -69,19 +72,59 @@ function resolveBaseFittedSize(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeContainerBounds(params: {
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
contentBounds?: AgentPsdTemplateImageContentBounds;
|
||||
}) {
|
||||
const { containerWidth, containerHeight, contentBounds } = params;
|
||||
if (!contentBounds) {
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
};
|
||||
}
|
||||
|
||||
const left = Math.max(0, Math.min(containerWidth - 1, Math.round(contentBounds.left)));
|
||||
const top = Math.max(0, Math.min(containerHeight - 1, Math.round(contentBounds.top)));
|
||||
const width = Math.max(
|
||||
1,
|
||||
Math.min(Math.round(contentBounds.width), Math.max(1, containerWidth - left))
|
||||
);
|
||||
const height = Math.max(
|
||||
1,
|
||||
Math.min(Math.round(contentBounds.height), Math.max(1, containerHeight - top))
|
||||
);
|
||||
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentPsdImagePlacementFrame(params: {
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
replaceMode: AgentPsdTemplateReplaceMode;
|
||||
contentBounds?: AgentPsdTemplateImageContentBounds;
|
||||
placement?: Partial<AgentPsdImagePlacement> | null;
|
||||
}) {
|
||||
const { containerWidth, containerHeight, imageWidth, imageHeight, replaceMode } = params;
|
||||
const placement = normalizeAgentPsdImagePlacement(params.placement);
|
||||
const baseFrame = resolveBaseFittedSize({
|
||||
const contentBounds = normalizeContainerBounds({
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
contentBounds: params.contentBounds,
|
||||
});
|
||||
const baseFrame = resolveBaseFittedSize({
|
||||
containerWidth: contentBounds.width,
|
||||
containerHeight: contentBounds.height,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
replaceMode,
|
||||
@ -89,8 +132,16 @@ export function resolveAgentPsdImagePlacementFrame(params: {
|
||||
|
||||
const width = baseFrame.width * placement.scale;
|
||||
const height = baseFrame.height * placement.scale;
|
||||
const left = baseFrame.left - (width - baseFrame.width) / 2 + placement.offsetX;
|
||||
const top = baseFrame.top - (height - baseFrame.height) / 2 + placement.offsetY;
|
||||
const left =
|
||||
contentBounds.left +
|
||||
baseFrame.left -
|
||||
(width - baseFrame.width) / 2 +
|
||||
placement.offsetX;
|
||||
const top =
|
||||
contentBounds.top +
|
||||
baseFrame.top -
|
||||
(height - baseFrame.height) / 2 +
|
||||
placement.offsetY;
|
||||
|
||||
return {
|
||||
width,
|
||||
|
||||
@ -2,7 +2,7 @@ import type { AgentPsdTextColorConfig } from "@/types/agent";
|
||||
import type { AgentPsdTemplateSecondaryCategoryConfig } from "@/lib/agents/psd-template-secondary-category-config";
|
||||
|
||||
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
|
||||
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 3;
|
||||
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 4;
|
||||
|
||||
export type AgentPsdTemplateStatus = "active" | "disabled";
|
||||
export type AgentPsdTemplateParseStatus =
|
||||
@ -15,6 +15,13 @@ export type AgentPsdTemplateSlotType = "image" | "text";
|
||||
export type AgentPsdTextAlign = "left" | "center" | "right";
|
||||
export type AgentPsdTextStyleSource = "auto" | "custom";
|
||||
|
||||
export interface AgentPsdTemplateImageContentBounds {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface AgentPsdTemplateBaseLayerSlot {
|
||||
id: string;
|
||||
layerId: string;
|
||||
@ -39,6 +46,7 @@ export interface AgentPsdTemplateImageLayerSlot
|
||||
isDefaultResultSource: boolean;
|
||||
allowUserUploadOverride: boolean;
|
||||
replaceMode: AgentPsdTemplateReplaceMode;
|
||||
contentBounds?: AgentPsdTemplateImageContentBounds;
|
||||
maskUrl?: string;
|
||||
maskPath?: string;
|
||||
}
|
||||
@ -204,6 +212,49 @@ export function normalizeAgentPsdTemplateReplaceMode(
|
||||
return "cover";
|
||||
}
|
||||
|
||||
function normalizeAgentPsdTemplateImageContentBounds(
|
||||
value: unknown,
|
||||
containerWidth: number,
|
||||
containerHeight: number
|
||||
): AgentPsdTemplateImageContentBounds | undefined {
|
||||
if (!value || typeof value !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const left = normalizePositiveInteger(record.left);
|
||||
const top = normalizePositiveInteger(record.top);
|
||||
const width = normalizePositiveInteger(record.width);
|
||||
const height = normalizePositiveInteger(record.height);
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const clampedLeft = Math.min(left, Math.max(0, containerWidth - 1));
|
||||
const clampedTop = Math.min(top, Math.max(0, containerHeight - 1));
|
||||
const maxWidth = Math.max(1, containerWidth - clampedLeft);
|
||||
const maxHeight = Math.max(1, containerHeight - clampedTop);
|
||||
const clampedWidth = Math.min(width, maxWidth);
|
||||
const clampedHeight = Math.min(height, maxHeight);
|
||||
|
||||
if (
|
||||
clampedLeft === 0 &&
|
||||
clampedTop === 0 &&
|
||||
clampedWidth === containerWidth &&
|
||||
clampedHeight === containerHeight
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
left: clampedLeft,
|
||||
top: clampedTop,
|
||||
width: clampedWidth,
|
||||
height: clampedHeight,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeAgentPsdTemplateSlotType(
|
||||
value: unknown
|
||||
): AgentPsdTemplateSlotType {
|
||||
@ -311,6 +362,11 @@ export function parseAgentPsdTemplateLayers(value: unknown) {
|
||||
isDefaultResultSource: isEditable && record.isDefaultResultSource === true,
|
||||
allowUserUploadOverride: isEditable ? record.allowUserUploadOverride !== false : false,
|
||||
replaceMode: normalizeAgentPsdTemplateReplaceMode(record.replaceMode),
|
||||
contentBounds: normalizeAgentPsdTemplateImageContentBounds(
|
||||
record.contentBounds,
|
||||
width,
|
||||
height
|
||||
),
|
||||
maskUrl: normalizeOptionalUrl(record.maskUrl),
|
||||
maskPath: normalizeText(record.maskPath) || undefined,
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ import { analyzeTextLayerPixels, normalizeHexColor } from "@/lib/agents/psd-text
|
||||
import {
|
||||
isAgentPsdImageLayerSlot,
|
||||
isAgentPsdTextLayerSlot,
|
||||
type AgentPsdTemplateImageContentBounds,
|
||||
type AgentPsdTemplateLayerSlot,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
|
||||
@ -104,6 +105,44 @@ export interface PsdTemplateSourceLayerEntry {
|
||||
composite: () => Promise<Uint8ClampedArray>;
|
||||
}
|
||||
|
||||
function resolveVisibleContentBounds(params: {
|
||||
rgba: Uint8ClampedArray;
|
||||
width: number;
|
||||
height: number;
|
||||
}): AgentPsdTemplateImageContentBounds | undefined {
|
||||
const { rgba, width, height } = params;
|
||||
const pixelCount = width * height;
|
||||
let minX = width;
|
||||
let minY = height;
|
||||
let maxX = -1;
|
||||
let maxY = -1;
|
||||
|
||||
for (let index = 0; index < pixelCount; index += 1) {
|
||||
const alpha = rgba[index * 4 + 3] || 0;
|
||||
if (alpha <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const x = index % width;
|
||||
const y = Math.floor(index / width);
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
|
||||
if (maxX < minX || maxY < minY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
left: minX,
|
||||
top: minY,
|
||||
width: maxX - minX + 1,
|
||||
height: maxY - minY + 1,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLayerName(value: unknown, fallback: string) {
|
||||
const text = String(value || "").trim();
|
||||
return text || fallback;
|
||||
@ -522,6 +561,11 @@ export async function preparePsdTemplateRenderAssets(params: {
|
||||
|
||||
try {
|
||||
const rgba = await entry.node.composite();
|
||||
const visibleContentBounds = resolveVisibleContentBounds({
|
||||
rgba,
|
||||
width: entry.layer.width,
|
||||
height: entry.layer.height,
|
||||
});
|
||||
|
||||
if (params.forceRefreshLayerAssets || !targetLayer.previewUrl) {
|
||||
const thumbnailBuffer = await createLayerThumbnailBuffer({
|
||||
@ -623,6 +667,13 @@ export async function preparePsdTemplateRenderAssets(params: {
|
||||
!targetLayer.isEditable ||
|
||||
(!params.forceRefreshLayerAssets && targetLayer.maskUrl)
|
||||
) {
|
||||
const layerIndex = nextLayers.findIndex((layer) => layer.id === targetLayer.id);
|
||||
if (layerIndex >= 0 && isAgentPsdImageLayerSlot(nextLayers[layerIndex])) {
|
||||
nextLayers[layerIndex] = {
|
||||
...nextLayers[layerIndex],
|
||||
contentBounds: visibleContentBounds,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -658,6 +709,7 @@ export async function preparePsdTemplateRenderAssets(params: {
|
||||
if (layerIndex >= 0 && isAgentPsdImageLayerSlot(nextLayers[layerIndex])) {
|
||||
nextLayers[layerIndex] = {
|
||||
...nextLayers[layerIndex],
|
||||
contentBounds: visibleContentBounds,
|
||||
maskUrl: uploaded.url,
|
||||
maskPath: uploaded.pathname,
|
||||
};
|
||||
|
||||
@ -466,6 +466,7 @@ export async function renderAgentPsdTemplate(params: {
|
||||
imageWidth: Math.max(1, Number(metadata.width || 1)),
|
||||
imageHeight: Math.max(1, Number(metadata.height || 1)),
|
||||
replaceMode: layer.replaceMode,
|
||||
contentBounds: layer.contentBounds,
|
||||
placement: imagePlacements[layer.id],
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user