mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: resize elements from the sides (#7855)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
6e5aeb112d
commit
88812e0386
19 changed files with 3913 additions and 3741 deletions
|
@ -90,6 +90,7 @@ import {
|
|||
EDITOR_LS_KEYS,
|
||||
isIOS,
|
||||
supportsResizeObserver,
|
||||
DEFAULT_COLLISION_THRESHOLD,
|
||||
} from "../constants";
|
||||
import { ExportedElements, exportCanvas, loadFromBlob } from "../data";
|
||||
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||
|
@ -1703,6 +1704,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
scale={window.devicePixelRatio}
|
||||
appState={this.state}
|
||||
device={this.device}
|
||||
renderInteractiveSceneCallback={
|
||||
this.renderInteractiveSceneCallback
|
||||
}
|
||||
|
@ -4528,7 +4530,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
shape: this.getElementShape(elementWithHighestZIndex),
|
||||
// when overlapping, we would like to be more precise
|
||||
// this also avoids the need to update past tests
|
||||
threshold: this.getHitThreshold() / 2,
|
||||
threshold: this.getElementHitThreshold() / 2,
|
||||
frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
|
||||
? this.frameNameBoundsCache.get(elementWithHighestZIndex)
|
||||
: null,
|
||||
|
@ -4591,8 +4593,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return elements;
|
||||
}
|
||||
|
||||
private getHitThreshold() {
|
||||
return 10 / this.state.zoom.value;
|
||||
private getElementHitThreshold() {
|
||||
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
|
||||
}
|
||||
|
||||
private hitElement(
|
||||
|
@ -4610,7 +4612,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const selectionShape = getSelectionBoxShape(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.getHitThreshold(),
|
||||
this.getElementHitThreshold(),
|
||||
);
|
||||
|
||||
return isPointInShape([x, y], selectionShape);
|
||||
|
@ -4631,7 +4633,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
y,
|
||||
element,
|
||||
shape: this.getElementShape(element),
|
||||
threshold: this.getHitThreshold(),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
frameNameBound: isFrameLikeElement(element)
|
||||
? this.frameNameBoundsCache.get(element)
|
||||
: null,
|
||||
|
@ -4663,7 +4665,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
y,
|
||||
element: elements[index],
|
||||
shape: this.getElementShape(elements[index]),
|
||||
threshold: this.getHitThreshold(),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
})
|
||||
) {
|
||||
hitElement = elements[index];
|
||||
|
@ -4916,7 +4918,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
y: sceneY,
|
||||
element: container,
|
||||
shape: this.getElementShape(container),
|
||||
threshold: this.getHitThreshold(),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
})
|
||||
) {
|
||||
const midPoint = getContainerCenter(
|
||||
|
@ -5331,24 +5333,41 @@ class App extends React.Component<AppProps, AppState> {
|
|||
!isOverScrollBar &&
|
||||
!this.state.editingLinearElement
|
||||
) {
|
||||
const elementWithTransformHandleType = getElementWithTransformHandleType(
|
||||
elements,
|
||||
this.state,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (
|
||||
elementWithTransformHandleType &&
|
||||
elementWithTransformHandleType.transformHandleType
|
||||
) {
|
||||
setCursor(
|
||||
this.interactiveCanvas,
|
||||
getCursorForResizingElement(elementWithTransformHandleType),
|
||||
// for linear elements, we'd like to prioritize point dragging over edge resizing
|
||||
// therefore, we update and check hovered point index first
|
||||
if (this.state.selectedLinearElement) {
|
||||
this.handleHoverSelectedLinearElement(
|
||||
this.state.selectedLinearElement,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.state.selectedLinearElement ||
|
||||
this.state.selectedLinearElement.hoverPointIndex === -1
|
||||
) {
|
||||
const elementWithTransformHandleType =
|
||||
getElementWithTransformHandleType(
|
||||
elements,
|
||||
this.state,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.device,
|
||||
);
|
||||
if (
|
||||
elementWithTransformHandleType &&
|
||||
elementWithTransformHandleType.transformHandleType
|
||||
) {
|
||||
setCursor(
|
||||
this.interactiveCanvas,
|
||||
getCursorForResizingElement(elementWithTransformHandleType),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (selectedElements.length > 1 && !isOverScrollBar) {
|
||||
const transformHandleType = getTransformHandleTypeFromCoords(
|
||||
|
@ -5357,6 +5376,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
scenePointerY,
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.device,
|
||||
);
|
||||
if (transformHandleType) {
|
||||
setCursor(
|
||||
|
@ -5509,7 +5529,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
const threshold = this.getHitThreshold();
|
||||
const threshold = this.getElementHitThreshold();
|
||||
const point = { ...pointerDownState.lastCoords };
|
||||
let samplingInterval = 0;
|
||||
while (samplingInterval <= distance) {
|
||||
|
@ -5606,7 +5626,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
} else {
|
||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
}
|
||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||
|
@ -6306,7 +6326,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
|
||||
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
||||
if (
|
||||
selectedElements.length === 1 &&
|
||||
!this.state.editingLinearElement &&
|
||||
!(
|
||||
this.state.selectedLinearElement &&
|
||||
this.state.selectedLinearElement.hoverPointIndex !== -1
|
||||
)
|
||||
) {
|
||||
const elementWithTransformHandleType =
|
||||
getElementWithTransformHandleType(
|
||||
elements,
|
||||
|
@ -6316,6 +6343,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.device,
|
||||
);
|
||||
if (elementWithTransformHandleType != null) {
|
||||
this.setState({
|
||||
|
@ -6331,6 +6359,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
pointerDownState.origin.y,
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.device,
|
||||
);
|
||||
}
|
||||
if (pointerDownState.resize.handleType) {
|
||||
|
@ -6587,7 +6616,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
// How many pixels off the shape boundary we still consider a hit
|
||||
const threshold = this.getHitThreshold();
|
||||
const threshold = this.getElementHitThreshold();
|
||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||
return (
|
||||
point.x > x1 - threshold &&
|
||||
|
@ -8412,7 +8441,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
y: pointerDownState.origin.y,
|
||||
element: hitElement,
|
||||
shape: this.getElementShape(hitElement),
|
||||
threshold: this.getHitThreshold(),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
frameNameBound: isFrameLikeElement(hitElement)
|
||||
? this.frameNameBoundsCache.get(hitElement)
|
||||
: null,
|
||||
|
@ -9525,7 +9554,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.scene.getElementsMapIncludingDeleted(),
|
||||
shouldRotateWithDiscreteAngle(event),
|
||||
shouldResizeFromCenter(event),
|
||||
selectedElements.length === 1 && isImageElement(selectedElements[0])
|
||||
selectedElements.some((element) => isImageElement(element))
|
||||
? !shouldMaintainAspectRatio(event)
|
||||
: shouldMaintainAspectRatio(event),
|
||||
resizeX,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils";
|
|||
import { CURSOR_TYPE } from "../../constants";
|
||||
import { t } from "../../i18n";
|
||||
import type { DOMAttributes } from "react";
|
||||
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
||||
import type { AppState, Device, InteractiveCanvasAppState } from "../../types";
|
||||
import type {
|
||||
InteractiveCanvasRenderConfig,
|
||||
RenderableElementsMap,
|
||||
|
@ -23,6 +23,7 @@ type InteractiveCanvasProps = {
|
|||
selectionNonce: number | undefined;
|
||||
scale: number;
|
||||
appState: InteractiveCanvasAppState;
|
||||
device: Device;
|
||||
renderInteractiveSceneCallback: (
|
||||
data: RenderInteractiveSceneCallback,
|
||||
) => void;
|
||||
|
@ -132,6 +133,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||
selectionColor,
|
||||
renderScrollbars: false,
|
||||
},
|
||||
device: props.device,
|
||||
callback: props.renderInteractiveSceneCallback,
|
||||
},
|
||||
isRenderThrottlingEnabled(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue