@@ -4,7 +4,7 @@ import { AppState, BinaryFileData, Collaborator, DataURL, ExcalidrawInitialDataS
44import { isInitializedImageElement , throttleRAF , measureText , getFontString , isTextElement } from '~/other/excalidraw' ;
55import { ClientData , ClientToServerEvents , SceneBroadcastData , ServerToClientEvents , StatsData } from '~/other/types' ;
66import { CollabUser , BoardsManager } from '@excali-boards/boards-api-client' ;
7- import { BoardExcalidrawState , BoardProps } from './types' ;
7+ import { BoardExcalidrawState , BoardProps , PreparedElement } from './types' ;
88import { Component , ContextType , Suspense } from 'react' ;
99import { PresenceContext } from '~/components/Context' ;
1010import { Box , Flex , Spinner } from '@chakra-ui/react' ;
@@ -753,17 +753,16 @@ export class ExcalidrawBoard extends Component<BoardProps, BoardExcalidrawState>
753753 if ( selectedElementIds . length === 0 ) return ;
754754
755755 const gapPx = 15 ;
756-
756+ const rowTolerance = 5 ;
757757 const shouldSnap : boolean = ! ! appState . gridModeEnabled ;
758758 const snapUnit : number = appState . gridSize ?? appState . gridStep ?? 10 ;
759759 const snap = ( v : number ) : number => ( shouldSnap ? Math . round ( v / snapUnit ) * snapUnit : v ) ;
760760
761761 const elements = api . getSceneElementsIncludingDeleted ( ) ;
762-
763762 const isSelected = ( id : string ) : boolean => selectedElementIds . includes ( id ) ;
764763 const isNonDeletedSelectedText = ( el : OrderedExcalidrawElement ) : el is Ordered < ExcalidrawTextElement > => isSelected ( el . id ) && ! el . isDeleted && isTextElement ( el ) ;
765764
766- const prepared = elements . filter ( isNonDeletedSelectedText ) . map ( ( el ) => {
765+ const prepared : PreparedElement [ ] = elements . filter ( isNonDeletedSelectedText ) . map ( ( el ) => {
767766 const normalizedText : string = ( el . originalText ?? el . text ?? '' ) . replace ( / \s * \n \s * / g, ' ' ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
768767 const metrics = measureText ( normalizedText , getFontString ( el ) , el . lineHeight ) ;
769768
@@ -772,31 +771,65 @@ export class ExcalidrawBoard extends Component<BoardProps, BoardExcalidrawState>
772771 x : el . x ,
773772 y : el . y ,
774773 height : metrics . height ,
774+ width : metrics . width ,
775775 patch : {
776- autoResize : true ,
776+ autoResize : true as const ,
777777 text : normalizedText ,
778778 originalText : normalizedText ,
779779 width : metrics . width ,
780780 height : metrics . height ,
781781 } ,
782782 } ;
783- } ) . sort ( ( a , b ) => ( a . y - b . y ) || ( a . x - b . x ) ) ;
784-
783+ } ) . sort ( ( a , b ) => a . y - b . y || a . x - b . x ) ;
785784 if ( prepared . length === 0 ) return ;
786785
787- const minX = Math . min ( ...prepared . map ( ( p ) => p . x ) ) ;
788- const minY = Math . min ( ...prepared . map ( ( p ) => p . y ) ) ;
789- const targetX = snap ( minX ) ;
790- const startY = snap ( minY ) ;
786+ const rows : PreparedElement [ ] [ ] = [ ] ;
787+ for ( let i = 0 ; i < prepared . length ; i ++ ) {
788+ const current : PreparedElement = prepared [ i ] ! ;
789+ if ( rows . length === 0 ) rows . push ( [ current ] ) ;
790+ else {
791+ const lastRow : PreparedElement [ ] = rows [ rows . length - 1 ] ! ;
792+ const firstInRow : PreparedElement = lastRow [ 0 ] ! ;
793+ const lastRowY : number = firstInRow . y ;
794+
795+ if ( Math . abs ( current . y - lastRowY ) <= rowTolerance ) lastRow . push ( current ) ;
796+ else rows . push ( [ current ] ) ;
797+ }
798+ }
799+
800+ const minX : number = Math . min ( ...prepared . map ( ( p ) => p . x ) ) ;
801+ const minY : number = Math . min ( ...prepared . map ( ( p ) => p . y ) ) ;
802+ const targetX : number = snap ( minX ) ;
803+ let currentY : number = snap ( minY ) ;
804+
805+ const updatesById = new Map < string , Partial < ExcalidrawTextElement > > ( ) ;
791806
792- const maxHeight = Math . max ( ...prepared . map ( ( p ) => p . height ) ) ;
793- const step = snap ( maxHeight + gapPx ) ;
807+ for ( let i = 0 ; i < rows . length ; i ++ ) {
808+ const row : PreparedElement [ ] = rows [ i ] ! ;
809+ const rowMaxHeight : number = Math . max ( ...row . map ( ( p ) => p . height ) ) ;
794810
795- const updatesById = new Map ( prepared . map ( ( p , i ) => [ p . id , {
796- ...p . patch ,
797- x : targetX ,
798- y : startY + i * step ,
799- } ] ) ) ;
811+ if ( row . length === 1 ) {
812+ const single : PreparedElement = row [ 0 ] ! ;
813+ updatesById . set ( single . id , {
814+ ...single . patch ,
815+ x : targetX ,
816+ y : currentY ,
817+ } ) ;
818+ } else {
819+ let currentX : number = targetX ;
820+ for ( let j = 0 ; j < row . length ; j ++ ) {
821+ const item : PreparedElement = row [ j ] ! ;
822+ updatesById . set ( item . id , {
823+ ...item . patch ,
824+ x : currentX ,
825+ y : currentY ,
826+ } ) ;
827+ currentX = snap ( currentX + item . width + gapPx ) ;
828+ }
829+ }
830+
831+ currentY = snap ( currentY + rowMaxHeight + gapPx ) ;
832+ }
800833
801834 api . updateScene ( {
802835 elements : elements . map ( ( el ) => {
@@ -805,8 +838,15 @@ export class ExcalidrawBoard extends Component<BoardProps, BoardExcalidrawState>
805838 } ) ,
806839 } ) ;
807840
841+ const rowCount : number = rows . length ;
842+ const multiRowCount : number = rows . filter ( ( r ) => r . length > 1 ) . length ;
843+ const message : string =
844+ multiRowCount > 0
845+ ? `Tidied ${ prepared . length } elements (${ rowCount } rows, ${ multiRowCount } horizontal).`
846+ : `Tidied ${ prepared . length } text element${ prepared . length > 1 ? 's' : '' } .` ;
847+
808848 api . setToast ( {
809- message : `Tidied ${ prepared . length } text element ${ prepared . length > 1 ? 's' : '' } .` ,
849+ message,
810850 closable : true ,
811851 duration : 1000 ,
812852 } ) ;
0 commit comments