fix: stabilize psd text preview positioning
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-10 20:35:59 +08:00
parent 9b745e5591
commit d450bec1d7

View File

@ -821,31 +821,47 @@ function getTextLayerRenderFrame(
};
}
function getTextLayerContentStyle(
function renderTextLayerPreviewContent(
layer: AgentPsdTemplateTextLayerSlot,
value: string,
color: string
): CSSProperties {
const resolvedLayout = getTextLayerRenderFrame(layer, value);
) {
const resolvedText = getResolvedTextPreview(layer, value);
const resolvedLayout = getTextLayerRenderFrame(layer, resolvedText);
return {
width: resolvedLayout.width,
height: resolvedLayout.height,
fontFamily: buildTextLayerFontFamily(layer),
fontSize: resolvedLayout.fontSize,
lineHeight: layer.lineHeight,
letterSpacing: `${layer.letterSpacing}px`,
color,
fontWeight: layer.fontWeight,
textAlign: layer.textAlign,
paddingLeft: layer.paddingX,
paddingRight: layer.paddingX,
paddingTop: layer.paddingY,
paddingBottom: layer.paddingY,
whiteSpace: "pre",
wordBreak: "keep-all",
overflowWrap: "normal",
};
return (
<div
className="relative overflow-visible"
style={{
width: resolvedLayout.width,
height: resolvedLayout.height,
}}
>
{resolvedLayout.lines.map((line, index) => (
<div
className="absolute overflow-visible whitespace-pre"
key={`${layer.id}-preview-line-${index + 1}`}
style={{
top: layer.paddingY + index * resolvedLayout.lineHeightPx,
width: resolvedLayout.width,
paddingLeft: layer.paddingX,
paddingRight: layer.paddingX,
fontFamily: buildTextLayerFontFamily(layer),
fontSize: resolvedLayout.fontSize,
lineHeight: `${resolvedLayout.fontSize}px`,
letterSpacing: `${layer.letterSpacing}px`,
color,
fontWeight: layer.fontWeight,
textAlign: layer.textAlign,
wordBreak: "keep-all",
overflowWrap: "normal",
}}
>
{line || "\u00a0"}
</div>
))}
</div>
);
}
function getAssignedImageMaskStyle(
@ -916,24 +932,6 @@ function hasCustomizedTextLayerAppearance(
);
}
function getInlineTextEditorStyle(
layer: AgentPsdTemplateTextLayerSlot,
preserveSourceAppearance: boolean,
value: string,
color: string
): CSSProperties {
const baseStyle = getTextLayerContentStyle(layer, value, color);
return {
...baseStyle,
background: "transparent",
boxSizing: "border-box",
overflow: "visible",
color: preserveSourceAppearance ? "transparent" : color,
caretColor: color,
WebkitTextFillColor: preserveSourceAppearance ? "transparent" : color,
};
}
function getTextLayerNameBadgeClassName() {
return "pointer-events-none absolute left-0 top-0 -translate-y-[calc(100%+6px)] rounded-full bg-black/70 px-2 py-1 text-[11px] whitespace-nowrap text-white shadow-lg backdrop-blur-md";
}
@ -1059,7 +1057,6 @@ export function AgentPsdTemplateEditorDialog({
const previewViewportRef = useRef<HTMLDivElement | null>(null);
const uploadInputRef = useRef<HTMLInputElement | null>(null);
const inlineTextEditorRef = useRef<HTMLTextAreaElement | null>(null);
const previousSelectedTemplateRef = useRef<AgentPsdTemplate | null>(null);
const imageDragStateRef = useRef<{
layerId: string;
@ -1735,45 +1732,6 @@ export function AgentPsdTemplateEditorDialog({
};
}, [selectedTemplate]);
useEffect(() => {
if (!selectedTextLayer) {
return;
}
const textarea = inlineTextEditorRef.current;
if (!textarea) {
return;
}
const focusTextarea = () => {
textarea.focus();
const valueLength = textarea.value.length;
try {
textarea.setSelectionRange(valueLength, valueLength);
} catch (error) {
console.warn(
"[AgentPsdTemplateEditorDialog] setSelectionRange failed:",
error
);
}
};
if (
typeof window !== "undefined" &&
typeof window.requestAnimationFrame === "function"
) {
const frameId = window.requestAnimationFrame(focusTextarea);
return () => {
window.cancelAnimationFrame(frameId);
};
}
const timeoutId = window.setTimeout(focusTextarea, 0);
return () => {
window.clearTimeout(timeoutId);
};
}, [selectedTextLayer?.id]);
async function handleSlotUpload(
slotId: string,
event: ChangeEvent<HTMLInputElement>
@ -2490,16 +2448,11 @@ export function AgentPsdTemplateEditorDialog({
height: textFrame.height,
}}
>
<div
className="whitespace-pre"
style={getTextLayerContentStyle(
layer,
value || layer.placeholder || layer.layerName,
resolvedTextColor
)}
>
{value || layer.placeholder || layer.layerName}
</div>
{renderTextLayerPreviewContent(
layer,
value || layer.placeholder || layer.layerName,
resolvedTextColor
)}
</div>
);
})}
@ -2673,43 +2626,31 @@ export function AgentPsdTemplateEditorDialog({
src={layer.sourceUrl}
/>
) : null}
<div
className="absolute"
style={
preserveSourceAppearance
? { inset: 0 }
: {
left: editedTextFrame.offsetX,
top: editedTextFrame.offsetY,
width: editedTextFrame.width,
height: editedTextFrame.height,
}
}
>
<textarea
className="resize-none border-none bg-transparent outline-none"
onChange={(event) =>
setTextAssignments((current) => ({
...current,
[layer.id]: event.target.value,
}))
}
ref={inlineTextEditorRef}
style={getInlineTextEditorStyle(
{preserveSourceAppearance ? null : (
<div
className="absolute"
style={{
left: editedTextFrame.offsetX,
top: editedTextFrame.offsetY,
width: editedTextFrame.width,
height: editedTextFrame.height,
}}
>
{renderTextLayerPreviewContent(
layer,
preserveSourceAppearance,
currentValue,
resolvedTextColor
)}
value={currentValue}
wrap="off"
/>
</div>
</div>
)}
<div
className={getTextLayerNameBadgeClassName()}
>
{layer.layerName}
</div>
<div className={getTextLayerHintBadgeClassName()}>
请在右侧面板编辑
</div>
</div>
);
}