@ -1,12 +1,7 @@
import { MIN_FONT_SIZE , SHIFT_LOCKING_ANGLE } from "../constants" ;
import { rescalePoints } from "../points" ;
import {
rotate ,
adjustXYWithRotation ,
centerPoint ,
rotatePoint ,
} from "../math" ;
import { rotate , centerPoint , rotatePoint } from "../math" ;
import {
ExcalidrawLinearElement ,
ExcalidrawTextElement ,
@ -23,7 +18,6 @@ import {
getCommonBounds ,
getResizedElementAbsoluteCoords ,
getCommonBoundingBox ,
getElementPointsCoords ,
} from "./bounds" ;
import {
isArrowElement ,
@ -38,7 +32,6 @@ import { mutateElement } from "./mutateElement";
import { getFontString } from "../utils" ;
import { updateBoundElements } from "./binding" ;
import {
TransformHandleType ,
MaybeTransformHandleType ,
TransformHandleDirection ,
} from "./transformHandles" ;
@ -54,6 +47,7 @@ import {
getApproxMinLineHeight ,
} from "./textElement" ;
import { LinearElementEditor } from "./linearElementEditor" ;
import { isInGroup } from "../groups" ;
export const normalizeAngle = ( angle : number ) : number = > {
if ( angle < 0 ) {
@ -133,18 +127,14 @@ export const transformElements = (
centerY ,
) ;
return true ;
} else if (
transformHandleType === "nw" ||
transformHandleType === "ne" ||
transformHandleType === "sw" ||
transformHandleType === "se"
) {
} else if ( transformHandleType ) {
resizeMultipleElements (
originalElements ,
selectedElements ,
elementsMap ,
transformHandleType ,
shouldResizeFromCenter ,
shouldMaintainAspectRatio ,
pointerX ,
pointerY ,
) ;
@ -232,26 +222,6 @@ const measureFontSizeFromWidth = (
} ;
} ;
const getSidesForTransformHandle = (
transformHandleType : TransformHandleType ,
shouldResizeFromCenter : boolean ,
) = > {
return {
n :
/^(n|ne|nw)$/ . test ( transformHandleType ) ||
( shouldResizeFromCenter && /^(s|se|sw)$/ . test ( transformHandleType ) ) ,
s :
/^(s|se|sw)$/ . test ( transformHandleType ) ||
( shouldResizeFromCenter && /^(n|ne|nw)$/ . test ( transformHandleType ) ) ,
w :
/^(w|nw|sw)$/ . test ( transformHandleType ) ||
( shouldResizeFromCenter && /^(e|ne|se)$/ . test ( transformHandleType ) ) ,
e :
/^(e|ne|se)$/ . test ( transformHandleType ) ||
( shouldResizeFromCenter && /^(w|nw|sw)$/ . test ( transformHandleType ) ) ,
} ;
} ;
const resizeSingleTextElement = (
element : NonDeleted < ExcalidrawTextElement > ,
elementsMap : ElementsMap ,
@ -260,9 +230,10 @@ const resizeSingleTextElement = (
pointerX : number ,
pointerY : number ,
) = > {
const [ x1 , y1 , x2 , y2 ] = getElementAbsoluteCoords ( element , elementsMap ) ;
const cx = ( x1 + x2 ) / 2 ;
const cy = ( y1 + y2 ) / 2 ;
const [ x1 , y1 , x2 , y2 , cx , cy ] = getElementAbsoluteCoords (
element ,
elementsMap ,
) ;
// rotation pointer with reverse angle
const [ rotatedX , rotatedY ] = rotate (
pointerX ,
@ -271,33 +242,24 @@ const resizeSingleTextElement = (
cy ,
- element . angle ,
) ;
let scale : number ;
switch ( transformHandleType ) {
case "se" :
scale = Math . max (
( rotatedX - x1 ) / ( x2 - x1 ) ,
( rotatedY - y1 ) / ( y2 - y1 ) ,
) ;
break ;
case "nw" :
scale = Math . max (
( x2 - rotatedX ) / ( x2 - x1 ) ,
( y2 - rotatedY ) / ( y2 - y1 ) ,
) ;
break ;
case "ne" :
scale = Math . max (
( rotatedX - x1 ) / ( x2 - x1 ) ,
( y2 - rotatedY ) / ( y2 - y1 ) ,
) ;
break ;
case "sw" :
scale = Math . max (
( x2 - rotatedX ) / ( x2 - x1 ) ,
( rotatedY - y1 ) / ( y2 - y1 ) ,
) ;
break ;
let scaleX = 0 ;
let scaleY = 0 ;
if ( transformHandleType . includes ( "e" ) ) {
scaleX = ( rotatedX - x1 ) / ( x2 - x1 ) ;
}
if ( transformHandleType . includes ( "w" ) ) {
scaleX = ( x2 - rotatedX ) / ( x2 - x1 ) ;
}
if ( transformHandleType . includes ( "n" ) ) {
scaleY = ( y2 - rotatedY ) / ( y2 - y1 ) ;
}
if ( transformHandleType . includes ( "s" ) ) {
scaleY = ( rotatedY - y1 ) / ( y2 - y1 ) ;
}
const scale = Math . max ( scaleX , scaleY ) ;
if ( scale > 0 ) {
const nextWidth = element . width * scale ;
const nextHeight = element . height * scale ;
@ -305,32 +267,55 @@ const resizeSingleTextElement = (
if ( metrics === null ) {
return ;
}
const [ nextX1 , nextY1 , nextX2 , nextY2 ] = getResizedElementAbsoluteCoords (
element ,
nextWidth ,
nextHeight ,
false ,
) ;
const deltaX1 = ( x1 - nextX1 ) / 2 ;
const deltaY1 = ( y1 - nextY1 ) / 2 ;
const deltaX2 = ( x2 - nextX2 ) / 2 ;
const deltaY2 = ( y2 - nextY2 ) / 2 ;
const [ nextElementX , nextElementY ] = adjustXYWithRotation (
getSidesForTransformHandle ( transformHandleType , shouldResizeFromCenter ) ,
element . x ,
element . y ,
element . angle ,
deltaX1 ,
deltaY1 ,
deltaX2 ,
deltaY2 ,
) ;
const startTopLeft = [ x1 , y1 ] ;
const startBottomRight = [ x2 , y2 ] ;
const startCenter = [ cx , cy ] ;
let newTopLeft = [ x1 , y1 ] as [ number , number ] ;
if ( [ "n" , "w" , "nw" ] . includes ( transformHandleType ) ) {
newTopLeft = [
startBottomRight [ 0 ] - Math . abs ( nextWidth ) ,
startBottomRight [ 1 ] - Math . abs ( nextHeight ) ,
] ;
}
if ( transformHandleType === "ne" ) {
const bottomLeft = [ startTopLeft [ 0 ] , startBottomRight [ 1 ] ] ;
newTopLeft = [ bottomLeft [ 0 ] , bottomLeft [ 1 ] - Math . abs ( nextHeight ) ] ;
}
if ( transformHandleType === "sw" ) {
const topRight = [ startBottomRight [ 0 ] , startTopLeft [ 1 ] ] ;
newTopLeft = [ topRight [ 0 ] - Math . abs ( nextWidth ) , topRight [ 1 ] ] ;
}
if ( [ "s" , "n" ] . includes ( transformHandleType ) ) {
newTopLeft [ 0 ] = startCenter [ 0 ] - nextWidth / 2 ;
}
if ( [ "e" , "w" ] . includes ( transformHandleType ) ) {
newTopLeft [ 1 ] = startCenter [ 1 ] - nextHeight / 2 ;
}
if ( shouldResizeFromCenter ) {
newTopLeft [ 0 ] = startCenter [ 0 ] - Math . abs ( nextWidth ) / 2 ;
newTopLeft [ 1 ] = startCenter [ 1 ] - Math . abs ( nextHeight ) / 2 ;
}
const angle = element . angle ;
const rotatedTopLeft = rotatePoint ( newTopLeft , [ cx , cy ] , angle ) ;
const newCenter : Point = [
newTopLeft [ 0 ] + Math . abs ( nextWidth ) / 2 ,
newTopLeft [ 1 ] + Math . abs ( nextHeight ) / 2 ,
] ;
const rotatedNewCenter = rotatePoint ( newCenter , [ cx , cy ] , angle ) ;
newTopLeft = rotatePoint ( rotatedTopLeft , rotatedNewCenter , - angle ) ;
const [ nextX , nextY ] = newTopLeft ;
mutateElement ( element , {
fontSize : metrics.size ,
width : nextWidth ,
height : nextHeight ,
x : nextElementX ,
y : nextElementY ,
x : next X,
y : next Y,
} ) ;
}
} ;
@ -636,8 +621,9 @@ export const resizeMultipleElements = (
originalElements : PointerDownState [ "originalElements" ] ,
selectedElements : readonly NonDeletedExcalidrawElement [ ] ,
elementsMap : ElementsMap ,
transformHandleType : "nw" | "ne" | "sw" | "se" ,
transformHandleType : TransformHandleDirection ,
shouldResizeFromCenter : boolean ,
shouldMaintainAspectRatio : boolean ,
pointerX : number ,
pointerY : number ,
) = > {
@ -691,43 +677,80 @@ export const resizeMultipleElements = (
const { minX , minY , maxX , maxY , midX , midY } = getCommonBoundingBox (
targetElements . map ( ( { orig } ) = > orig ) . concat ( boundTextElements ) ,
) ;
// const originalHeight = maxY - minY;
// const originalWidth = maxX - minX;
const width = maxX - minX ;
const height = maxY - minY ;
const direction = transformHandleType ;
const mapDirectionsToAnchors: Record < typeof d irection, Point > = {
const anchorsMap: Record < TransformHandleD irection, Point > = {
ne : [ minX , maxY ] ,
se : [ minX , minY ] ,
sw : [ maxX , minY ] ,
nw : [ maxX , maxY ] ,
e : [ minX , minY + height / 2 ] ,
w : [ maxX , minY + height / 2 ] ,
n : [ minX + width / 2 , maxY ] ,
s : [ minX + width / 2 , minY ] ,
} ;
// anchor point must be on the opposite side of the dragged selection handle
// or be the center of the selection if shouldResizeFromCenter
const [ anchorX , anchorY ] : Point = shouldResizeFromCenter
? [ midX , midY ]
: mapDirectionsToAnchors [ direction ] ;
: anchorsMap [ direction ] ;
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1 ;
const scale =
Math . max (
Math . abs ( pointerX - anchorX ) / ( maxX - minX ) || 0 ,
Math . abs ( pointerY - anchorY ) / ( maxY - minY ) || 0 ,
) * ( shouldResizeFromCenter ? 2 : 1 ) ;
Math . abs ( pointerX - anchorX ) / width || 0 ,
Math . abs ( pointerY - anchorY ) / height || 0 ,
) * resizeFromCenterScale ;
if ( scale === 0 ) {
return ;
}
const mapDirectionsToPointerPositions : Record <
typeof direction ,
let scaleX =
direction . includes ( "e" ) || direction . includes ( "w" )
? ( Math . abs ( pointerX - anchorX ) / width ) * resizeFromCenterScale
: 1 ;
let scaleY =
direction . includes ( "n" ) || direction . includes ( "s" )
? ( Math . abs ( pointerY - anchorY ) / height ) * resizeFromCenterScale
: 1 ;
const keepAspectRatio =
shouldMaintainAspectRatio ||
targetElements . some (
( item ) = >
item . latest . angle !== 0 ||
isTextElement ( item . latest ) ||
isInGroup ( item . latest ) ,
) ;
if ( keepAspectRatio ) {
scaleX = scale ;
scaleY = scale ;
}
const flipConditionsMap : Record <
TransformHandleDirection ,
// Condition for which we should flip or not flip the selected elements
// - when evaluated to `true`, we flip
// - therefore, setting it to always `false` means we do not flip (in that direction) at all
[ x : boolean , y : boolean ]
> = {
ne : [ pointerX >= anchorX , pointerY <= anchorY ] ,
se : [ pointerX >= anchorX , pointerY >= anchorY ] ,
sw : [ pointerX <= anchorX , pointerY >= anchorY ] ,
nw : [ pointerX <= anchorX , pointerY <= anchorY ] ,
ne : [ pointerX < anchorX , pointerY > anchorY ] ,
se : [ pointerX < anchorX , pointerY < anchorY ] ,
sw : [ pointerX > anchorX , pointerY < anchorY ] ,
nw : [ pointerX > anchorX , pointerY > anchorY ] ,
// e.g. when resizing from the "e" side, we do not need to consider changes in the `y` direction
// and therefore, we do not need to flip in the `y` direction at all
e : [ pointerX < anchorX , false ] ,
w : [ pointerX > anchorX , false ] ,
n : [ false , pointerY > anchorY ] ,
s : [ false , pointerY < anchorY ] ,
} ;
/ * *
@ -738,9 +761,9 @@ export const resizeMultipleElements = (
* mirror points in the case of linear & freedraw elemenets
* 3 . adjust element angle
* /
const [ flipFactorX , flipFactorY ] = mapDirectionsToPointerPositions[
direction
]. map ( ( condition ) = > ( condition ? 1 : - 1 ) );
const [ flipFactorX , flipFactorY ] = flipConditionsMap[ direction ] . map (
( condition ) = > ( condition ? - 1 : 1 ) ,
);
const isFlippedByX = flipFactorX < 0 ;
const isFlippedByY = flipFactorY < 0 ;
@ -762,8 +785,8 @@ export const resizeMultipleElements = (
continue ;
}
const width = orig . width * scale ;
const height = orig . height * scale ;
const width = orig . width * scale X ;
const height = orig . height * scale Y ;
const angle = normalizeAngle ( orig . angle * flipFactorX * flipFactorY ) ;
const isLinearOrFreeDraw = isLinearElement ( orig ) || isFreeDrawElement ( orig ) ;
@ -771,8 +794,8 @@ export const resizeMultipleElements = (
const offsetY = orig . y - anchorY ;
const shiftX = isFlippedByX && ! isLinearOrFreeDraw ? width : 0 ;
const shiftY = isFlippedByY && ! isLinearOrFreeDraw ? height : 0 ;
const x = anchorX + flipFactorX * ( offsetX * scale + shiftX ) ;
const y = anchorY + flipFactorY * ( offsetY * scale + shiftY ) ;
const x = anchorX + flipFactorX * ( offsetX * scale X + shiftX ) ;
const y = anchorY + flipFactorY * ( offsetY * scale Y + shiftY ) ;
const rescaledPoints = rescalePointsInElement (
orig ,
@ -790,40 +813,10 @@ export const resizeMultipleElements = (
. . . rescaledPoints ,
} ;
if ( isImageElement ( orig ) && targetElements . length === 1 ) {
if ( isImageElement ( orig ) ) {
update . scale = [ orig . scale [ 0 ] * flipFactorX , orig . scale [ 1 ] * flipFactorY ] ;
}
if ( isLinearElement ( orig ) && ( isFlippedByX || isFlippedByY ) ) {
const origBounds = getElementPointsCoords ( orig , orig . points ) ;
const newBounds = getElementPointsCoords (
{ . . . orig , x , y } ,
rescaledPoints . points ! ,
) ;
const origXY = [ orig . x , orig . y ] ;
const newXY = [ x , y ] ;
const linearShift = ( axis : "x" | "y" ) = > {
const i = axis === "x" ? 0 : 1 ;
return (
( newBounds [ i + 2 ] -
newXY [ i ] -
( origXY [ i ] - origBounds [ i ] ) * scale +
( origBounds [ i + 2 ] - origXY [ i ] ) * scale -
( newXY [ i ] - newBounds [ i ] ) ) /
2
) ;
} ;
if ( isFlippedByX ) {
update . x -= linearShift ( "x" ) ;
}
if ( isFlippedByY ) {
update . y -= linearShift ( "y" ) ;
}
}
if ( isTextElement ( orig ) ) {
const metrics = measureFontSizeFromWidth ( orig , elementsMap , width ) ;
if ( ! metrics ) {
@ -837,11 +830,15 @@ export const resizeMultipleElements = (
) as ExcalidrawTextElementWithContainer | undefined ;
if ( boundTextElement ) {
if ( keepAspectRatio ) {
const newFontSize = boundTextElement . fontSize * scale ;
if ( newFontSize < MIN_FONT_SIZE ) {
return ;
}
update . boundTextFontSize = newFontSize ;
} else {
update . boundTextFontSize = boundTextElement . fontSize ;
}
}
elementsAndUpdates . push ( {