From b8f7b63ff1a00633393ff092ee561bb6f7b8973d Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 3 May 2026 14:25:27 +0330 Subject: [PATCH] box size text --- src/pages/editor/Editor.tsx | 11 ++ .../editor/components/tools/TextShape.tsx | 113 ++++++++++++++---- 2 files changed, 103 insertions(+), 21 deletions(-) diff --git a/src/pages/editor/Editor.tsx b/src/pages/editor/Editor.tsx index dbf79e9..5e364f6 100644 --- a/src/pages/editor/Editor.tsx +++ b/src/pages/editor/Editor.tsx @@ -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!) diff --git a/src/pages/editor/components/tools/TextShape.tsx b/src/pages/editor/components/tools/TextShape.tsx index a32c7a5..3584766 100644 --- a/src/pages/editor/components/tools/TextShape.tsx +++ b/src/pages/editor/components/tools/TextShape.tsx @@ -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 }); } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [obj.text, obj.fontSize, obj.fontFamily, obj.fontWeight, obj.letterSpacing]); + 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; + } + + 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" />