feat: resize elements from the sides (#7855)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Ryan Di 2024-05-01 00:05:00 +08:00 committed by GitHub
parent 6e5aeb112d
commit 88812e0386
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 3913 additions and 3741 deletions

View file

@ -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,

View file

@ -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(),