manipulate and update a crop
This commit is contained in:
parent
71ed96eabb
commit
997fec6c75
@ -441,7 +441,14 @@ import {
|
|||||||
getLinkDirectionFromKey,
|
getLinkDirectionFromKey,
|
||||||
} from "../element/flowchart";
|
} from "../element/flowchart";
|
||||||
import type { LocalPoint, Radians } from "../../math";
|
import type { LocalPoint, Radians } from "../../math";
|
||||||
import { point, pointDistance, vector } from "../../math";
|
import {
|
||||||
|
clamp,
|
||||||
|
point,
|
||||||
|
pointDistance,
|
||||||
|
pointRotateRads,
|
||||||
|
vector,
|
||||||
|
} from "../../math";
|
||||||
|
import { cropElement } from "../element/cropElement";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@ -584,6 +591,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
|
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
|
||||||
null;
|
null;
|
||||||
lastPointerMoveEvent: PointerEvent | null = null;
|
lastPointerMoveEvent: PointerEvent | null = null;
|
||||||
|
lastPointerMoveCoords: { x: number; y: number } | null = null;
|
||||||
lastViewportPosition = { x: 0, y: 0 };
|
lastViewportPosition = { x: 0, y: 0 };
|
||||||
|
|
||||||
animationFrameHandler = new AnimationFrameHandler();
|
animationFrameHandler = new AnimationFrameHandler();
|
||||||
@ -3862,6 +3870,28 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isInputLike(event.target)) {
|
if (!isInputLike(event.target)) {
|
||||||
|
if (
|
||||||
|
(event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
|
||||||
|
this.state.croppingElement
|
||||||
|
) {
|
||||||
|
this.finishImageCropping();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
isImageElement(selectedElements[0]) &&
|
||||||
|
event.key === KEYS.ENTER
|
||||||
|
) {
|
||||||
|
this.startImageCropping(selectedElements[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.key === KEYS.ESCAPE &&
|
event.key === KEYS.ESCAPE &&
|
||||||
this.flowChartCreator.isCreatingChart
|
this.flowChartCreator.isCreatingChart
|
||||||
@ -6560,6 +6590,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
arrowDirection: "origin",
|
arrowDirection: "origin",
|
||||||
center: { x: (maxX + minX) / 2, y: (maxY + minY) / 2 },
|
center: { x: (maxX + minX) / 2, y: (maxY + minY) / 2 },
|
||||||
},
|
},
|
||||||
|
crop: {
|
||||||
|
handleType: false,
|
||||||
|
isCropping: false,
|
||||||
|
offset: { x: 0, y: 0 },
|
||||||
|
complete: false,
|
||||||
|
},
|
||||||
hit: {
|
hit: {
|
||||||
element: null,
|
element: null,
|
||||||
allHitElements: [],
|
allHitElements: [],
|
||||||
@ -6671,11 +6707,28 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.device,
|
this.device,
|
||||||
);
|
);
|
||||||
if (elementWithTransformHandleType != null) {
|
if (elementWithTransformHandleType != null) {
|
||||||
this.setState({
|
if (
|
||||||
resizingElement: elementWithTransformHandleType.element,
|
elementWithTransformHandleType.transformHandleType === "rotation"
|
||||||
});
|
) {
|
||||||
pointerDownState.resize.handleType =
|
this.setState({
|
||||||
elementWithTransformHandleType.transformHandleType;
|
resizingElement: elementWithTransformHandleType.element,
|
||||||
|
});
|
||||||
|
pointerDownState.resize.handleType =
|
||||||
|
elementWithTransformHandleType.transformHandleType;
|
||||||
|
} else if (this.state.croppingElement) {
|
||||||
|
this.setState({
|
||||||
|
croppingElement:
|
||||||
|
elementWithTransformHandleType.element as ExcalidrawImageElement,
|
||||||
|
});
|
||||||
|
pointerDownState.crop.handleType =
|
||||||
|
elementWithTransformHandleType.transformHandleType;
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
resizingElement: elementWithTransformHandleType.element,
|
||||||
|
});
|
||||||
|
pointerDownState.resize.handleType =
|
||||||
|
elementWithTransformHandleType.transformHandleType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (selectedElements.length > 1) {
|
} else if (selectedElements.length > 1) {
|
||||||
pointerDownState.resize.handleType = getTransformHandleTypeFromCoords(
|
pointerDownState.resize.handleType = getTransformHandleTypeFromCoords(
|
||||||
@ -6708,6 +6761,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
selectedElements[0],
|
selectedElements[0],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (pointerDownState.crop.handleType) {
|
||||||
|
pointerDownState.crop.isCropping = true;
|
||||||
|
pointerDownState.crop.offset = tupleToCoors(
|
||||||
|
getResizeOffsetXY(
|
||||||
|
pointerDownState.crop.handleType,
|
||||||
|
selectedElements,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (this.state.selectedLinearElement) {
|
if (this.state.selectedLinearElement) {
|
||||||
const linearElementEditor =
|
const linearElementEditor =
|
||||||
@ -7604,6 +7668,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pointerDownState.crop.isCropping) {
|
||||||
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||||
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||||
|
if (this.maybeHandleCrop(pointerDownState, event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
if (this.state.selectedLinearElement) {
|
if (this.state.selectedLinearElement) {
|
||||||
@ -7773,6 +7844,65 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #region drag
|
||||||
|
if (
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
isImageElement(selectedElements[0]) &&
|
||||||
|
this.state.croppingElement?.id === selectedElements[0].id &&
|
||||||
|
selectedElements[0].crop !== null
|
||||||
|
) {
|
||||||
|
const crop = selectedElements[0].crop;
|
||||||
|
const image = selectedElements[0];
|
||||||
|
|
||||||
|
const lastPointerCoords =
|
||||||
|
this.lastPointerMoveCoords ?? pointerDownState.origin;
|
||||||
|
|
||||||
|
const instantDragOffset = {
|
||||||
|
x: pointerCoords.x - lastPointerCoords.x,
|
||||||
|
y: pointerCoords.y - lastPointerCoords.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
// current offset is based on the element's width and height
|
||||||
|
const uncroppedWidth = image.widthAtCreation * image.resizedFactorX;
|
||||||
|
const uncroppedHeight =
|
||||||
|
image.heightAtCreation * image.resizedFactorY;
|
||||||
|
|
||||||
|
const SENSITIVITY_FACTOR = 3;
|
||||||
|
|
||||||
|
const adjustedOffset = {
|
||||||
|
x:
|
||||||
|
instantDragOffset.x *
|
||||||
|
(uncroppedWidth / image.naturalWidth) *
|
||||||
|
SENSITIVITY_FACTOR,
|
||||||
|
y:
|
||||||
|
instantDragOffset.y *
|
||||||
|
(uncroppedHeight / image.naturalHeight) *
|
||||||
|
SENSITIVITY_FACTOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextCrop = {
|
||||||
|
...crop,
|
||||||
|
x: clamp(
|
||||||
|
crop.x - adjustedOffset.x,
|
||||||
|
0,
|
||||||
|
image.naturalWidth - crop.width,
|
||||||
|
),
|
||||||
|
y: clamp(
|
||||||
|
crop.y - adjustedOffset.y,
|
||||||
|
0,
|
||||||
|
image.naturalHeight - crop.height,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
mutateElement(image, {
|
||||||
|
crop: nextCrop,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastPointerMoveCoords = pointerCoords;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Snap cache *must* be synchronously popuplated before initial drag,
|
// Snap cache *must* be synchronously popuplated before initial drag,
|
||||||
// otherwise the first drag even will not snap, causing a jump before
|
// otherwise the first drag even will not snap, causing a jump before
|
||||||
// it snaps to its position if previously snapped already.
|
// it snaps to its position if previously snapped already.
|
||||||
@ -7906,6 +8036,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||||
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.lastPointerMoveCoords = pointerCoords;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8158,6 +8290,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
activeTool,
|
activeTool,
|
||||||
isResizing,
|
isResizing,
|
||||||
isRotating,
|
isRotating,
|
||||||
|
isCropping,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
@ -8172,6 +8305,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
originSnapOffset: null,
|
originSnapOffset: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.lastPointerMoveCoords = null;
|
||||||
|
|
||||||
SnapCache.setReferenceSnapPoints(null);
|
SnapCache.setReferenceSnapPoints(null);
|
||||||
SnapCache.setVisibleGaps(null);
|
SnapCache.setVisibleGaps(null);
|
||||||
|
|
||||||
@ -8654,6 +8789,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isCropping &&
|
||||||
|
!isResizing &&
|
||||||
|
((!hitElement && !pointerDownState.crop.isCropping) ||
|
||||||
|
(hitElement && hitElement !== this.state.croppingElement))
|
||||||
|
) {
|
||||||
|
this.finishImageCropping();
|
||||||
|
}
|
||||||
|
|
||||||
const pointerStart = this.lastPointerDownEvent;
|
const pointerStart = this.lastPointerDownEvent;
|
||||||
const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent;
|
const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent;
|
||||||
|
|
||||||
@ -8909,7 +9053,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.store.shouldCaptureIncrement();
|
this.store.shouldCaptureIncrement();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
|
if (
|
||||||
|
pointerDownState.drag.hasOccurred ||
|
||||||
|
isResizing ||
|
||||||
|
isRotating ||
|
||||||
|
isCropping
|
||||||
|
) {
|
||||||
// We only allow binding via linear elements, specifically via dragging
|
// We only allow binding via linear elements, specifically via dragging
|
||||||
// the endpoints ("start" or "end").
|
// the endpoints ("start" or "end").
|
||||||
const linearElements = this.scene
|
const linearElements = this.scene
|
||||||
@ -9324,7 +9473,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
||||||
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
||||||
|
|
||||||
mutateElement(imageElement, { x, y, width, height });
|
mutateElement(imageElement, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
widthAtCreation: width,
|
||||||
|
heightAtCreation: height,
|
||||||
|
naturalWidth: image.naturalWidth,
|
||||||
|
naturalHeight: image.naturalHeight,
|
||||||
|
resizedFactorX: 1,
|
||||||
|
resizedFactorY: 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -9863,6 +10023,46 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private maybeHandleCrop = (
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
event: MouseEvent | KeyboardEvent,
|
||||||
|
): boolean => {
|
||||||
|
if (pointerDownState.crop.complete) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedElements.length > 1) {
|
||||||
|
// don't see much sense in allowing multi-crop, that would be weird
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformHandleType = pointerDownState.crop.handleType;
|
||||||
|
const pointerCoords = pointerDownState.lastCoords;
|
||||||
|
const [x, y] = getGridPoint(
|
||||||
|
pointerCoords.x - pointerDownState.crop.offset.x,
|
||||||
|
pointerCoords.y - pointerDownState.crop.offset.y,
|
||||||
|
this.getEffectiveGridSize(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementToCrop = selectedElements[0] as ExcalidrawImageElement;
|
||||||
|
if (transformHandleType) {
|
||||||
|
cropElement(
|
||||||
|
elementToCrop,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
transformHandleType,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
private maybeHandleResize = (
|
private maybeHandleResize = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
event: MouseEvent | KeyboardEvent,
|
event: MouseEvent | KeyboardEvent,
|
||||||
|
292
packages/excalidraw/element/cropElement.ts
Normal file
292
packages/excalidraw/element/cropElement.ts
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
import { Point } from "points-on-curve";
|
||||||
|
import {
|
||||||
|
type Radians,
|
||||||
|
point,
|
||||||
|
pointCenter,
|
||||||
|
pointRotateRads,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorNormalize,
|
||||||
|
vectorSubtract,
|
||||||
|
vectorAdd,
|
||||||
|
vectorScale,
|
||||||
|
pointFromVector,
|
||||||
|
clamp,
|
||||||
|
} from "../../math";
|
||||||
|
import { updateBoundElements } from "./binding";
|
||||||
|
import { mutateElement } from "./mutateElement";
|
||||||
|
import { TransformHandleType } from "./transformHandles";
|
||||||
|
import {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawImageElement,
|
||||||
|
NonDeleted,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
} from "./types";
|
||||||
|
import {
|
||||||
|
getElementAbsoluteCoords,
|
||||||
|
getResizedElementAbsoluteCoords,
|
||||||
|
} from "./bounds";
|
||||||
|
|
||||||
|
// i split out these 'internal' functions so that this functionality can be easily unit tested
|
||||||
|
const cropElementInternal = (
|
||||||
|
element: ExcalidrawImageElement,
|
||||||
|
transformHandle: TransformHandleType,
|
||||||
|
pointerX: number,
|
||||||
|
pointerY: number,
|
||||||
|
) => {
|
||||||
|
const uncroppedWidth = element.widthAtCreation * element.resizedFactorX;
|
||||||
|
const uncroppedHeight = element.heightAtCreation * element.resizedFactorY;
|
||||||
|
|
||||||
|
const naturalWidthToUncropped = element.naturalWidth / uncroppedWidth;
|
||||||
|
const naturalHeightToUncropped = element.naturalHeight / uncroppedHeight;
|
||||||
|
|
||||||
|
const croppedLeft = (element.crop?.x ?? 0) / naturalWidthToUncropped;
|
||||||
|
const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uncropped width
|
||||||
|
* *––––––––––––––––––––––––*
|
||||||
|
* | (x,y) (natural) |
|
||||||
|
* | *–––––––* |
|
||||||
|
* | |///////| height | uncropped height
|
||||||
|
* | *–––––––* |
|
||||||
|
* | width (natural) |
|
||||||
|
* *––––––––––––––––––––––––*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const availableTopCropSpace = croppedTop;
|
||||||
|
const availableLeftCropSpace = croppedLeft;
|
||||||
|
|
||||||
|
const rotatedPointer = pointRotateRads(
|
||||||
|
point(pointerX, pointerY),
|
||||||
|
point(element.x + element.width / 2, element.y + element.height / 2),
|
||||||
|
-element.angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
pointerX = rotatedPointer[0];
|
||||||
|
pointerY = rotatedPointer[1];
|
||||||
|
|
||||||
|
let nextWidth = element.width;
|
||||||
|
let nextHeight = element.height;
|
||||||
|
const crop = element.crop ?? {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: element.naturalWidth,
|
||||||
|
height: element.naturalHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (transformHandle.includes("n")) {
|
||||||
|
const northBound = element.y - availableTopCropSpace;
|
||||||
|
const southBound = element.y + element.height;
|
||||||
|
|
||||||
|
pointerY = clamp(pointerY, northBound, southBound);
|
||||||
|
|
||||||
|
const pointerDeltaY = pointerY - element.y;
|
||||||
|
nextHeight = element.height - pointerDeltaY;
|
||||||
|
|
||||||
|
crop.y =
|
||||||
|
((pointerDeltaY + croppedTop) / uncroppedHeight) * element.naturalHeight;
|
||||||
|
crop.height = (nextHeight / uncroppedHeight) * element.naturalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformHandle.includes("s")) {
|
||||||
|
const northBound = element.y;
|
||||||
|
const southBound = element.y + (uncroppedHeight - croppedTop);
|
||||||
|
|
||||||
|
pointerY = clamp(pointerY, northBound, southBound);
|
||||||
|
|
||||||
|
nextHeight = pointerY - element.y;
|
||||||
|
crop.height = (nextHeight / uncroppedHeight) * element.naturalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformHandle.includes("w")) {
|
||||||
|
const eastBound = element.x + element.width;
|
||||||
|
const westBound = element.x - availableLeftCropSpace;
|
||||||
|
|
||||||
|
pointerX = clamp(pointerX, westBound, eastBound);
|
||||||
|
|
||||||
|
const pointerDeltaX = pointerX - element.x;
|
||||||
|
nextWidth = element.width - pointerDeltaX;
|
||||||
|
|
||||||
|
crop.x =
|
||||||
|
((pointerDeltaX + croppedLeft) / uncroppedWidth) * element.naturalWidth;
|
||||||
|
crop.width = (nextWidth / uncroppedWidth) * element.naturalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformHandle.includes("e")) {
|
||||||
|
const eastBound = element.x + (uncroppedWidth - croppedLeft);
|
||||||
|
const westBound = element.x;
|
||||||
|
|
||||||
|
pointerX = clamp(pointerX, westBound, eastBound);
|
||||||
|
|
||||||
|
nextWidth = pointerX - element.x;
|
||||||
|
crop.width = (nextWidth / uncroppedWidth) * element.naturalWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newOrigin = recomputeOrigin(
|
||||||
|
element,
|
||||||
|
transformHandle,
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: newOrigin[0],
|
||||||
|
y: newOrigin[1],
|
||||||
|
width: nextWidth,
|
||||||
|
height: nextHeight,
|
||||||
|
crop,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cropElement = (
|
||||||
|
element: ExcalidrawImageElement,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
transformHandle: TransformHandleType,
|
||||||
|
pointerX: number,
|
||||||
|
pointerY: number,
|
||||||
|
) => {
|
||||||
|
const mutation = cropElementInternal(
|
||||||
|
element,
|
||||||
|
transformHandle,
|
||||||
|
pointerX,
|
||||||
|
pointerY,
|
||||||
|
);
|
||||||
|
|
||||||
|
mutateElement(element, mutation);
|
||||||
|
|
||||||
|
updateBoundElements(element, elementsMap, {
|
||||||
|
oldSize: { width: element.width, height: element.height },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: replace with the refactored resizeSingleElement
|
||||||
|
const recomputeOrigin = (
|
||||||
|
stateAtCropStart: NonDeleted<ExcalidrawElement>,
|
||||||
|
transformHandle: TransformHandleType,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
) => {
|
||||||
|
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
|
||||||
|
stateAtCropStart,
|
||||||
|
stateAtCropStart.width,
|
||||||
|
stateAtCropStart.height,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const startTopLeft = point(x1, y1);
|
||||||
|
const startBottomRight = point(x2, y2);
|
||||||
|
const startCenter: any = pointCenter(startTopLeft, startBottomRight);
|
||||||
|
|
||||||
|
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
||||||
|
getResizedElementAbsoluteCoords(stateAtCropStart, width, height, true);
|
||||||
|
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
||||||
|
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
||||||
|
|
||||||
|
// Calculate new topLeft based on fixed corner during resize
|
||||||
|
let newTopLeft = [...startTopLeft] as [number, number];
|
||||||
|
|
||||||
|
if (["n", "w", "nw"].includes(transformHandle)) {
|
||||||
|
newTopLeft = [
|
||||||
|
startBottomRight[0] - Math.abs(newBoundsWidth),
|
||||||
|
startBottomRight[1] - Math.abs(newBoundsHeight),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (transformHandle === "ne") {
|
||||||
|
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
||||||
|
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
|
||||||
|
}
|
||||||
|
if (transformHandle === "sw") {
|
||||||
|
const topRight = [startBottomRight[0], startTopLeft[1]];
|
||||||
|
newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust topLeft to new rotation point
|
||||||
|
const angle = stateAtCropStart.angle;
|
||||||
|
const rotatedTopLeft = pointRotateRads(newTopLeft, startCenter, angle);
|
||||||
|
const newCenter: Point = [
|
||||||
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||||
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||||
|
];
|
||||||
|
const rotatedNewCenter = pointRotateRads(newCenter, startCenter, angle);
|
||||||
|
newTopLeft = pointRotateRads(
|
||||||
|
rotatedTopLeft,
|
||||||
|
rotatedNewCenter,
|
||||||
|
-angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newOrigin = [...newTopLeft];
|
||||||
|
newOrigin[0] += stateAtCropStart.x - newBoundsX1;
|
||||||
|
newOrigin[1] += stateAtCropStart.y - newBoundsY1;
|
||||||
|
|
||||||
|
return newOrigin;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUncroppedImageElement = (
|
||||||
|
image: ExcalidrawImageElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
|
if (image.crop) {
|
||||||
|
const width = image.widthAtCreation * image.resizedFactorX;
|
||||||
|
const height = image.heightAtCreation * image.resizedFactorY;
|
||||||
|
|
||||||
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
|
image,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const topLeftVector = vectorFromPoint(
|
||||||
|
pointRotateRads(point(x1, y1), point(cx, cy), image.angle),
|
||||||
|
);
|
||||||
|
const topRightVector = vectorFromPoint(
|
||||||
|
pointRotateRads(point(x2, y1), point(cx, cy), image.angle),
|
||||||
|
);
|
||||||
|
const topEdgeNormalized = vectorNormalize(
|
||||||
|
vectorSubtract(topRightVector, topLeftVector),
|
||||||
|
);
|
||||||
|
const bottomLeftVector = vectorFromPoint(
|
||||||
|
pointRotateRads(point(x1, y2), point(cx, cy), image.angle),
|
||||||
|
);
|
||||||
|
const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector);
|
||||||
|
const leftEdgeNormalized = vectorNormalize(leftEdgeVector);
|
||||||
|
|
||||||
|
const rotatedTopLeft = vectorAdd(
|
||||||
|
vectorAdd(
|
||||||
|
topLeftVector,
|
||||||
|
vectorScale(
|
||||||
|
topEdgeNormalized,
|
||||||
|
(-image.crop.x * width) / image.naturalWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
vectorScale(
|
||||||
|
leftEdgeNormalized,
|
||||||
|
(-image.crop.y * height) / image.naturalHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const center = pointFromVector(
|
||||||
|
vectorAdd(
|
||||||
|
vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)),
|
||||||
|
vectorScale(leftEdgeNormalized, height / 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const unrotatedTopLeft = pointRotateRads(
|
||||||
|
pointFromVector(rotatedTopLeft),
|
||||||
|
center,
|
||||||
|
-image.angle as Radians,
|
||||||
|
);
|
||||||
|
|
||||||
|
const uncroppedElement: ExcalidrawImageElement = {
|
||||||
|
...image,
|
||||||
|
x: unrotatedTopLeft[0],
|
||||||
|
y: unrotatedTopLeft[1],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
crop: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return uncroppedElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
};
|
@ -696,6 +696,13 @@ export const resizeSingleElement = (
|
|||||||
points: rescaledPoints,
|
points: rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: this is not the best approach
|
||||||
|
updateInternalScale(
|
||||||
|
element,
|
||||||
|
eleNewWidth / element.width,
|
||||||
|
eleNewHeight / element.height,
|
||||||
|
);
|
||||||
|
|
||||||
if ("scale" in element && "scale" in stateAtResizeStart) {
|
if ("scale" in element && "scale" in stateAtResizeStart) {
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
scale: [
|
scale: [
|
||||||
@ -750,6 +757,36 @@ export const resizeSingleElement = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateInternalScale = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
scaleX: number,
|
||||||
|
scaleY: number,
|
||||||
|
) => {
|
||||||
|
if ("type" in element && element.type === "image") {
|
||||||
|
element = element as ExcalidrawImageElement;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the scales happen to be 0 (which is insanely unlikely), it will
|
||||||
|
// zero out the rolling multiplier and cause weird bugs with cropping.
|
||||||
|
// if zero is detected, just set the scales to an obnoxiously small number
|
||||||
|
if (scaleX === 0) {
|
||||||
|
scaleX = Number.EPSILON;
|
||||||
|
}
|
||||||
|
if (scaleY === 0) {
|
||||||
|
scaleY = Number.EPSILON;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleX = Math.abs(scaleX);
|
||||||
|
scaleY = Math.abs(scaleY);
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
resizedFactorX: element.resizedFactorX * scaleX,
|
||||||
|
resizedFactorY: element.resizedFactorY * scaleY,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const resizeMultipleElements = (
|
export const resizeMultipleElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user