box size text

This commit is contained in:
hamid zarghami
2026-05-03 14:25:27 +03:30
parent 1bad82e680
commit b8f7b63ff1
2 changed files with 103 additions and 21 deletions
+11
View File
@@ -1,4 +1,5 @@
import { type FC, useEffect, useState } from 'react'
import Konva from 'konva'
import { clx } from '@/helpers/utils'
import EditorSidebar from './components/EditorSidebar'
import EditorCanvas from './components/EditorCanvas'
@@ -10,6 +11,16 @@ import { useEditorStore } from './store/editorStore'
const Editor: FC = () => {
const { id } = useParams()
// Konva 10 با legacyTextRendering=false متن را با baseline جدید می‌کشد ولی getWidth/getHeight
// و کادر ترنسفورمر هنوز بر اساس جعبهٔ قدیمی‌اند؛ نتیجه: متن از کادر آبی بیرون می‌زند.
useEffect(() => {
const prev = Konva.legacyTextRendering
Konva.legacyTextRendering = true
return () => {
Konva.legacyTextRendering = prev
}
}, [])
const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false)
const { loadPages, pages } = useEditorStore()
const { data } = useGetCatalogById(id!)
+90 -19
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useMemo, useState } from "react";
import { useEffect, useLayoutEffect, useRef, useMemo, useState } from "react";
import { Text, Group } from "react-konva";
import Konva from "konva";
import type { TextShapeProps } from "./types";
@@ -29,6 +29,15 @@ const getFontWeight = (fontWeight?: string): string => {
return weightMap[fontWeight] || "normal";
};
/** Konva.Text فقط `fontStyle` دارد (نه fontWeight جداگانه)؛ مقدار باید normal|bold|italic باشد. */
const getKonvaFontStyle = (fontWeight?: string): string => {
const w = getFontWeight(fontWeight);
if (w === "bold" || w === "bolder") return "bold";
const n = parseInt(w, 10);
if (!Number.isNaN(n) && n >= 600) return "bold";
return "normal";
};
const getColorWithOpacity = (fill?: string, opacity?: number): string => {
if (!fill) return "#000000";
if (opacity === undefined || opacity === 100) return fill;
@@ -61,26 +70,90 @@ const TextShape = ({
}
}, []);
// Update actual text dimensions after render
// Only update if width/height are not set (initial render)
useEffect(() => {
if (shapeRef.current && (!obj.width || !obj.height)) {
const node = shapeRef.current;
const clientRect = node.getClientRect({ skipTransform: true });
// هم‌تراز کردن width/height با Konva؛ بعد از لود وب‌فونت باید `_setTextData` دوباره اجرا شود
// وگرنه Konva هنوز عرض خطوط را با متریک فونت جایگزین نگه می‌دارد و باکس از متن کوچک می‌ماند.
useLayoutEffect(() => {
let cancelled = false;
// Update store with actual dimensions
if (clientRect.width && clientRect.height) {
onUpdate(obj.id, {
width: Math.ceil(clientRect.width),
height: Math.ceil(clientRect.height),
const syncFromNode = () => {
if (cancelled) return;
const textNode = shapeRef.current;
const groupNode = groupRef.current;
if (!textNode || !groupNode) return;
if (groupNode.scaleX() !== 1 || groupNode.scaleY() !== 1) return;
textNode._setTextData();
const { width: rw, height: rh } = textNode.getClientRect({
skipTransform: true,
});
const w = Math.ceil(rw);
const h = Math.ceil(rh);
if (w > 0 && h > 0 && (w !== obj.width || h !== obj.height)) {
onUpdate(obj.id, { width: w, height: h });
}
textNode.getLayer()?.batchDraw();
};
const scheduleSync = () => {
requestAnimationFrame(() => {
if (!cancelled) syncFromNode();
});
};
const resolvedFamily = getFontFamily(obj.fontFamily).split(",")[0].trim();
const fontSize = obj.fontSize || 24;
const konvaStyle = getKonvaFontStyle(obj.fontWeight);
const fontsApi = document.fonts;
const waitFontsThenSync = () => {
if (!fontsApi || typeof fontsApi.load !== "function") {
scheduleSync();
return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [obj.text, obj.fontSize, obj.fontFamily, obj.fontWeight, obj.letterSpacing]);
const specs = [
`${fontSize}px ${resolvedFamily}`,
`${konvaStyle} normal ${fontSize}px ${resolvedFamily}`,
`bold ${fontSize}px ${resolvedFamily}`,
`300 ${fontSize}px ${resolvedFamily}`,
`200 ${fontSize}px ${resolvedFamily}`,
];
void Promise.all(
specs.map((s) => fontsApi.load(s).catch(() => [])),
).finally(() => {
if (!cancelled) scheduleSync();
});
};
scheduleSync();
waitFontsThenSync();
const onLoadingDone = () => {
if (!cancelled) scheduleSync();
};
fontsApi?.addEventListener?.("loadingdone", onLoadingDone);
return () => {
cancelled = true;
fontsApi?.removeEventListener?.("loadingdone", onLoadingDone);
};
}, [
obj.id,
obj.text,
obj.fontSize,
obj.fontFamily,
obj.fontWeight,
obj.letterSpacing,
obj.width,
obj.height,
onUpdate,
]);
const fontFamily = useMemo(() => getFontFamily(obj.fontFamily), [obj.fontFamily]);
const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]);
const konvaFontStyle = useMemo(() => getKonvaFontStyle(obj.fontWeight), [obj.fontWeight]);
const fillColor = useMemo(() => getColorWithOpacity(obj.fill, obj.opacity), [obj.fill, obj.opacity]);
const handleDblClick = () => {
@@ -125,7 +198,7 @@ const TextShape = ({
minHeight: `${box.height}px`,
fontSize: `${(obj.fontSize || 24) * stageScale}px`,
fontFamily: fontFamily,
fontWeight: fontWeight,
fontWeight: getFontWeight(obj.fontWeight),
color: obj.fill || "#000000",
letterSpacing: `${(obj.letterSpacing || 0) * stageScale}px`,
lineHeight: textNode.lineHeight().toString(),
@@ -214,8 +287,6 @@ const TextShape = ({
name="text-object"
x={obj.x}
y={obj.y}
width={obj.width}
height={obj.height}
rotation={obj.rotation || 0}
draggable={draggable && !isEditing}
onClick={(e) => {
@@ -240,7 +311,7 @@ const TextShape = ({
fontSize={obj.fontSize || 24}
fill={fillColor}
fontFamily={fontFamily}
fontStyle={fontWeight}
fontStyle={konvaFontStyle}
letterSpacing={obj.letterSpacing ?? 0}
align="right"
/>