This commit is contained in:
@@ -3,6 +3,7 @@ import Konva from "konva";
|
|||||||
import { useEditorStore, type EditorObject } from "@/pages/editor/store/editorStore";
|
import { useEditorStore, type EditorObject } from "@/pages/editor/store/editorStore";
|
||||||
import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore";
|
import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore";
|
||||||
import { createDrawingObject } from "./drawingUtils";
|
import { createDrawingObject } from "./drawingUtils";
|
||||||
|
import { DEFAULT_TEXT_MAX_WIDTH_PX } from "@/pages/editor/utils/textStyle";
|
||||||
|
|
||||||
export const useDrawingHandlers = () => {
|
export const useDrawingHandlers = () => {
|
||||||
const [isDrawing, setIsDrawing] = useState(false);
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
@@ -82,6 +83,7 @@ export const useDrawingHandlers = () => {
|
|||||||
letterSpacing: defaults.letterSpacing,
|
letterSpacing: defaults.letterSpacing,
|
||||||
wordSpacing: defaults.wordSpacing,
|
wordSpacing: defaults.wordSpacing,
|
||||||
height: defaults.fontSize || 24,
|
height: defaults.fontSize || 24,
|
||||||
|
textMaxWidth: DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||||
};
|
};
|
||||||
useEditorStore.getState().commitObjectHistoryBeforeChange();
|
useEditorStore.getState().commitObjectHistoryBeforeChange();
|
||||||
addObject(newText);
|
addObject(newText);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { EditorObject } from "../../../store/editorStore";
|
import type { EditorObject } from "../../../store/editorStore";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import ColorPicker from "@/components/ColorPicker";
|
import ColorPicker from "@/components/ColorPicker";
|
||||||
|
import { DEFAULT_TEXT_MAX_WIDTH_PX } from "@/pages/editor/utils/textStyle";
|
||||||
|
|
||||||
type TextSettingsProps = {
|
type TextSettingsProps = {
|
||||||
selectedObject: EditorObject;
|
selectedObject: EditorObject;
|
||||||
@@ -60,6 +61,18 @@ const TextSettings = ({ selectedObject, onUpdate }: TextSettingsProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
label="حداکثر عرض متن"
|
||||||
|
type="number"
|
||||||
|
value={selectedObject.textMaxWidth ?? DEFAULT_TEXT_MAX_WIDTH_PX}
|
||||||
|
step="1"
|
||||||
|
min="80"
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate(selectedObject.id, {
|
||||||
|
textMaxWidth: Math.max(80, parseNumber(e.target.value) || DEFAULT_TEXT_MAX_WIDTH_PX),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm">چینش متن</p>
|
<p className="text-sm">چینش متن</p>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import Konva from "konva";
|
|||||||
import type { TextShapeProps } from "./types";
|
import type { TextShapeProps } from "./types";
|
||||||
import { getFontFamily } from "@/pages/editor/utils/fontFamily";
|
import { getFontFamily } from "@/pages/editor/utils/fontFamily";
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||||
getEffectiveTextWidth,
|
getEffectiveTextWidth,
|
||||||
measureTextBlock,
|
measureTextBlock,
|
||||||
|
resolveTextMaxWidth,
|
||||||
usesWrappedLayout,
|
usesWrappedLayout,
|
||||||
} from "@/pages/editor/utils/textStyle";
|
} from "@/pages/editor/utils/textStyle";
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ const TextShape = ({
|
|||||||
const shapeRef = useRef<Konva.Text>(null);
|
const shapeRef = useRef<Konva.Text>(null);
|
||||||
const groupRef = useRef<Konva.Group>(null);
|
const groupRef = useRef<Konva.Group>(null);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [maxTextWidth, setMaxTextWidth] = useState<number>(DEFAULT_TEXT_MAX_WIDTH_PX);
|
||||||
const fontSize = obj.fontSize || 24;
|
const fontSize = obj.fontSize || 24;
|
||||||
const allowWrap = usesWrappedLayout(
|
const allowWrap = usesWrappedLayout(
|
||||||
obj.text,
|
obj.text,
|
||||||
@@ -86,6 +89,7 @@ const TextShape = ({
|
|||||||
const groupNode = groupRef.current;
|
const groupNode = groupRef.current;
|
||||||
if (!textNode || !groupNode) return;
|
if (!textNode || !groupNode) return;
|
||||||
if (groupNode.scaleX() !== 1 || groupNode.scaleY() !== 1) return;
|
if (groupNode.scaleX() !== 1 || groupNode.scaleY() !== 1) return;
|
||||||
|
const stage = textNode.getStage();
|
||||||
|
|
||||||
textNode._setTextData();
|
textNode._setTextData();
|
||||||
|
|
||||||
@@ -110,6 +114,14 @@ const TextShape = ({
|
|||||||
lineHeight: obj.lineHeight ?? 1.2,
|
lineHeight: obj.lineHeight ?? 1.2,
|
||||||
});
|
});
|
||||||
const konvaWidth = Math.max(1, Math.ceil(rw));
|
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(
|
const nextWidth = getEffectiveTextWidth(
|
||||||
konvaWidth,
|
konvaWidth,
|
||||||
obj.text,
|
obj.text,
|
||||||
@@ -122,16 +134,33 @@ const TextShape = ({
|
|||||||
},
|
},
|
||||||
!allowWrap,
|
!allowWrap,
|
||||||
) ?? konvaWidth;
|
) ?? 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(
|
const nextHeight = Math.max(
|
||||||
1,
|
1,
|
||||||
Math.ceil(rh),
|
Math.ceil(measuredHeight),
|
||||||
cssMeasured.height,
|
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;
|
const heightChanged = Math.abs((obj.height || 0) - nextHeight) > 1;
|
||||||
|
|
||||||
if (widthChanged || heightChanged) {
|
if (widthChanged || heightChanged) {
|
||||||
onUpdate(obj.id, { width: nextWidth, height: nextHeight });
|
onUpdate(obj.id, { width: clampedWidth, height: nextHeight });
|
||||||
}
|
}
|
||||||
textNode.getLayer()?.batchDraw();
|
textNode.getLayer()?.batchDraw();
|
||||||
};
|
};
|
||||||
@@ -191,7 +220,10 @@ const TextShape = ({
|
|||||||
obj.lineHeight,
|
obj.lineHeight,
|
||||||
obj.width,
|
obj.width,
|
||||||
obj.height,
|
obj.height,
|
||||||
|
obj.x,
|
||||||
|
obj.textMaxWidth,
|
||||||
allowWrap,
|
allowWrap,
|
||||||
|
maxTextWidth,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -199,6 +231,8 @@ const TextShape = ({
|
|||||||
const konvaFontStyle = useMemo(() => getKonvaFontStyle(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 fillColor = useMemo(() => getColorWithOpacity(obj.fill, obj.opacity), [obj.fill, obj.opacity]);
|
||||||
const textAlign = obj.textAlign ?? "right";
|
const textAlign = obj.textAlign ?? "right";
|
||||||
|
const wrapByMaxWidth = (obj.width ?? 0) >= maxTextWidth - 1;
|
||||||
|
const shouldWrap = allowWrap || wrapByMaxWidth;
|
||||||
|
|
||||||
const handleDblClick = () => {
|
const handleDblClick = () => {
|
||||||
if (!shapeRef.current || !groupRef.current) return;
|
if (!shapeRef.current || !groupRef.current) return;
|
||||||
@@ -238,7 +272,8 @@ const TextShape = ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: `${areaPosition.y}px`,
|
top: `${areaPosition.y}px`,
|
||||||
left: `${areaPosition.x}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`,
|
minHeight: `${box.height}px`,
|
||||||
fontSize: `${(obj.fontSize || 24) * stageScale}px`,
|
fontSize: `${(obj.fontSize || 24) * stageScale}px`,
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
@@ -358,9 +393,9 @@ const TextShape = ({
|
|||||||
fontStyle={konvaFontStyle}
|
fontStyle={konvaFontStyle}
|
||||||
letterSpacing={obj.letterSpacing ?? 0}
|
letterSpacing={obj.letterSpacing ?? 0}
|
||||||
lineHeight={obj.lineHeight ?? 1.2}
|
lineHeight={obj.lineHeight ?? 1.2}
|
||||||
width={allowWrap ? obj.width || undefined : undefined}
|
width={shouldWrap ? obj.width || maxTextWidth : undefined}
|
||||||
align={textAlign}
|
align={textAlign}
|
||||||
wrap={allowWrap ? "word" : "none"}
|
wrap={shouldWrap ? "word" : "none"}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ export type EditorObject = {
|
|||||||
stroke?: string;
|
stroke?: string;
|
||||||
strokeWidth?: number;
|
strokeWidth?: number;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
/** حداکثر عرض باکس متن (px) برای wrap پیشفرض متنهای بلند */
|
||||||
|
textMaxWidth?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
lineHeight?: number;
|
lineHeight?: number;
|
||||||
textAlign?: "left" | "center" | "right";
|
textAlign?: "left" | "center" | "right";
|
||||||
|
|||||||
@@ -63,6 +63,39 @@ export const usesWrappedLayout = (
|
|||||||
|
|
||||||
/** Extra px so HTML layout does not wrap tighter than Konva/canvas metrics. */
|
/** Extra px so HTML layout does not wrap tighter than Konva/canvas metrics. */
|
||||||
export const SINGLE_LINE_WIDTH_SLACK_PX = 12;
|
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 = {
|
export type TextMeasureOptions = {
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import { toCssLinearGradient, getSvgGradientEndpoints } from '@/pages/editor/uti
|
|||||||
import { getCustomShapePointBounds } from '@/pages/editor/utils/customShape';
|
import { getCustomShapePointBounds } from '@/pages/editor/utils/customShape';
|
||||||
import { getFontFamily } from '@/pages/editor/utils/fontFamily';
|
import { getFontFamily } from '@/pages/editor/utils/fontFamily';
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_TEXT_MAX_WIDTH_PX,
|
||||||
getCssFontWeight,
|
getCssFontWeight,
|
||||||
getViewerTextLayout,
|
getViewerTextLayout,
|
||||||
|
resolveTextMaxWidth,
|
||||||
usesWrappedLayout,
|
usesWrappedLayout,
|
||||||
} from '@/pages/editor/utils/textStyle';
|
} from '@/pages/editor/utils/textStyle';
|
||||||
import '@/pages/viewer/styles/entranceAnimations.css';
|
import '@/pages/viewer/styles/entranceAnimations.css';
|
||||||
@@ -199,14 +201,39 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
measureOpts,
|
measureOpts,
|
||||||
wrapped,
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={applyStyle(
|
style={applyStyle(
|
||||||
{
|
{
|
||||||
...textBaseStyle,
|
...textBaseStyle,
|
||||||
left: `${textLayout.leftPx}px`,
|
left: `${resolvedLeftPx}px`,
|
||||||
width: textLayout.widthPx !== undefined ? `${textLayout.widthPx}px` : 'max-content',
|
width: resolvedWidthPx !== undefined ? `${resolvedWidthPx}px` : 'max-content',
|
||||||
fontSize: `${fontSize * scale}px`,
|
fontSize: `${fontSize * scale}px`,
|
||||||
fontFamily: getFontFamily(obj.fontFamily),
|
fontFamily: getFontFamily(obj.fontFamily),
|
||||||
fontWeight: getCssFontWeight(obj.fontWeight),
|
fontWeight: getCssFontWeight(obj.fontWeight),
|
||||||
@@ -214,10 +241,11 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
textAlign: textLayout.textAlign,
|
textAlign: textLayout.textAlign,
|
||||||
direction: 'rtl',
|
direction: 'rtl',
|
||||||
color: getColorWithOpacity(obj.fill, obj.opacity),
|
color: getColorWithOpacity(obj.fill, obj.opacity),
|
||||||
whiteSpace: wrapped ? 'pre-wrap' : 'nowrap',
|
whiteSpace: finalWrapped ? 'pre-wrap' : 'nowrap',
|
||||||
overflowWrap: wrapped ? 'break-word' : 'normal',
|
overflowWrap: finalWrapped ? 'break-word' : 'normal',
|
||||||
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
|
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
|
||||||
boxSizing: 'content-box',
|
boxSizing: 'content-box',
|
||||||
|
zIndex: index,
|
||||||
transform: textLayout.transform,
|
transform: textLayout.transform,
|
||||||
transformOrigin: textLayout.transformOrigin,
|
transformOrigin: textLayout.transformOrigin,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user