fix: streamline psd editor layout and scaling
This commit is contained in:
parent
8c0ce434dc
commit
a63db2af6b
@ -43,7 +43,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
getDefaultAgentPsdImagePlacement,
|
||||
normalizeAgentPsdImagePlacement,
|
||||
resolveAgentPsdImagePlacementFrame,
|
||||
resolveAgentPsdImageBaseFrame,
|
||||
type AgentPsdImagePlacement,
|
||||
type AgentPsdImageSize,
|
||||
} from "@/lib/agents/psd-image-placement";
|
||||
@ -344,22 +344,25 @@ function getPlacedAssignedImageStyle(params: {
|
||||
placement: AgentPsdImagePlacement;
|
||||
}): CSSProperties {
|
||||
const { layer, imageSize, placement } = params;
|
||||
const frame = resolveAgentPsdImagePlacementFrame({
|
||||
const normalizedPlacement = normalizeAgentPsdImagePlacement(placement);
|
||||
const baseFrame = resolveAgentPsdImageBaseFrame({
|
||||
containerWidth: layer.width,
|
||||
containerHeight: layer.height,
|
||||
imageWidth: imageSize.width,
|
||||
imageHeight: imageSize.height,
|
||||
replaceMode: layer.replaceMode,
|
||||
contentBounds: layer.contentBounds,
|
||||
placement,
|
||||
});
|
||||
|
||||
return {
|
||||
position: "absolute",
|
||||
left: frame.left,
|
||||
top: frame.top,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
left: baseFrame.centerX + normalizedPlacement.offsetX,
|
||||
top: baseFrame.centerY + normalizedPlacement.offsetY,
|
||||
width: baseFrame.width,
|
||||
height: baseFrame.height,
|
||||
transform: `translate(-50%, -50%) scale(${normalizedPlacement.scale})`,
|
||||
transformOrigin: "center center",
|
||||
willChange: "transform",
|
||||
};
|
||||
}
|
||||
|
||||
@ -442,6 +445,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
const [uploadingSlotId, setUploadingSlotId] = useState<string | null>(null);
|
||||
const [isRendering, setIsRendering] = useState(false);
|
||||
const [renderedResultUrl, setRenderedResultUrl] = useState<string>("");
|
||||
const [rightPanelView, setRightPanelView] = useState<"editor" | "export">("editor");
|
||||
const [stageScale, setStageScale] = useState(1);
|
||||
|
||||
const previewViewportRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -639,6 +643,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setRenderedResultUrl("");
|
||||
setRightPanelView("editor");
|
||||
const initialState = createInitialState(selectedTemplate, sourceImageUrl || "");
|
||||
setImageAssignments(initialState.imageAssignments);
|
||||
setImagePlacements(initialState.imagePlacements);
|
||||
@ -978,6 +983,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
}
|
||||
|
||||
setRenderedResultUrl(payload.result.url);
|
||||
setRightPanelView("export");
|
||||
toast.success("模板替换结果已生成");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "PSD 模板渲染失败");
|
||||
@ -1544,7 +1550,37 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-col bg-[#09090b] p-4 xl:p-5">
|
||||
<div className="mb-3 text-sm font-medium">当前选中图层</div>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium">
|
||||
{renderedResultUrl && rightPanelView === "export" ? "导出结果" : "当前选中图层"}
|
||||
</div>
|
||||
{renderedResultUrl ? (
|
||||
<div className="inline-flex rounded-full border border-white/10 bg-white/[0.03] p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelView("editor")}
|
||||
className={`rounded-full px-3 py-1.5 text-xs font-medium transition ${
|
||||
rightPanelView === "editor"
|
||||
? "bg-emerald-500/[0.16] text-emerald-100"
|
||||
: "text-white/60 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
编辑面板
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRightPanelView("export")}
|
||||
className={`rounded-full px-3 py-1.5 text-xs font-medium transition ${
|
||||
rightPanelView === "export"
|
||||
? "bg-emerald-500/[0.16] text-emerald-100"
|
||||
: "text-white/60 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
导出结果
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
ref={uploadInputRef}
|
||||
@ -1557,10 +1593,35 @@ 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">
|
||||
{renderedResultUrl && rightPanelView === "export" ? (
|
||||
<div className="flex min-h-0 flex-1 flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">
|
||||
<div className="flex h-full min-h-0 flex-col justify-between gap-4">
|
||||
<div className="mx-auto w-full max-w-[230px] overflow-hidden rounded-2xl border border-white/10 bg-white/5">
|
||||
<div
|
||||
style={{
|
||||
aspectRatio: selectedTemplate
|
||||
? `${Math.max(1, selectedTemplate.width)} / ${Math.max(1, selectedTemplate.height)}`
|
||||
: "1 / 1",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={renderedResultUrl}
|
||||
alt="PSD 模板导出结果"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleDownload} className="w-full">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
下载结果图
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-0 flex-1 flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">
|
||||
{selectedImageLayer ? (
|
||||
<div className="min-h-0 space-y-3 overflow-y-auto pr-1">
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="default">可替换图片</Badge>
|
||||
<div className="text-sm font-medium">{selectedImageLayer.layerName}</div>
|
||||
@ -1570,7 +1631,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{selectedImageLayer.layerPath}</div>
|
||||
<div className="overflow-hidden rounded-xl border border-white/10 bg-white/5">
|
||||
<div className="h-[150px] xl:h-[160px]">
|
||||
<div className="flex h-[160px] items-center justify-center p-3 xl:h-[180px]">
|
||||
{effectiveImageAssignments[selectedImageLayer.id] ? (
|
||||
<img
|
||||
src={effectiveImageAssignments[selectedImageLayer.id]}
|
||||
@ -1622,7 +1683,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
thumbClassName="h-5 w-5 border-2 border-emerald-100 bg-[#0a0a0d] shadow-[0_0_0_5px_rgba(16,185,129,0.2)]"
|
||||
onValueChange={(value) => {
|
||||
const newScale = clampImageScale(value[0] || 1);
|
||||
// offset 本身就是相对中心位移,缩放时不应按比例放大,否则会导致画面偏移看起来像“变形”。
|
||||
updateImagePlacement(selectedImageLayer.id, (current) => ({
|
||||
...current,
|
||||
scale: newScale,
|
||||
@ -1658,29 +1718,27 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="mt-auto flex flex-wrap gap-2">
|
||||
{sourceImageUrl ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
{
|
||||
setImageAssignments((current) => ({
|
||||
...current,
|
||||
[selectedImageLayer.id]: sourceImageUrl,
|
||||
}));
|
||||
setImagePlacements((current) => ({
|
||||
...current,
|
||||
[selectedImageLayer.id]: getDefaultAgentPsdImagePlacement(),
|
||||
}));
|
||||
setImageSizes((current) => {
|
||||
const next = { ...current };
|
||||
delete next[selectedImageLayer.id];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
onClick={() => {
|
||||
setImageAssignments((current) => ({
|
||||
...current,
|
||||
[selectedImageLayer.id]: sourceImageUrl,
|
||||
}));
|
||||
setImagePlacements((current) => ({
|
||||
...current,
|
||||
[selectedImageLayer.id]: getDefaultAgentPsdImagePlacement(),
|
||||
}));
|
||||
setImageSizes((current) => {
|
||||
const next = { ...current };
|
||||
delete next[selectedImageLayer.id];
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
使用当前图片
|
||||
</Button>
|
||||
@ -1728,7 +1786,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
) : selectedTextLayer ? (
|
||||
<div className="min-h-0 space-y-3 overflow-y-auto pr-1">
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="default">可编辑文字</Badge>
|
||||
<div className="text-sm font-medium">{selectedTextLayer.layerName}</div>
|
||||
@ -1741,7 +1799,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
rows={4}
|
||||
value={effectiveTextAssignments[selectedTextLayer.id] || ""}
|
||||
placeholder={selectedTextLayer.placeholder || "请输入文字"}
|
||||
className="min-h-[150px] resize-none xl:min-h-[132px]"
|
||||
className="min-h-[180px] flex-1 resize-none xl:min-h-[200px]"
|
||||
onChange={(event) =>
|
||||
setTextAssignments((current) => ({
|
||||
...current,
|
||||
@ -1817,49 +1875,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">
|
||||
<div className="mb-3 text-sm font-medium">导出结果</div>
|
||||
{renderedResultUrl ? (
|
||||
<div className="flex h-full min-h-0 flex-col justify-between gap-4">
|
||||
<div className="mx-auto w-full max-w-[210px] overflow-hidden rounded-2xl border border-white/10 bg-white/5">
|
||||
<div
|
||||
style={{
|
||||
aspectRatio: selectedTemplate
|
||||
? `${Math.max(1, selectedTemplate.width)} / ${Math.max(1, selectedTemplate.height)}`
|
||||
: "1 / 1",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={renderedResultUrl}
|
||||
alt="PSD 模板导出结果"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleDownload} className="w-full">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
下载结果图
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full min-h-[160px] flex-col items-center justify-center rounded-2xl border border-dashed border-white/15 bg-white/[0.02] px-5 text-center">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/5">
|
||||
{selectedTextLayer ? (
|
||||
<Type className="h-7 w-7 text-muted-foreground" />
|
||||
) : (
|
||||
<ImageIcon className="h-7 w-7 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-sm font-medium">还未导出成品图</div>
|
||||
<div className="mt-2 text-xs leading-6 text-muted-foreground">
|
||||
在模板画布里完成替换后,点击“应用模板并导出”,这里会显示最终图片。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@ -14,6 +14,15 @@ export interface AgentPsdImageSize {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface AgentPsdImageFrame {
|
||||
width: number;
|
||||
height: number;
|
||||
left: number;
|
||||
top: number;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
}
|
||||
|
||||
export function getDefaultAgentPsdImagePlacement(): AgentPsdImagePlacement {
|
||||
return {
|
||||
scale: 1,
|
||||
@ -106,17 +115,15 @@ function normalizeContainerBounds(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentPsdImagePlacementFrame(params: {
|
||||
export function resolveAgentPsdImageBaseFrame(params: {
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
replaceMode: AgentPsdTemplateReplaceMode;
|
||||
contentBounds?: AgentPsdTemplateImageContentBounds;
|
||||
placement?: Partial<AgentPsdImagePlacement> | null;
|
||||
}) {
|
||||
}): AgentPsdImageFrame {
|
||||
const { containerWidth, containerHeight, imageWidth, imageHeight, replaceMode } = params;
|
||||
const placement = normalizeAgentPsdImagePlacement(params.placement);
|
||||
const contentBounds = normalizeContainerBounds({
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
@ -130,23 +137,36 @@ export function resolveAgentPsdImagePlacementFrame(params: {
|
||||
replaceMode,
|
||||
});
|
||||
|
||||
return {
|
||||
width: baseFrame.width,
|
||||
height: baseFrame.height,
|
||||
left: contentBounds.left + baseFrame.left,
|
||||
top: contentBounds.top + baseFrame.top,
|
||||
centerX: contentBounds.left + baseFrame.left + baseFrame.width / 2,
|
||||
centerY: contentBounds.top + baseFrame.top + baseFrame.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentPsdImagePlacementFrame(params: {
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
replaceMode: AgentPsdTemplateReplaceMode;
|
||||
contentBounds?: AgentPsdTemplateImageContentBounds;
|
||||
placement?: Partial<AgentPsdImagePlacement> | null;
|
||||
}): AgentPsdImageFrame {
|
||||
const placement = normalizeAgentPsdImagePlacement(params.placement);
|
||||
const baseFrame = resolveAgentPsdImageBaseFrame(params);
|
||||
const width = baseFrame.width * placement.scale;
|
||||
const height = baseFrame.height * placement.scale;
|
||||
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,
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
left: baseFrame.centerX + placement.offsetX - width / 2,
|
||||
top: baseFrame.centerY + placement.offsetY - height / 2,
|
||||
centerX: baseFrame.centerX + placement.offsetX,
|
||||
centerY: baseFrame.centerY + placement.offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
@ -459,7 +459,8 @@ export async function renderAgentPsdTemplate(params: {
|
||||
}
|
||||
|
||||
const sourceBuffer = await readCachedBuffer(assignmentUrl);
|
||||
const metadata = await sharp(sourceBuffer).metadata();
|
||||
const normalizedSourceBuffer = await sharp(sourceBuffer).rotate().png().toBuffer();
|
||||
const metadata = await sharp(normalizedSourceBuffer).metadata();
|
||||
const placementFrame = resolveAgentPsdImagePlacementFrame({
|
||||
containerWidth: layer.width,
|
||||
containerHeight: layer.height,
|
||||
@ -470,8 +471,7 @@ export async function renderAgentPsdTemplate(params: {
|
||||
placement: imagePlacements[layer.id],
|
||||
});
|
||||
|
||||
const resizedOverlayBuffer = await sharp(sourceBuffer)
|
||||
.rotate()
|
||||
const resizedOverlayBuffer = await sharp(normalizedSourceBuffer)
|
||||
.resize({
|
||||
width: Math.max(1, Math.round(placementFrame.width)),
|
||||
height: Math.max(1, Math.round(placementFrame.height)),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user