1+ /* eslint-disable no-use-before-define, @typescript-eslint/no-use-before-define */
2+
13import { Point } from 'fabric'
24import { resizeShapeNode } from './shape-factory'
35import {
@@ -11,6 +13,7 @@ const TEXT_FRAME_FILL_EPSILON = 0.5
1113const MAX_HORIZONTAL_PADDING_PX = 12
1214const MAX_VERTICAL_PADDING_PX = 12
1315const MAX_PADDING_RATIO = 0.45
16+ const MAX_WIDTH_RESIZE_ITERATIONS = 8
1417const MAX_HEIGHT_RESIZE_ITERATIONS = 8
1518
1619type TextboxMeasurementState = {
@@ -45,10 +48,15 @@ export const applyShapeTextLayout = ({
4548 alignV,
4649 padding
4750} : ShapeLayoutInput ) : void => {
48- const safeWidth = Math . max ( MIN_TEXT_FRAME_SIZE , width )
4951 const normalizedPadding = normalizePadding ( {
5052 padding
5153 } )
54+ const minWidth = resolveRequiredShapeWidthForText ( {
55+ text,
56+ width,
57+ padding : normalizedPadding
58+ } )
59+ const safeWidth = Math . max ( MIN_TEXT_FRAME_SIZE , minWidth )
5260 const minHeight = resolveRequiredShapeHeightForText ( {
5361 text,
5462 width : safeWidth ,
@@ -246,6 +254,54 @@ export const resolveRequiredShapeHeightForText = ({
246254 return nextHeight
247255}
248256
257+ /**
258+ * Возвращает минимальную ширину shape, при которой текст не выходит за пределы текстового фрейма.
259+ */
260+ function resolveRequiredShapeWidthForText ( {
261+ text,
262+ width,
263+ padding
264+ } : {
265+ text : ShapeLayoutInput [ 'text' ]
266+ width : number
267+ padding : ShapePadding
268+ } ) : number {
269+ const safeWidth = Math . max ( MIN_TEXT_FRAME_SIZE , width )
270+ const normalizedPadding = normalizePadding ( {
271+ padding
272+ } )
273+ const currentFrameWidth = resolveTextFrameWidth ( {
274+ width : safeWidth ,
275+ padding : normalizedPadding
276+ } )
277+ const requiredFrameWidth = resolveRequiredTextFrameWidth ( {
278+ text,
279+ frameWidth : currentFrameWidth
280+ } )
281+
282+ if ( requiredFrameWidth <= currentFrameWidth + TEXT_FRAME_FILL_EPSILON ) {
283+ return safeWidth
284+ }
285+
286+ let nextWidth = safeWidth
287+
288+ for ( let iteration = 0 ; iteration < MAX_WIDTH_RESIZE_ITERATIONS ; iteration += 1 ) {
289+ const nextFrameWidth = resolveTextFrameWidth ( {
290+ width : nextWidth ,
291+ padding : normalizedPadding
292+ } )
293+
294+ if ( nextFrameWidth >= requiredFrameWidth - TEXT_FRAME_FILL_EPSILON ) {
295+ return nextWidth
296+ }
297+
298+ const missingWidth = requiredFrameWidth - nextFrameWidth
299+ nextWidth = Math . max ( nextWidth + missingWidth , nextWidth * 1.05 )
300+ }
301+
302+ return nextWidth
303+ }
304+
249305/**
250306 * Возвращает центр для размещения группы на канвасе.
251307 */
@@ -328,6 +384,30 @@ function createTextFrame({
328384 }
329385}
330386
387+ /**
388+ * Возвращает доступную ширину текстового фрейма для переданной ширины шейпа.
389+ */
390+ function resolveTextFrameWidth ( {
391+ width,
392+ padding
393+ } : {
394+ width : number
395+ padding : ShapePadding
396+ } ) : number {
397+ const leftPadding = resolvePaddingPixels ( {
398+ size : width ,
399+ ratio : padding . left ,
400+ axis : 'horizontal'
401+ } )
402+ const rightPadding = resolvePaddingPixels ( {
403+ size : width ,
404+ ratio : padding . right ,
405+ axis : 'horizontal'
406+ } )
407+
408+ return Math . max ( MIN_TEXT_FRAME_SIZE , width - leftPadding - rightPadding )
409+ }
410+
331411/**
332412 * Возвращает визуальную высоту textbox.
333413 */
@@ -384,6 +464,71 @@ function measureTextboxHeightForFrame({
384464 return measuredHeight
385465}
386466
467+ /**
468+ * Возвращает требуемую ширину текстового фрейма, если даже перенос по символам не устраняет горизонтальный overflow.
469+ */
470+ function resolveRequiredTextFrameWidth ( {
471+ text,
472+ frameWidth
473+ } : {
474+ text : ShapeLayoutInput [ 'text' ]
475+ frameWidth : number
476+ } ) : number {
477+ const safeFrameWidth = Math . max ( MIN_TEXT_FRAME_SIZE , frameWidth )
478+ const longestWordWidth = measureTextboxLongestLineWidthForFrame ( {
479+ text,
480+ frameWidth : safeFrameWidth ,
481+ splitByGrapheme : false
482+ } )
483+
484+ if ( longestWordWidth <= safeFrameWidth + TEXT_FRAME_FILL_EPSILON ) {
485+ return safeFrameWidth
486+ }
487+
488+ const longestGraphemeLineWidth = measureTextboxLongestLineWidthForFrame ( {
489+ text,
490+ frameWidth : safeFrameWidth ,
491+ splitByGrapheme : true
492+ } )
493+
494+ if ( longestGraphemeLineWidth <= safeFrameWidth + TEXT_FRAME_FILL_EPSILON ) {
495+ return safeFrameWidth
496+ }
497+
498+ return longestGraphemeLineWidth
499+ }
500+
501+ /**
502+ * Измеряет максимальную ширину строки textbox при заданной ширине фрейма и режиме переноса.
503+ */
504+ function measureTextboxLongestLineWidthForFrame ( {
505+ text,
506+ frameWidth,
507+ splitByGrapheme
508+ } : {
509+ text : ShapeLayoutInput [ 'text' ]
510+ frameWidth : number
511+ splitByGrapheme : boolean
512+ } ) : number {
513+ const previousState = captureTextboxMeasurementState ( { text } )
514+
515+ text . set ( {
516+ autoExpand : false ,
517+ width : Math . max ( MIN_TEXT_FRAME_SIZE , frameWidth ) ,
518+ splitByGrapheme
519+ } )
520+
521+ text . initDimensions ( )
522+ const longestLineWidth = getTextboxLongestLineWidth ( { text } )
523+
524+ restoreTextboxMeasurementState ( {
525+ text,
526+ state : previousState
527+ } )
528+
529+ return longestLineWidth
530+ }
531+
387532/**
388533 * Вычисляет верхнюю координату текста по вертикальному выравниванию.
389534 */
@@ -457,6 +602,57 @@ function resolveSplitByGraphemeForFrame({
457602 return shouldSplitByGrapheme
458603}
459604
605+ /**
606+ * Возвращает ширину самой длинной отрисованной строки textbox.
607+ */
608+ function getTextboxLongestLineWidth ( {
609+ text
610+ } : {
611+ text : ShapeLayoutInput [ 'text' ]
612+ } ) : number {
613+ const textbox = text as ShapeLayoutInput [ 'text' ] & {
614+ textLines ?: string [ ]
615+ }
616+
617+ if ( Array . isArray ( textbox . textLines ) && textbox . textLines . length > 0 ) {
618+ return measureLongestRenderedLineWidth ( {
619+ text,
620+ lineCount : textbox . textLines . length
621+ } )
622+ }
623+
624+ const rawText = textbox . text ?? ''
625+ const lineCount = Math . max ( rawText . split ( '\n' ) . length , 1 )
626+
627+ return measureLongestRenderedLineWidth ( {
628+ text,
629+ lineCount
630+ } )
631+ }
632+
633+ /**
634+ * Измеряет ширину самой длинной уже отрисованной строки textbox.
635+ */
636+ function measureLongestRenderedLineWidth ( {
637+ text,
638+ lineCount
639+ } : {
640+ text : ShapeLayoutInput [ 'text' ]
641+ lineCount : number
642+ } ) : number {
643+ let longestLineWidth = MIN_TEXT_FRAME_SIZE
644+
645+ for ( let lineIndex = 0 ; lineIndex < lineCount ; lineIndex += 1 ) {
646+ const lineWidth = text . getLineWidth ( lineIndex )
647+
648+ if ( lineWidth > longestLineWidth ) {
649+ longestLineWidth = lineWidth
650+ }
651+ }
652+
653+ return longestLineWidth
654+ }
655+
460656/**
461657 * Возвращает текущее состояние textbox для временных измерений.
462658 */
0 commit comments