diff --git a/src/pages/editor/components/canvas/useDrawingHandlers.ts b/src/pages/editor/components/canvas/useDrawingHandlers.ts
index 54d8862..2c87916 100644
--- a/src/pages/editor/components/canvas/useDrawingHandlers.ts
+++ b/src/pages/editor/components/canvas/useDrawingHandlers.ts
@@ -3,6 +3,7 @@ import Konva from "konva";
import { useEditorStore, type EditorObject } from "@/pages/editor/store/editorStore";
import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore";
import { createDrawingObject } from "./drawingUtils";
+import { DEFAULT_TEXT_MAX_WIDTH_PX } from "@/pages/editor/utils/textStyle";
export const useDrawingHandlers = () => {
const [isDrawing, setIsDrawing] = useState(false);
@@ -82,6 +83,7 @@ export const useDrawingHandlers = () => {
letterSpacing: defaults.letterSpacing,
wordSpacing: defaults.wordSpacing,
height: defaults.fontSize || 24,
+ textMaxWidth: DEFAULT_TEXT_MAX_WIDTH_PX,
};
useEditorStore.getState().commitObjectHistoryBeforeChange();
addObject(newText);
diff --git a/src/pages/editor/components/sidebar/settings/TextSettings.tsx b/src/pages/editor/components/sidebar/settings/TextSettings.tsx
index cc7ad66..fb22457 100644
--- a/src/pages/editor/components/sidebar/settings/TextSettings.tsx
+++ b/src/pages/editor/components/sidebar/settings/TextSettings.tsx
@@ -1,6 +1,7 @@
import type { EditorObject } from "../../../store/editorStore";
import Input from "@/components/Input";
import ColorPicker from "@/components/ColorPicker";
+import { DEFAULT_TEXT_MAX_WIDTH_PX } from "@/pages/editor/utils/textStyle";
type TextSettingsProps = {
selectedObject: EditorObject;
@@ -60,6 +61,18 @@ const TextSettings = ({ selectedObject, onUpdate }: TextSettingsProps) => {
})
}
/>
+
+ onUpdate(selectedObject.id, {
+ textMaxWidth: Math.max(80, parseNumber(e.target.value) || DEFAULT_TEXT_MAX_WIDTH_PX),
+ })
+ }
+ />
چینش متن
diff --git a/src/pages/editor/components/tools/TextShape.tsx b/src/pages/editor/components/tools/TextShape.tsx
index b659a9f..8e11a9f 100644
--- a/src/pages/editor/components/tools/TextShape.tsx
+++ b/src/pages/editor/components/tools/TextShape.tsx
@@ -4,8 +4,10 @@ import Konva from "konva";
import type { TextShapeProps } from "./types";
import { getFontFamily } from "@/pages/editor/utils/fontFamily";
import {
+ DEFAULT_TEXT_MAX_WIDTH_PX,
getEffectiveTextWidth,
measureTextBlock,
+ resolveTextMaxWidth,
usesWrappedLayout,
} from "@/pages/editor/utils/textStyle";
@@ -58,6 +60,7 @@ const TextShape = ({
const shapeRef = useRef
(null);
const groupRef = useRef(null);
const [isEditing, setIsEditing] = useState(false);
+ const [maxTextWidth, setMaxTextWidth] = useState(DEFAULT_TEXT_MAX_WIDTH_PX);
const fontSize = obj.fontSize || 24;
const allowWrap = usesWrappedLayout(
obj.text,
@@ -86,6 +89,7 @@ const TextShape = ({
const groupNode = groupRef.current;
if (!textNode || !groupNode) return;
if (groupNode.scaleX() !== 1 || groupNode.scaleY() !== 1) return;
+ const stage = textNode.getStage();
textNode._setTextData();
@@ -110,6 +114,14 @@ const TextShape = ({
lineHeight: obj.lineHeight ?? 1.2,
});
const konvaWidth = Math.max(1, Math.ceil(rw));
+ const resolvedMaxWidth = resolveTextMaxWidth({
+ anchorX: obj.x || 0,
+ pageWidth: stage?.width(),
+ defaultMaxWidth: obj.textMaxWidth ?? DEFAULT_TEXT_MAX_WIDTH_PX,
+ });
+ if (Math.abs(resolvedMaxWidth - maxTextWidth) > 1) {
+ setMaxTextWidth(resolvedMaxWidth);
+ }
const nextWidth = getEffectiveTextWidth(
konvaWidth,
obj.text,
@@ -122,16 +134,33 @@ const TextShape = ({
},
!allowWrap,
) ?? konvaWidth;
+ const clampedWidth = Math.min(nextWidth, resolvedMaxWidth);
+ const forcedWrapByWidth = clampedWidth < nextWidth - 1;
+ let measuredHeight = rh;
+ if (forcedWrapByWidth) {
+ const wrappedMeasureNode = textNode.clone({
+ width: clampedWidth,
+ wrap: "word",
+ x: 0,
+ y: 0,
+ });
+ wrappedMeasureNode._setTextData();
+ const { height: wrappedHeight } = wrappedMeasureNode.getClientRect({
+ skipTransform: true,
+ });
+ wrappedMeasureNode.destroy();
+ measuredHeight = Math.max(measuredHeight, wrappedHeight);
+ }
const nextHeight = Math.max(
1,
- Math.ceil(rh),
+ Math.ceil(measuredHeight),
cssMeasured.height,
);
- const widthChanged = Math.abs((obj.width || 0) - nextWidth) > 1;
+ const widthChanged = Math.abs((obj.width || 0) - clampedWidth) > 1;
const heightChanged = Math.abs((obj.height || 0) - nextHeight) > 1;
if (widthChanged || heightChanged) {
- onUpdate(obj.id, { width: nextWidth, height: nextHeight });
+ onUpdate(obj.id, { width: clampedWidth, height: nextHeight });
}
textNode.getLayer()?.batchDraw();
};
@@ -191,7 +220,10 @@ const TextShape = ({
obj.lineHeight,
obj.width,
obj.height,
+ obj.x,
+ obj.textMaxWidth,
allowWrap,
+ maxTextWidth,
onUpdate,
]);
@@ -199,6 +231,8 @@ const TextShape = ({
const konvaFontStyle = useMemo(() => getKonvaFontStyle(obj.fontWeight), [obj.fontWeight]);
const fillColor = useMemo(() => getColorWithOpacity(obj.fill, obj.opacity), [obj.fill, obj.opacity]);
const textAlign = obj.textAlign ?? "right";
+ const wrapByMaxWidth = (obj.width ?? 0) >= maxTextWidth - 1;
+ const shouldWrap = allowWrap || wrapByMaxWidth;
const handleDblClick = () => {
if (!shapeRef.current || !groupRef.current) return;
@@ -238,7 +272,8 @@ const TextShape = ({
position: "absolute",
top: `${areaPosition.y}px`,
left: `${areaPosition.x}px`,
- width: `${box.width}px`,
+ width: `${Math.min(box.width, maxTextWidth * stageScale)}px`,
+ maxWidth: `${maxTextWidth * stageScale}px`,
minHeight: `${box.height}px`,
fontSize: `${(obj.fontSize || 24) * stageScale}px`,
fontFamily: fontFamily,
@@ -358,9 +393,9 @@ const TextShape = ({
fontStyle={konvaFontStyle}
letterSpacing={obj.letterSpacing ?? 0}
lineHeight={obj.lineHeight ?? 1.2}
- width={allowWrap ? obj.width || undefined : undefined}
+ width={shouldWrap ? obj.width || maxTextWidth : undefined}
align={textAlign}
- wrap={allowWrap ? "word" : "none"}
+ wrap={shouldWrap ? "word" : "none"}
/>
);
diff --git a/src/pages/editor/store/editorStore.types.ts b/src/pages/editor/store/editorStore.types.ts
index d9cde94..19bf24b 100644
--- a/src/pages/editor/store/editorStore.types.ts
+++ b/src/pages/editor/store/editorStore.types.ts
@@ -61,6 +61,8 @@ export type EditorObject = {
stroke?: string;
strokeWidth?: number;
text?: string;
+ /** حداکثر عرض باکس متن (px) برای wrap پیشفرض متنهای بلند */
+ textMaxWidth?: number;
fontSize?: number;
lineHeight?: number;
textAlign?: "left" | "center" | "right";
diff --git a/src/pages/editor/utils/textStyle.ts b/src/pages/editor/utils/textStyle.ts
index 1bd552e..93462d7 100644
--- a/src/pages/editor/utils/textStyle.ts
+++ b/src/pages/editor/utils/textStyle.ts
@@ -63,6 +63,39 @@ export const usesWrappedLayout = (
/** Extra px so HTML layout does not wrap tighter than Konva/canvas metrics. */
export const SINGLE_LINE_WIDTH_SLACK_PX = 12;
+export const DEFAULT_TEXT_MAX_WIDTH_PX = 520;
+const MIN_TEXT_MAX_WIDTH_PX = 80;
+
+/**
+ * Maximum width for text boxes.
+ * - never larger than page/view bounds
+ * - has a sane default for long single-line text
+ */
+export const resolveTextMaxWidth = (args: {
+ anchorX: number;
+ pageWidth?: number;
+ defaultMaxWidth?: number;
+ edgePadding?: number;
+}): number => {
+ const {
+ anchorX,
+ pageWidth,
+ defaultMaxWidth = DEFAULT_TEXT_MAX_WIDTH_PX,
+ edgePadding = 8,
+ } = args;
+
+ const fromAnchor = Math.floor(anchorX - edgePadding);
+ const fromPage =
+ pageWidth !== undefined
+ ? Math.floor(Math.min(anchorX - edgePadding, pageWidth - edgePadding))
+ : fromAnchor;
+
+ const hardLimit = Math.max(MIN_TEXT_MAX_WIDTH_PX, fromPage);
+ return Math.max(
+ MIN_TEXT_MAX_WIDTH_PX,
+ Math.min(defaultMaxWidth, hardLimit),
+ );
+};
export type TextMeasureOptions = {
fontSize: number;
diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx
index 1c8c2f1..3938aba 100644
--- a/src/pages/viewer/components/BookPage.tsx
+++ b/src/pages/viewer/components/BookPage.tsx
@@ -5,8 +5,10 @@ import { toCssLinearGradient, getSvgGradientEndpoints } from '@/pages/editor/uti
import { getCustomShapePointBounds } from '@/pages/editor/utils/customShape';
import { getFontFamily } from '@/pages/editor/utils/fontFamily';
import {
+ DEFAULT_TEXT_MAX_WIDTH_PX,
getCssFontWeight,
getViewerTextLayout,
+ resolveTextMaxWidth,
usesWrappedLayout,
} from '@/pages/editor/utils/textStyle';
import '@/pages/viewer/styles/entranceAnimations.css';
@@ -199,14 +201,39 @@ const BookPage = forwardRef(
measureOpts,
wrapped,
});
+ const maxWidthPx = Math.ceil(
+ resolveTextMaxWidth({
+ anchorX: (obj.x || 0) * scale,
+ pageWidth: pageWidth ?? 794 * scale,
+ defaultMaxWidth: (obj.textMaxWidth ?? DEFAULT_TEXT_MAX_WIDTH_PX) * scale,
+ }),
+ );
+ const wrappedWidthPx =
+ obj.width !== undefined ? Math.ceil(obj.width * scale) : undefined;
+ const wrapByMaxWidth =
+ (wrapped ? wrappedWidthPx : textLayout.widthPx) !== undefined &&
+ (wrapped ? wrappedWidthPx : textLayout.widthPx)! > maxWidthPx;
+ const finalWrapped = wrapped || wrapByMaxWidth;
+ const resolvedWidthPx = finalWrapped
+ ? (wrappedWidthPx !== undefined
+ ? Math.min(wrappedWidthPx, maxWidthPx)
+ : (textLayout.widthPx !== undefined
+ ? Math.min(textLayout.widthPx, maxWidthPx)
+ : undefined))
+ : textLayout.widthPx;
+ const anchorRightPx = Math.round((obj.x || 0) * scale);
+ const resolvedLeftPx =
+ resolvedWidthPx !== undefined
+ ? anchorRightPx - resolvedWidthPx
+ : textLayout.leftPx;
return (
(
textAlign: textLayout.textAlign,
direction: 'rtl',
color: getColorWithOpacity(obj.fill, obj.opacity),
- whiteSpace: wrapped ? 'pre-wrap' : 'nowrap',
- overflowWrap: wrapped ? 'break-word' : 'normal',
+ whiteSpace: finalWrapped ? 'pre-wrap' : 'nowrap',
+ overflowWrap: finalWrapped ? 'break-word' : 'normal',
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
boxSizing: 'content-box',
+ zIndex: index,
transform: textLayout.transform,
transformOrigin: textLayout.transformOrigin,
},