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, layer: AgentPsdTemplateTextLayerSlot,
value: string, value: string,
color: string color: string
): CSSProperties { ) {
const resolvedLayout = getTextLayerRenderFrame(layer, value); const resolvedText = getResolvedTextPreview(layer, value);
const resolvedLayout = getTextLayerRenderFrame(layer, resolvedText);
return { return (
width: resolvedLayout.width, <div
height: resolvedLayout.height, className="relative overflow-visible"
fontFamily: buildTextLayerFontFamily(layer), style={{
fontSize: resolvedLayout.fontSize, width: resolvedLayout.width,
lineHeight: layer.lineHeight, height: resolvedLayout.height,
letterSpacing: `${layer.letterSpacing}px`, }}
color, >
fontWeight: layer.fontWeight, {resolvedLayout.lines.map((line, index) => (
textAlign: layer.textAlign, <div
paddingLeft: layer.paddingX, className="absolute overflow-visible whitespace-pre"
paddingRight: layer.paddingX, key={`${layer.id}-preview-line-${index + 1}`}
paddingTop: layer.paddingY, style={{
paddingBottom: layer.paddingY, top: layer.paddingY + index * resolvedLayout.lineHeightPx,
whiteSpace: "pre", width: resolvedLayout.width,
wordBreak: "keep-all", paddingLeft: layer.paddingX,
overflowWrap: "normal", 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( 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() { 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"; 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 previewViewportRef = useRef<HTMLDivElement | null>(null);
const uploadInputRef = useRef<HTMLInputElement | null>(null); const uploadInputRef = useRef<HTMLInputElement | null>(null);
const inlineTextEditorRef = useRef<HTMLTextAreaElement | null>(null);
const previousSelectedTemplateRef = useRef<AgentPsdTemplate | null>(null); const previousSelectedTemplateRef = useRef<AgentPsdTemplate | null>(null);
const imageDragStateRef = useRef<{ const imageDragStateRef = useRef<{
layerId: string; layerId: string;
@ -1735,45 +1732,6 @@ export function AgentPsdTemplateEditorDialog({
}; };
}, [selectedTemplate]); }, [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( async function handleSlotUpload(
slotId: string, slotId: string,
event: ChangeEvent<HTMLInputElement> event: ChangeEvent<HTMLInputElement>
@ -2490,16 +2448,11 @@ export function AgentPsdTemplateEditorDialog({
height: textFrame.height, height: textFrame.height,
}} }}
> >
<div {renderTextLayerPreviewContent(
className="whitespace-pre" layer,
style={getTextLayerContentStyle( value || layer.placeholder || layer.layerName,
layer, resolvedTextColor
value || layer.placeholder || layer.layerName, )}
resolvedTextColor
)}
>
{value || layer.placeholder || layer.layerName}
</div>
</div> </div>
); );
})} })}
@ -2673,43 +2626,31 @@ export function AgentPsdTemplateEditorDialog({
src={layer.sourceUrl} src={layer.sourceUrl}
/> />
) : null} ) : null}
<div {preserveSourceAppearance ? null : (
className="absolute" <div
style={ className="absolute"
preserveSourceAppearance style={{
? { inset: 0 } left: editedTextFrame.offsetX,
: { top: editedTextFrame.offsetY,
left: editedTextFrame.offsetX, width: editedTextFrame.width,
top: editedTextFrame.offsetY, height: editedTextFrame.height,
width: editedTextFrame.width, }}
height: editedTextFrame.height, >
} {renderTextLayerPreviewContent(
}
>
<textarea
className="resize-none border-none bg-transparent outline-none"
onChange={(event) =>
setTextAssignments((current) => ({
...current,
[layer.id]: event.target.value,
}))
}
ref={inlineTextEditorRef}
style={getInlineTextEditorStyle(
layer, layer,
preserveSourceAppearance,
currentValue, currentValue,
resolvedTextColor resolvedTextColor
)} )}
value={currentValue} </div>
wrap="off" )}
/>
</div>
<div <div
className={getTextLayerNameBadgeClassName()} className={getTextLayerNameBadgeClassName()}
> >
{layer.layerName} {layer.layerName}
</div> </div>
<div className={getTextLayerHintBadgeClassName()}>
请在右侧面板编辑
</div>
</div> </div>
); );
} }