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
|
@ -26,8 +26,8 @@
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/lodash.throttle": "4.1.7",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
"@types/react": "18.0.15",
|
"@types/react": "18.2.0",
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.2.0",
|
||||||
"@types/socket.io-client": "3.0.0",
|
"@types/socket.io-client": "3.0.0",
|
||||||
"@vitejs/plugin-react": "3.1.0",
|
"@vitejs/plugin-react": "3.1.0",
|
||||||
"@vitest/coverage-v8": "0.33.0",
|
"@vitest/coverage-v8": "0.33.0",
|
||||||
|
|
|
@ -119,6 +119,7 @@ const flipElements = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
"nw",
|
"nw",
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
flipDirection === "horizontal" ? maxX : minX,
|
flipDirection === "horizontal" ? maxX : minX,
|
||||||
flipDirection === "horizontal" ? minY : maxY,
|
flipDirection === "horizontal" ? minY : maxY,
|
||||||
);
|
);
|
||||||
|
|
|
@ -90,6 +90,7 @@ import {
|
||||||
EDITOR_LS_KEYS,
|
EDITOR_LS_KEYS,
|
||||||
isIOS,
|
isIOS,
|
||||||
supportsResizeObserver,
|
supportsResizeObserver,
|
||||||
|
DEFAULT_COLLISION_THRESHOLD,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { ExportedElements, exportCanvas, loadFromBlob } from "../data";
|
import { ExportedElements, exportCanvas, loadFromBlob } from "../data";
|
||||||
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||||
|
@ -1703,6 +1704,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
scale={window.devicePixelRatio}
|
scale={window.devicePixelRatio}
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
|
device={this.device}
|
||||||
renderInteractiveSceneCallback={
|
renderInteractiveSceneCallback={
|
||||||
this.renderInteractiveSceneCallback
|
this.renderInteractiveSceneCallback
|
||||||
}
|
}
|
||||||
|
@ -4528,7 +4530,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
shape: this.getElementShape(elementWithHighestZIndex),
|
shape: this.getElementShape(elementWithHighestZIndex),
|
||||||
// when overlapping, we would like to be more precise
|
// when overlapping, we would like to be more precise
|
||||||
// this also avoids the need to update past tests
|
// this also avoids the need to update past tests
|
||||||
threshold: this.getHitThreshold() / 2,
|
threshold: this.getElementHitThreshold() / 2,
|
||||||
frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
|
frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
|
||||||
? this.frameNameBoundsCache.get(elementWithHighestZIndex)
|
? this.frameNameBoundsCache.get(elementWithHighestZIndex)
|
||||||
: null,
|
: null,
|
||||||
|
@ -4591,8 +4593,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHitThreshold() {
|
private getElementHitThreshold() {
|
||||||
return 10 / this.state.zoom.value;
|
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private hitElement(
|
private hitElement(
|
||||||
|
@ -4610,7 +4612,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const selectionShape = getSelectionBoxShape(
|
const selectionShape = getSelectionBoxShape(
|
||||||
element,
|
element,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.getHitThreshold(),
|
this.getElementHitThreshold(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return isPointInShape([x, y], selectionShape);
|
return isPointInShape([x, y], selectionShape);
|
||||||
|
@ -4631,7 +4633,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
y,
|
y,
|
||||||
element,
|
element,
|
||||||
shape: this.getElementShape(element),
|
shape: this.getElementShape(element),
|
||||||
threshold: this.getHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
frameNameBound: isFrameLikeElement(element)
|
frameNameBound: isFrameLikeElement(element)
|
||||||
? this.frameNameBoundsCache.get(element)
|
? this.frameNameBoundsCache.get(element)
|
||||||
: null,
|
: null,
|
||||||
|
@ -4663,7 +4665,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
y,
|
y,
|
||||||
element: elements[index],
|
element: elements[index],
|
||||||
shape: this.getElementShape(elements[index]),
|
shape: this.getElementShape(elements[index]),
|
||||||
threshold: this.getHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
hitElement = elements[index];
|
hitElement = elements[index];
|
||||||
|
@ -4916,7 +4918,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
y: sceneY,
|
y: sceneY,
|
||||||
element: container,
|
element: container,
|
||||||
shape: this.getElementShape(container),
|
shape: this.getElementShape(container),
|
||||||
threshold: this.getHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
const midPoint = getContainerCenter(
|
const midPoint = getContainerCenter(
|
||||||
|
@ -5331,24 +5333,41 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
!isOverScrollBar &&
|
!isOverScrollBar &&
|
||||||
!this.state.editingLinearElement
|
!this.state.editingLinearElement
|
||||||
) {
|
) {
|
||||||
const elementWithTransformHandleType = getElementWithTransformHandleType(
|
// for linear elements, we'd like to prioritize point dragging over edge resizing
|
||||||
elements,
|
// therefore, we update and check hovered point index first
|
||||||
this.state,
|
if (this.state.selectedLinearElement) {
|
||||||
scenePointerX,
|
this.handleHoverSelectedLinearElement(
|
||||||
scenePointerY,
|
this.state.selectedLinearElement,
|
||||||
this.state.zoom,
|
scenePointerX,
|
||||||
event.pointerType,
|
scenePointerY,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
elementWithTransformHandleType &&
|
|
||||||
elementWithTransformHandleType.transformHandleType
|
|
||||||
) {
|
|
||||||
setCursor(
|
|
||||||
this.interactiveCanvas,
|
|
||||||
getCursorForResizingElement(elementWithTransformHandleType),
|
|
||||||
);
|
);
|
||||||
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) {
|
} else if (selectedElements.length > 1 && !isOverScrollBar) {
|
||||||
const transformHandleType = getTransformHandleTypeFromCoords(
|
const transformHandleType = getTransformHandleTypeFromCoords(
|
||||||
|
@ -5357,6 +5376,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
scenePointerY,
|
scenePointerY,
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
event.pointerType,
|
event.pointerType,
|
||||||
|
this.device,
|
||||||
);
|
);
|
||||||
if (transformHandleType) {
|
if (transformHandleType) {
|
||||||
setCursor(
|
setCursor(
|
||||||
|
@ -5509,7 +5529,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
scenePointer.y,
|
scenePointer.y,
|
||||||
);
|
);
|
||||||
const threshold = this.getHitThreshold();
|
const threshold = this.getElementHitThreshold();
|
||||||
const point = { ...pointerDownState.lastCoords };
|
const point = { ...pointerDownState.lastCoords };
|
||||||
let samplingInterval = 0;
|
let samplingInterval = 0;
|
||||||
while (samplingInterval <= distance) {
|
while (samplingInterval <= distance) {
|
||||||
|
@ -5606,7 +5626,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
} else {
|
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
}
|
}
|
||||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||||
|
@ -6306,7 +6326,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
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 =
|
const elementWithTransformHandleType =
|
||||||
getElementWithTransformHandleType(
|
getElementWithTransformHandleType(
|
||||||
elements,
|
elements,
|
||||||
|
@ -6316,6 +6343,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
event.pointerType,
|
event.pointerType,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
this.device,
|
||||||
);
|
);
|
||||||
if (elementWithTransformHandleType != null) {
|
if (elementWithTransformHandleType != null) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -6331,6 +6359,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
pointerDownState.origin.y,
|
pointerDownState.origin.y,
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
event.pointerType,
|
event.pointerType,
|
||||||
|
this.device,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (pointerDownState.resize.handleType) {
|
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
|
// 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);
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||||
return (
|
return (
|
||||||
point.x > x1 - threshold &&
|
point.x > x1 - threshold &&
|
||||||
|
@ -8412,7 +8441,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
y: pointerDownState.origin.y,
|
y: pointerDownState.origin.y,
|
||||||
element: hitElement,
|
element: hitElement,
|
||||||
shape: this.getElementShape(hitElement),
|
shape: this.getElementShape(hitElement),
|
||||||
threshold: this.getHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
frameNameBound: isFrameLikeElement(hitElement)
|
frameNameBound: isFrameLikeElement(hitElement)
|
||||||
? this.frameNameBoundsCache.get(hitElement)
|
? this.frameNameBoundsCache.get(hitElement)
|
||||||
: null,
|
: null,
|
||||||
|
@ -9525,7 +9554,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scene.getElementsMapIncludingDeleted(),
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
shouldRotateWithDiscreteAngle(event),
|
shouldRotateWithDiscreteAngle(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
selectedElements.length === 1 && isImageElement(selectedElements[0])
|
selectedElements.some((element) => isImageElement(element))
|
||||||
? !shouldMaintainAspectRatio(event)
|
? !shouldMaintainAspectRatio(event)
|
||||||
: shouldMaintainAspectRatio(event),
|
: shouldMaintainAspectRatio(event),
|
||||||
resizeX,
|
resizeX,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils";
|
||||||
import { CURSOR_TYPE } from "../../constants";
|
import { CURSOR_TYPE } from "../../constants";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import type { DOMAttributes } from "react";
|
import type { DOMAttributes } from "react";
|
||||||
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
import type { AppState, Device, InteractiveCanvasAppState } from "../../types";
|
||||||
import type {
|
import type {
|
||||||
InteractiveCanvasRenderConfig,
|
InteractiveCanvasRenderConfig,
|
||||||
RenderableElementsMap,
|
RenderableElementsMap,
|
||||||
|
@ -23,6 +23,7 @@ type InteractiveCanvasProps = {
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: InteractiveCanvasAppState;
|
appState: InteractiveCanvasAppState;
|
||||||
|
device: Device;
|
||||||
renderInteractiveSceneCallback: (
|
renderInteractiveSceneCallback: (
|
||||||
data: RenderInteractiveSceneCallback,
|
data: RenderInteractiveSceneCallback,
|
||||||
) => void;
|
) => void;
|
||||||
|
@ -132,6 +133,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||||
selectionColor,
|
selectionColor,
|
||||||
renderScrollbars: false,
|
renderScrollbars: false,
|
||||||
},
|
},
|
||||||
|
device: props.device,
|
||||||
callback: props.renderInteractiveSceneCallback,
|
callback: props.renderInteractiveSceneCallback,
|
||||||
},
|
},
|
||||||
isRenderThrottlingEnabled(),
|
isRenderThrottlingEnabled(),
|
||||||
|
|
|
@ -148,6 +148,13 @@ export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||||
export const DEFAULT_VERSION = "{version}";
|
export const DEFAULT_VERSION = "{version}";
|
||||||
export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2;
|
export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2;
|
||||||
|
|
||||||
|
export const SIDE_RESIZING_THRESHOLD = 2 * DEFAULT_TRANSFORM_HANDLE_SPACING;
|
||||||
|
// a small epsilon to make side resizing always take precedence
|
||||||
|
// (avoids an increase in renders and changes to tests)
|
||||||
|
const EPSILON = 0.00001;
|
||||||
|
export const DEFAULT_COLLISION_THRESHOLD =
|
||||||
|
2 * SIDE_RESIZING_THRESHOLD - EPSILON;
|
||||||
|
|
||||||
export const COLOR_WHITE = "#ffffff";
|
export const COLOR_WHITE = "#ffffff";
|
||||||
export const COLOR_CHARCOAL_BLACK = "#1e1e1e";
|
export const COLOR_CHARCOAL_BLACK = "#1e1e1e";
|
||||||
// keep this in sync with CSS
|
// keep this in sync with CSS
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
|
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import {
|
import { rotate, centerPoint, rotatePoint } from "../math";
|
||||||
rotate,
|
|
||||||
adjustXYWithRotation,
|
|
||||||
centerPoint,
|
|
||||||
rotatePoint,
|
|
||||||
} from "../math";
|
|
||||||
import {
|
import {
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
@ -23,7 +18,6 @@ import {
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
getCommonBoundingBox,
|
getCommonBoundingBox,
|
||||||
getElementPointsCoords,
|
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
|
@ -38,7 +32,6 @@ import { mutateElement } from "./mutateElement";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import {
|
import {
|
||||||
TransformHandleType,
|
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
TransformHandleDirection,
|
TransformHandleDirection,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
|
@ -54,6 +47,7 @@ import {
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { isInGroup } from "../groups";
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
if (angle < 0) {
|
if (angle < 0) {
|
||||||
|
@ -133,18 +127,14 @@ export const transformElements = (
|
||||||
centerY,
|
centerY,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else if (
|
} else if (transformHandleType) {
|
||||||
transformHandleType === "nw" ||
|
|
||||||
transformHandleType === "ne" ||
|
|
||||||
transformHandleType === "sw" ||
|
|
||||||
transformHandleType === "se"
|
|
||||||
) {
|
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
originalElements,
|
originalElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
|
shouldMaintainAspectRatio,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
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 = (
|
const resizeSingleTextElement = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
@ -260,9 +230,10 @@ const resizeSingleTextElement = (
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
const cx = (x1 + x2) / 2;
|
element,
|
||||||
const cy = (y1 + y2) / 2;
|
elementsMap,
|
||||||
|
);
|
||||||
// rotation pointer with reverse angle
|
// rotation pointer with reverse angle
|
||||||
const [rotatedX, rotatedY] = rotate(
|
const [rotatedX, rotatedY] = rotate(
|
||||||
pointerX,
|
pointerX,
|
||||||
|
@ -271,33 +242,24 @@ const resizeSingleTextElement = (
|
||||||
cy,
|
cy,
|
||||||
-element.angle,
|
-element.angle,
|
||||||
);
|
);
|
||||||
let scale: number;
|
let scaleX = 0;
|
||||||
switch (transformHandleType) {
|
let scaleY = 0;
|
||||||
case "se":
|
|
||||||
scale = Math.max(
|
if (transformHandleType.includes("e")) {
|
||||||
(rotatedX - x1) / (x2 - x1),
|
scaleX = (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;
|
|
||||||
}
|
}
|
||||||
|
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) {
|
if (scale > 0) {
|
||||||
const nextWidth = element.width * scale;
|
const nextWidth = element.width * scale;
|
||||||
const nextHeight = element.height * scale;
|
const nextHeight = element.height * scale;
|
||||||
|
@ -305,32 +267,55 @@ const resizeSingleTextElement = (
|
||||||
if (metrics === null) {
|
if (metrics === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
|
||||||
element,
|
const startTopLeft = [x1, y1];
|
||||||
nextWidth,
|
const startBottomRight = [x2, y2];
|
||||||
nextHeight,
|
const startCenter = [cx, cy];
|
||||||
false,
|
|
||||||
);
|
let newTopLeft = [x1, y1] as [number, number];
|
||||||
const deltaX1 = (x1 - nextX1) / 2;
|
if (["n", "w", "nw"].includes(transformHandleType)) {
|
||||||
const deltaY1 = (y1 - nextY1) / 2;
|
newTopLeft = [
|
||||||
const deltaX2 = (x2 - nextX2) / 2;
|
startBottomRight[0] - Math.abs(nextWidth),
|
||||||
const deltaY2 = (y2 - nextY2) / 2;
|
startBottomRight[1] - Math.abs(nextHeight),
|
||||||
const [nextElementX, nextElementY] = adjustXYWithRotation(
|
];
|
||||||
getSidesForTransformHandle(transformHandleType, shouldResizeFromCenter),
|
}
|
||||||
element.x,
|
if (transformHandleType === "ne") {
|
||||||
element.y,
|
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
|
||||||
element.angle,
|
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(nextHeight)];
|
||||||
deltaX1,
|
}
|
||||||
deltaY1,
|
if (transformHandleType === "sw") {
|
||||||
deltaX2,
|
const topRight = [startBottomRight[0], startTopLeft[1]];
|
||||||
deltaY2,
|
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, {
|
mutateElement(element, {
|
||||||
fontSize: metrics.size,
|
fontSize: metrics.size,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
x: nextElementX,
|
x: nextX,
|
||||||
y: nextElementY,
|
y: nextY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -636,8 +621,9 @@ export const resizeMultipleElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
transformHandleType: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
|
shouldMaintainAspectRatio: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
) => {
|
) => {
|
||||||
|
@ -691,43 +677,80 @@ export const resizeMultipleElements = (
|
||||||
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
||||||
targetElements.map(({ orig }) => orig).concat(boundTextElements),
|
targetElements.map(({ orig }) => orig).concat(boundTextElements),
|
||||||
);
|
);
|
||||||
|
const width = maxX - minX;
|
||||||
// const originalHeight = maxY - minY;
|
const height = maxY - minY;
|
||||||
// const originalWidth = maxX - minX;
|
|
||||||
|
|
||||||
const direction = transformHandleType;
|
const direction = transformHandleType;
|
||||||
|
|
||||||
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
|
const anchorsMap: Record<TransformHandleDirection, Point> = {
|
||||||
ne: [minX, maxY],
|
ne: [minX, maxY],
|
||||||
se: [minX, minY],
|
se: [minX, minY],
|
||||||
sw: [maxX, minY],
|
sw: [maxX, minY],
|
||||||
nw: [maxX, maxY],
|
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
|
// anchor point must be on the opposite side of the dragged selection handle
|
||||||
// or be the center of the selection if shouldResizeFromCenter
|
// or be the center of the selection if shouldResizeFromCenter
|
||||||
const [anchorX, anchorY]: Point = shouldResizeFromCenter
|
const [anchorX, anchorY]: Point = shouldResizeFromCenter
|
||||||
? [midX, midY]
|
? [midX, midY]
|
||||||
: mapDirectionsToAnchors[direction];
|
: anchorsMap[direction];
|
||||||
|
|
||||||
|
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
|
||||||
|
|
||||||
const scale =
|
const scale =
|
||||||
Math.max(
|
Math.max(
|
||||||
Math.abs(pointerX - anchorX) / (maxX - minX) || 0,
|
Math.abs(pointerX - anchorX) / width || 0,
|
||||||
Math.abs(pointerY - anchorY) / (maxY - minY) || 0,
|
Math.abs(pointerY - anchorY) / height || 0,
|
||||||
) * (shouldResizeFromCenter ? 2 : 1);
|
) * resizeFromCenterScale;
|
||||||
|
|
||||||
if (scale === 0) {
|
if (scale === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDirectionsToPointerPositions: Record<
|
let scaleX =
|
||||||
typeof direction,
|
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]
|
[x: boolean, y: boolean]
|
||||||
> = {
|
> = {
|
||||||
ne: [pointerX >= anchorX, pointerY <= anchorY],
|
ne: [pointerX < anchorX, pointerY > anchorY],
|
||||||
se: [pointerX >= anchorX, pointerY >= anchorY],
|
se: [pointerX < anchorX, pointerY < anchorY],
|
||||||
sw: [pointerX <= anchorX, pointerY >= anchorY],
|
sw: [pointerX > anchorX, pointerY < anchorY],
|
||||||
nw: [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
|
* mirror points in the case of linear & freedraw elemenets
|
||||||
* 3. adjust element angle
|
* 3. adjust element angle
|
||||||
*/
|
*/
|
||||||
const [flipFactorX, flipFactorY] = mapDirectionsToPointerPositions[
|
const [flipFactorX, flipFactorY] = flipConditionsMap[direction].map(
|
||||||
direction
|
(condition) => (condition ? -1 : 1),
|
||||||
].map((condition) => (condition ? 1 : -1));
|
);
|
||||||
const isFlippedByX = flipFactorX < 0;
|
const isFlippedByX = flipFactorX < 0;
|
||||||
const isFlippedByY = flipFactorY < 0;
|
const isFlippedByY = flipFactorY < 0;
|
||||||
|
|
||||||
|
@ -762,8 +785,8 @@ export const resizeMultipleElements = (
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = orig.width * scale;
|
const width = orig.width * scaleX;
|
||||||
const height = orig.height * scale;
|
const height = orig.height * scaleY;
|
||||||
const angle = normalizeAngle(orig.angle * flipFactorX * flipFactorY);
|
const angle = normalizeAngle(orig.angle * flipFactorX * flipFactorY);
|
||||||
|
|
||||||
const isLinearOrFreeDraw = isLinearElement(orig) || isFreeDrawElement(orig);
|
const isLinearOrFreeDraw = isLinearElement(orig) || isFreeDrawElement(orig);
|
||||||
|
@ -771,8 +794,8 @@ export const resizeMultipleElements = (
|
||||||
const offsetY = orig.y - anchorY;
|
const offsetY = orig.y - anchorY;
|
||||||
const shiftX = isFlippedByX && !isLinearOrFreeDraw ? width : 0;
|
const shiftX = isFlippedByX && !isLinearOrFreeDraw ? width : 0;
|
||||||
const shiftY = isFlippedByY && !isLinearOrFreeDraw ? height : 0;
|
const shiftY = isFlippedByY && !isLinearOrFreeDraw ? height : 0;
|
||||||
const x = anchorX + flipFactorX * (offsetX * scale + shiftX);
|
const x = anchorX + flipFactorX * (offsetX * scaleX + shiftX);
|
||||||
const y = anchorY + flipFactorY * (offsetY * scale + shiftY);
|
const y = anchorY + flipFactorY * (offsetY * scaleY + shiftY);
|
||||||
|
|
||||||
const rescaledPoints = rescalePointsInElement(
|
const rescaledPoints = rescalePointsInElement(
|
||||||
orig,
|
orig,
|
||||||
|
@ -790,40 +813,10 @@ export const resizeMultipleElements = (
|
||||||
...rescaledPoints,
|
...rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isImageElement(orig) && targetElements.length === 1) {
|
if (isImageElement(orig)) {
|
||||||
update.scale = [orig.scale[0] * flipFactorX, orig.scale[1] * flipFactorY];
|
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)) {
|
if (isTextElement(orig)) {
|
||||||
const metrics = measureFontSizeFromWidth(orig, elementsMap, width);
|
const metrics = measureFontSizeFromWidth(orig, elementsMap, width);
|
||||||
if (!metrics) {
|
if (!metrics) {
|
||||||
|
@ -837,11 +830,15 @@ export const resizeMultipleElements = (
|
||||||
) as ExcalidrawTextElementWithContainer | undefined;
|
) as ExcalidrawTextElementWithContainer | undefined;
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const newFontSize = boundTextElement.fontSize * scale;
|
if (keepAspectRatio) {
|
||||||
if (newFontSize < MIN_FONT_SIZE) {
|
const newFontSize = boundTextElement.fontSize * scale;
|
||||||
return;
|
if (newFontSize < MIN_FONT_SIZE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
update.boundTextFontSize = newFontSize;
|
||||||
|
} else {
|
||||||
|
update.boundTextFontSize = boundTextElement.fontSize;
|
||||||
}
|
}
|
||||||
update.boundTextFontSize = newFontSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
elementsAndUpdates.push({
|
elementsAndUpdates.push({
|
||||||
|
|
|
@ -6,15 +6,24 @@ import {
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
|
||||||
getTransformHandlesFromCoords,
|
getTransformHandlesFromCoords,
|
||||||
getTransformHandles,
|
getTransformHandles,
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
TransformHandle,
|
TransformHandle,
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
|
getOmitSidesForDevice,
|
||||||
|
canResizeFromSides,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { AppState, Zoom } from "../types";
|
import { AppState, Device, Zoom } from "../types";
|
||||||
import { Bounds } from "./bounds";
|
import { Bounds, getElementAbsoluteCoords } from "./bounds";
|
||||||
|
import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
||||||
|
import {
|
||||||
|
angleToDegrees,
|
||||||
|
pointOnLine,
|
||||||
|
pointRotate,
|
||||||
|
} from "../../utils/geometry/geometry";
|
||||||
|
import { Line, Point } from "../../utils/geometry/shape";
|
||||||
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
const isInsideTransformHandle = (
|
const isInsideTransformHandle = (
|
||||||
transformHandle: TransformHandle,
|
transformHandle: TransformHandle,
|
||||||
|
@ -34,13 +43,20 @@ export const resizeTest = (
|
||||||
y: number,
|
y: number,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
pointerType: PointerType,
|
pointerType: PointerType,
|
||||||
|
device: Device,
|
||||||
): MaybeTransformHandleType => {
|
): MaybeTransformHandleType => {
|
||||||
if (!appState.selectedElementIds[element.id]) {
|
if (!appState.selectedElementIds[element.id]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rotation: rotationTransformHandle, ...transformHandles } =
|
const { rotation: rotationTransformHandle, ...transformHandles } =
|
||||||
getTransformHandles(element, zoom, elementsMap, pointerType);
|
getTransformHandles(
|
||||||
|
element,
|
||||||
|
zoom,
|
||||||
|
elementsMap,
|
||||||
|
pointerType,
|
||||||
|
getOmitSidesForDevice(device),
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
rotationTransformHandle &&
|
rotationTransformHandle &&
|
||||||
|
@ -62,6 +78,35 @@ export const resizeTest = (
|
||||||
return filter[0] as TransformHandleType;
|
return filter[0] as TransformHandleType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canResizeFromSides(device)) {
|
||||||
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note that for a text element, when "resized" from the side
|
||||||
|
// we should make it wrap/unwrap
|
||||||
|
if (
|
||||||
|
element.type !== "text" &&
|
||||||
|
!(isLinearElement(element) && element.points.length <= 2)
|
||||||
|
) {
|
||||||
|
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||||
|
const sides = getSelectionBorders(
|
||||||
|
[x1 - SPACING, y1 - SPACING],
|
||||||
|
[x2 + SPACING, y2 + SPACING],
|
||||||
|
[cx, cy],
|
||||||
|
angleToDegrees(element.angle),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [dir, side] of Object.entries(sides)) {
|
||||||
|
// test to see if x, y are on the line segment
|
||||||
|
if (pointOnLine([x, y], side as Line, SPACING)) {
|
||||||
|
return dir as TransformHandleType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -73,6 +118,7 @@ export const getElementWithTransformHandleType = (
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
pointerType: PointerType,
|
pointerType: PointerType,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
device: Device,
|
||||||
) => {
|
) => {
|
||||||
return elements.reduce((result, element) => {
|
return elements.reduce((result, element) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
|
@ -86,6 +132,7 @@ export const getElementWithTransformHandleType = (
|
||||||
scenePointerY,
|
scenePointerY,
|
||||||
zoom,
|
zoom,
|
||||||
pointerType,
|
pointerType,
|
||||||
|
device,
|
||||||
);
|
);
|
||||||
return transformHandleType ? { element, transformHandleType } : null;
|
return transformHandleType ? { element, transformHandleType } : null;
|
||||||
}, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null);
|
}, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null);
|
||||||
|
@ -97,13 +144,14 @@ export const getTransformHandleTypeFromCoords = (
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
pointerType: PointerType,
|
pointerType: PointerType,
|
||||||
|
device: Device,
|
||||||
): MaybeTransformHandleType => {
|
): MaybeTransformHandleType => {
|
||||||
const transformHandles = getTransformHandlesFromCoords(
|
const transformHandles = getTransformHandlesFromCoords(
|
||||||
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
|
||||||
0,
|
0,
|
||||||
zoom,
|
zoom,
|
||||||
pointerType,
|
pointerType,
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
getOmitSidesForDevice(device),
|
||||||
);
|
);
|
||||||
|
|
||||||
const found = Object.keys(transformHandles).find((key) => {
|
const found = Object.keys(transformHandles).find((key) => {
|
||||||
|
@ -114,7 +162,33 @@ export const getTransformHandleTypeFromCoords = (
|
||||||
isInsideTransformHandle(transformHandle, scenePointerX, scenePointerY)
|
isInsideTransformHandle(transformHandle, scenePointerX, scenePointerY)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (found || false) as MaybeTransformHandleType;
|
|
||||||
|
if (found) {
|
||||||
|
return found as MaybeTransformHandleType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canResizeFromSides(device)) {
|
||||||
|
const cx = (x1 + x2) / 2;
|
||||||
|
const cy = (y1 + y2) / 2;
|
||||||
|
|
||||||
|
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||||
|
|
||||||
|
const sides = getSelectionBorders(
|
||||||
|
[x1 - SPACING, y1 - SPACING],
|
||||||
|
[x2 + SPACING, y2 + SPACING],
|
||||||
|
[cx, cy],
|
||||||
|
angleToDegrees(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [dir, side] of Object.entries(sides)) {
|
||||||
|
// test to see if x, y are on the line segment
|
||||||
|
if (pointOnLine([scenePointerX, scenePointerY], side as Line, SPACING)) {
|
||||||
|
return dir as TransformHandleType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RESIZE_CURSORS = ["ns", "nesw", "ew", "nwse"];
|
const RESIZE_CURSORS = ["ns", "nesw", "ew", "nwse"];
|
||||||
|
@ -174,3 +248,22 @@ export const getCursorForResizingElement = (resizingElement: {
|
||||||
|
|
||||||
return cursor ? `${cursor}-resize` : "";
|
return cursor ? `${cursor}-resize` : "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSelectionBorders = (
|
||||||
|
[x1, y1]: Point,
|
||||||
|
[x2, y2]: Point,
|
||||||
|
center: Point,
|
||||||
|
angleInDegrees: number,
|
||||||
|
) => {
|
||||||
|
const topLeft = pointRotate([x1, y1], angleInDegrees, center);
|
||||||
|
const topRight = pointRotate([x2, y1], angleInDegrees, center);
|
||||||
|
const bottomLeft = pointRotate([x1, y2], angleInDegrees, center);
|
||||||
|
const bottomRight = pointRotate([x2, y2], angleInDegrees, center);
|
||||||
|
|
||||||
|
return {
|
||||||
|
n: [topLeft, topRight],
|
||||||
|
e: [topRight, bottomRight],
|
||||||
|
s: [bottomRight, bottomLeft],
|
||||||
|
w: [bottomLeft, topLeft],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -7,10 +7,14 @@ import {
|
||||||
|
|
||||||
import { Bounds, getElementAbsoluteCoords } from "./bounds";
|
import { Bounds, getElementAbsoluteCoords } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
import { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
||||||
import { DEFAULT_TRANSFORM_HANDLE_SPACING } from "../constants";
|
import {
|
||||||
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
|
isAndroid,
|
||||||
|
isIOS,
|
||||||
|
} from "../constants";
|
||||||
|
|
||||||
export type TransformHandleDirection =
|
export type TransformHandleDirection =
|
||||||
| "n"
|
| "n"
|
||||||
|
@ -38,6 +42,13 @@ const transformHandleSizes: { [k in PointerType]: number } = {
|
||||||
|
|
||||||
const ROTATION_RESIZE_HANDLE_GAP = 16;
|
const ROTATION_RESIZE_HANDLE_GAP = 16;
|
||||||
|
|
||||||
|
export const DEFAULT_OMIT_SIDES = {
|
||||||
|
e: true,
|
||||||
|
s: true,
|
||||||
|
n: true,
|
||||||
|
w: true,
|
||||||
|
};
|
||||||
|
|
||||||
export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
|
export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
|
||||||
e: true,
|
e: true,
|
||||||
s: true,
|
s: true,
|
||||||
|
@ -89,6 +100,26 @@ const generateTransformHandle = (
|
||||||
return [xx - width / 2, yy - height / 2, width, height];
|
return [xx - width / 2, yy - height / 2, width, height];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const canResizeFromSides = (device: Device) => {
|
||||||
|
if (device.viewport.isMobile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.isTouchScreen && (isAndroid || isIOS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOmitSidesForDevice = (device: Device) => {
|
||||||
|
if (canResizeFromSides(device)) {
|
||||||
|
return DEFAULT_OMIT_SIDES;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
export const getTransformHandlesFromCoords = (
|
export const getTransformHandlesFromCoords = (
|
||||||
[x1, y1, x2, y2, cx, cy]: [number, number, number, number, number, number],
|
[x1, y1, x2, y2, cx, cy]: [number, number, number, number, number, number],
|
||||||
angle: number,
|
angle: number,
|
||||||
|
@ -232,8 +263,8 @@ export const getTransformHandles = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
|
||||||
pointerType: PointerType = "mouse",
|
pointerType: PointerType = "mouse",
|
||||||
|
omitSides: { [T in TransformHandleType]?: boolean } = DEFAULT_OMIT_SIDES,
|
||||||
): TransformHandles => {
|
): TransformHandles => {
|
||||||
// so that when locked element is selected (especially when you toggle lock
|
// so that when locked element is selected (especially when you toggle lock
|
||||||
// via keyboard) the locked element is visually distinct, indicating
|
// via keyboard) the locked element is visually distinct, indicating
|
||||||
|
@ -242,7 +273,6 @@ export const getTransformHandles = (
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
let omitSides: { [T in TransformHandleType]?: boolean } = {};
|
|
||||||
if (element.type === "freedraw" || isLinearElement(element)) {
|
if (element.type === "freedraw" || isLinearElement(element)) {
|
||||||
if (element.points.length === 2) {
|
if (element.points.length === 2) {
|
||||||
// only check the last point because starting point is always (0,0)
|
// only check the last point because starting point is always (0,0)
|
||||||
|
@ -263,6 +293,7 @@ export const getTransformHandles = (
|
||||||
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
||||||
} else if (isFrameLikeElement(element)) {
|
} else if (isFrameLikeElement(element)) {
|
||||||
omitSides = {
|
omitSides = {
|
||||||
|
...omitSides,
|
||||||
rotation: true,
|
rotation: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -387,3 +387,7 @@ export const elementsAreInSameGroup = (elements: ExcalidrawElement[]) => {
|
||||||
|
|
||||||
return maxGroup === elements.length;
|
return maxGroup === elements.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isInGroup = (element: NonDeletedExcalidrawElement) => {
|
||||||
|
return element.groupIds.length > 0;
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
|
||||||
getTransformHandlesFromCoords,
|
getTransformHandlesFromCoords,
|
||||||
getTransformHandles,
|
getTransformHandles,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
|
@ -23,7 +22,7 @@ import {
|
||||||
selectGroupsFromGivenElements,
|
selectGroupsFromGivenElements,
|
||||||
} from "../groups";
|
} from "../groups";
|
||||||
import {
|
import {
|
||||||
OMIT_SIDES_FOR_FRAME,
|
getOmitSidesForDevice,
|
||||||
shouldShowBoundingBox,
|
shouldShowBoundingBox,
|
||||||
TransformHandles,
|
TransformHandles,
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
|
@ -577,6 +576,7 @@ const _renderInteractiveScene = ({
|
||||||
scale,
|
scale,
|
||||||
appState,
|
appState,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
device,
|
||||||
}: InteractiveSceneRenderConfig) => {
|
}: InteractiveSceneRenderConfig) => {
|
||||||
if (canvas === null) {
|
if (canvas === null) {
|
||||||
return { atLeastOneVisibleElement: false, elementsMap };
|
return { atLeastOneVisibleElement: false, elementsMap };
|
||||||
|
@ -806,6 +806,7 @@ const _renderInteractiveScene = ({
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
"mouse", // when we render we don't know which pointer type so use mouse,
|
"mouse", // when we render we don't know which pointer type so use mouse,
|
||||||
|
getOmitSidesForDevice(device),
|
||||||
);
|
);
|
||||||
if (!appState.viewModeEnabled && showBoundingBox) {
|
if (!appState.viewModeEnabled && showBoundingBox) {
|
||||||
renderTransformHandles(
|
renderTransformHandles(
|
||||||
|
@ -844,8 +845,8 @@ const _renderInteractiveScene = ({
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
"mouse",
|
"mouse",
|
||||||
isFrameSelected
|
isFrameSelected
|
||||||
? OMIT_SIDES_FOR_FRAME
|
? { ...getOmitSidesForDevice(device), rotation: true }
|
||||||
: OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
: getOmitSidesForDevice(device),
|
||||||
);
|
);
|
||||||
if (selectedElements.some((element) => !element.locked)) {
|
if (selectedElements.some((element) => !element.locked)) {
|
||||||
renderTransformHandles(
|
renderTransformHandles(
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
StaticCanvasAppState,
|
StaticCanvasAppState,
|
||||||
SocketId,
|
SocketId,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
|
Device,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { MakeBrand } from "../utility-types";
|
import { MakeBrand } from "../utility-types";
|
||||||
|
|
||||||
|
@ -85,6 +86,7 @@ export type InteractiveSceneRenderConfig = {
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: InteractiveCanvasAppState;
|
appState: InteractiveCanvasAppState;
|
||||||
renderConfig: InteractiveCanvasRenderConfig;
|
renderConfig: InteractiveCanvasRenderConfig;
|
||||||
|
device: Device;
|
||||||
callback: (data: RenderInteractiveSceneCallback) => void;
|
callback: (data: RenderInteractiveSceneCallback) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2170,14 +2170,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1278240551,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 1014066025,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
@ -2404,14 +2404,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1278240551,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 401146281,
|
"versionNonce": 1150084233,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
@ -2438,14 +2438,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1150084233,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1116226695,
|
"versionNonce": 238820263,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
|
@ -2704,14 +2704,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1278240551,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1505387817,
|
"versionNonce": 493213705,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
@ -2740,14 +2740,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1150084233,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 915032327,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
@ -3060,14 +3060,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1278240551,
|
"seed": 449462985,
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 640725609,
|
"versionNonce": 941653321,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
@ -3094,14 +3094,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 760410951,
|
"seed": 289600103,
|
||||||
"strokeColor": "#e03131",
|
"strokeColor": "#e03131",
|
||||||
"strokeStyle": "dotted",
|
"strokeStyle": "dotted",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 9,
|
"version": 9,
|
||||||
"versionNonce": 1315507081,
|
"versionNonce": 640725609,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
@ -3840,14 +3840,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1150084233,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 23633383,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
@ -3874,14 +3874,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1278240551,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 401146281,
|
"versionNonce": 1150084233,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
@ -5224,8 +5224,8 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"left": -19,
|
"left": -17,
|
||||||
"top": -9,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
|
@ -5342,14 +5342,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 453191,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 1014066025,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
@ -5376,16 +5376,16 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1014066025,
|
"seed": 400692809,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 23633383,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -5493,7 +5493,7 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
@ -6349,8 +6349,8 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"left": -19,
|
"left": -17,
|
||||||
"top": -9,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
|
@ -6516,7 +6516,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 747212839,
|
"versionNonce": 747212839,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -6624,7 +6624,7 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 12,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
@ -8181,8 +8181,8 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"left": -19,
|
"left": -17,
|
||||||
"top": -9,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
|
|
|
@ -1400,9 +1400,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
||||||
"penDetected": false,
|
"penDetected": false,
|
||||||
"penMode": false,
|
"penMode": false,
|
||||||
"pendingImageElementId": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": {
|
"previousSelectedElementIds": {},
|
||||||
"id0": true,
|
|
||||||
},
|
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
|
@ -1522,7 +1520,7 @@ History {
|
||||||
|
|
||||||
exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of elements 1`] = `0`;
|
exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of elements 1`] = `0`;
|
||||||
|
|
||||||
exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `9`;
|
exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `11`;
|
||||||
|
|
||||||
exports[`regression tests > adjusts z order when grouping > [end of test] appState 1`] = `
|
exports[`regression tests > adjusts z order when grouping > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
|
|
|
@ -45,6 +45,7 @@ describe("element binding", () => {
|
||||||
mouse.downAt(100, 0);
|
mouse.downAt(100, 0);
|
||||||
mouse.moveTo(55, 0);
|
mouse.moveTo(55, 0);
|
||||||
mouse.up(0, 0);
|
mouse.up(0, 0);
|
||||||
|
expect(API.getSelectedElements()).toEqual([arrow]);
|
||||||
expect(arrow.startBinding).toEqual({
|
expect(arrow.startBinding).toEqual({
|
||||||
elementId: rect.id,
|
elementId: rect.id,
|
||||||
focus: expect.toBeNonNaNNumber(),
|
focus: expect.toBeNonNaNNumber(),
|
||||||
|
|
|
@ -108,8 +108,8 @@ describe("contextMenu element", () => {
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
const contextMenuOptions =
|
const contextMenuOptions =
|
||||||
|
@ -188,19 +188,19 @@ describe("contextMenu element", () => {
|
||||||
mouse.up(10, 10);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, -10);
|
mouse.down(12, -10);
|
||||||
mouse.up(10, 10);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.click(10, 10);
|
mouse.click(10, 10);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.click(20, 0);
|
mouse.click(22, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
|
@ -240,13 +240,13 @@ describe("contextMenu element", () => {
|
||||||
mouse.up(10, 10);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down(10, -10);
|
mouse.down(12, -10);
|
||||||
mouse.up(10, 10);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.click(10, 10);
|
mouse.click(10, 10);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.click(20, 0);
|
mouse.click(22, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
@ -255,8 +255,8 @@ describe("contextMenu element", () => {
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
|
@ -297,8 +297,8 @@ describe("contextMenu element", () => {
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
expect(copiedStyles).toBe("{}");
|
expect(copiedStyles).toBe("{}");
|
||||||
|
@ -382,8 +382,8 @@ describe("contextMenu element", () => {
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
fireEvent.click(queryAllByText(contextMenu!, "Delete")[0]);
|
fireEvent.click(queryAllByText(contextMenu!, "Delete")[0]);
|
||||||
|
@ -398,8 +398,8 @@ describe("contextMenu element", () => {
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
fireEvent.click(queryByText(contextMenu!, "Add to library")!);
|
fireEvent.click(queryByText(contextMenu!, "Add to library")!);
|
||||||
|
@ -417,8 +417,8 @@ describe("contextMenu element", () => {
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
fireEvent.click(queryByText(contextMenu!, "Duplicate")!);
|
fireEvent.click(queryByText(contextMenu!, "Duplicate")!);
|
||||||
|
@ -548,8 +548,8 @@ describe("contextMenu element", () => {
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
fireEvent.click(queryByText(contextMenu!, "Group selection")!);
|
fireEvent.click(queryByText(contextMenu!, "Group selection")!);
|
||||||
|
@ -578,8 +578,8 @@ describe("contextMenu element", () => {
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 3,
|
||||||
clientY: 1,
|
clientY: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
const contextMenu = UI.queryContextMenu();
|
||||||
|
|
|
@ -315,6 +315,7 @@ const transform = (
|
||||||
h.state.zoom,
|
h.state.zoom,
|
||||||
arrayToMap(h.elements),
|
arrayToMap(h.elements),
|
||||||
"mouse",
|
"mouse",
|
||||||
|
{},
|
||||||
)[handle];
|
)[handle];
|
||||||
} else {
|
} else {
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||||
|
|
|
@ -199,7 +199,6 @@ describe("regression tests", () => {
|
||||||
expect(
|
expect(
|
||||||
h.elements.filter((element) => element.type === "rectangle").length,
|
h.elements.filter((element) => element.type === "rectangle").length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(-8, -8);
|
mouse.down(-8, -8);
|
||||||
mouse.up(10, 10);
|
mouse.up(10, 10);
|
||||||
|
@ -725,7 +724,7 @@ describe("regression tests", () => {
|
||||||
mouse.up(10, 10);
|
mouse.up(10, 10);
|
||||||
|
|
||||||
const { x: prevX, y: prevY } = API.getSelectedElement();
|
const { x: prevX, y: prevY } = API.getSelectedElement();
|
||||||
|
API.clearSelection();
|
||||||
// drag element from point on bounding box that doesn't hit element
|
// drag element from point on bounding box that doesn't hit element
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.down(8, 8);
|
mouse.down(8, 8);
|
||||||
|
@ -1015,12 +1014,22 @@ describe("regression tests", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("single-clicking on a subgroup of a selected group should not alter selection", () => {
|
it("single-clicking on a subgroup of a selected group should not alter selection", () => {
|
||||||
const rect1 = UI.createElement("rectangle", { x: 10 });
|
const rect1 = UI.createElement("rectangle", {
|
||||||
const rect2 = UI.createElement("rectangle", { x: 50 });
|
x: 10,
|
||||||
|
});
|
||||||
|
const rect2 = UI.createElement("rectangle", {
|
||||||
|
x: 50,
|
||||||
|
});
|
||||||
UI.group([rect1, rect2]);
|
UI.group([rect1, rect2]);
|
||||||
|
|
||||||
const rect3 = UI.createElement("rectangle", { x: 10, y: 50 });
|
const rect3 = UI.createElement("rectangle", {
|
||||||
const rect4 = UI.createElement("rectangle", { x: 50, y: 50 });
|
x: 10,
|
||||||
|
y: 50,
|
||||||
|
});
|
||||||
|
const rect4 = UI.createElement("rectangle", {
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
});
|
||||||
UI.group([rect3, rect4]);
|
UI.group([rect3, rect4]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
@ -1079,8 +1088,9 @@ describe("regression tests", () => {
|
||||||
UI.group([rect1, rect3]);
|
UI.group([rect1, rect3]);
|
||||||
assertSelectedElements(rect1, rect2, rect3);
|
assertSelectedElements(rect1, rect2, rect3);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
mouse.clickOn(rect1);
|
mouse.click(10, 5);
|
||||||
});
|
});
|
||||||
assertSelectedElements(rect1);
|
assertSelectedElements(rect1);
|
||||||
|
|
||||||
|
|
|
@ -544,7 +544,9 @@ describe("multiple selection", () => {
|
||||||
1 + move[1] / selectionHeight,
|
1 + move[1] / selectionHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
UI.resize([rectangle, diamond, ellipse], "se", move);
|
UI.resize([rectangle, diamond, ellipse], "se", move, {
|
||||||
|
shift: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(rectangle.x).toBeCloseTo(0);
|
expect(rectangle.x).toBeCloseTo(0);
|
||||||
expect(rectangle.y).toBeCloseTo(0);
|
expect(rectangle.y).toBeCloseTo(0);
|
||||||
|
@ -613,7 +615,9 @@ describe("multiple selection", () => {
|
||||||
1 + move[1] / selectionHeight,
|
1 + move[1] / selectionHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
UI.resize([line, freedraw], "se", move);
|
UI.resize([line, freedraw], "se", move, {
|
||||||
|
shift: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(line.x).toBeCloseTo(60 * scale);
|
expect(line.x).toBeCloseTo(60 * scale);
|
||||||
expect(line.y).toBeCloseTo(40 * scale);
|
expect(line.y).toBeCloseTo(40 * scale);
|
||||||
|
@ -653,7 +657,9 @@ describe("multiple selection", () => {
|
||||||
1 - move[1] / selectionHeight,
|
1 - move[1] / selectionHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
UI.resize([horizLine, vertLine, diagLine], "nw", move);
|
UI.resize([horizLine, vertLine, diagLine], "nw", move, {
|
||||||
|
shift: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(horizLine.x).toBeCloseTo(selectionWidth * (1 - scale));
|
expect(horizLine.x).toBeCloseTo(selectionWidth * (1 - scale));
|
||||||
expect(horizLine.y).toBeCloseTo(selectionHeight * (1 - scale));
|
expect(horizLine.y).toBeCloseTo(selectionHeight * (1 - scale));
|
||||||
|
@ -703,7 +709,9 @@ describe("multiple selection", () => {
|
||||||
const rightArrowBinding = { ...rightBoundArrow.endBinding };
|
const rightArrowBinding = { ...rightBoundArrow.endBinding };
|
||||||
delete rightArrowBinding.gap;
|
delete rightArrowBinding.gap;
|
||||||
|
|
||||||
UI.resize([rectangle, rightBoundArrow], "nw", move);
|
UI.resize([rectangle, rightBoundArrow], "nw", move, {
|
||||||
|
shift: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||||
|
@ -751,7 +759,9 @@ describe("multiple selection", () => {
|
||||||
const move = [80, 0] as [number, number];
|
const move = [80, 0] as [number, number];
|
||||||
const scale = move[0] / selectionWidth + 1;
|
const scale = move[0] / selectionWidth + 1;
|
||||||
const elementsMap = arrayToMap(h.elements);
|
const elementsMap = arrayToMap(h.elements);
|
||||||
UI.resize([topArrow.get(), bottomArrow.get()], "se", move);
|
UI.resize([topArrow.get(), bottomArrow.get()], "se", move, {
|
||||||
|
shift: true,
|
||||||
|
});
|
||||||
const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
|
const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
|
||||||
topArrow,
|
topArrow,
|
||||||
topArrowLabel,
|
topArrowLabel,
|
||||||
|
@ -815,7 +825,7 @@ describe("multiple selection", () => {
|
||||||
1 - move[1] / selectionHeight,
|
1 - move[1] / selectionHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
UI.resize([topText, bottomText], "ne", move);
|
UI.resize([topText, bottomText], "ne", move, { shift: true });
|
||||||
|
|
||||||
expect(topText.x).toBeCloseTo(0);
|
expect(topText.x).toBeCloseTo(0);
|
||||||
expect(topText.y).toBeCloseTo(-selectionHeight * (scale - 1));
|
expect(topText.y).toBeCloseTo(-selectionHeight * (scale - 1));
|
||||||
|
@ -828,7 +838,7 @@ describe("multiple selection", () => {
|
||||||
expect(bottomText.angle).toEqual(0);
|
expect(bottomText.angle).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with images", () => {
|
it("resizes with images (proportional)", () => {
|
||||||
const topImage = API.createElement({
|
const topImage = API.createElement({
|
||||||
type: "image",
|
type: "image",
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -891,7 +901,7 @@ describe("multiple selection", () => {
|
||||||
1 + (2 * move[1]) / selectionHeight,
|
1 + (2 * move[1]) / selectionHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
UI.resize([rectangle, ellipse], "se", move, { alt: true });
|
UI.resize([rectangle, ellipse], "se", move, { shift: true, alt: true });
|
||||||
|
|
||||||
expect(rectangle.x).toBeCloseTo(-200 * scale);
|
expect(rectangle.x).toBeCloseTo(-200 * scale);
|
||||||
expect(rectangle.y).toBeCloseTo(-140 * scale);
|
expect(rectangle.y).toBeCloseTo(-140 * scale);
|
||||||
|
@ -954,7 +964,9 @@ describe("multiple selection", () => {
|
||||||
const scaleY = -scaleX;
|
const scaleY = -scaleX;
|
||||||
const lineOrigBounds = getBoundsFromPoints(line);
|
const lineOrigBounds = getBoundsFromPoints(line);
|
||||||
const elementsMap = arrayToMap(h.elements);
|
const elementsMap = arrayToMap(h.elements);
|
||||||
UI.resize([line, image, rectangle, boundArrow], "se", move);
|
UI.resize([line, image, rectangle, boundArrow], "se", move, {
|
||||||
|
shift: true,
|
||||||
|
});
|
||||||
const lineNewBounds = getBoundsFromPoints(line);
|
const lineNewBounds = getBoundsFromPoints(line);
|
||||||
const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
|
const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
|
||||||
boundArrow,
|
boundArrow,
|
||||||
|
@ -979,7 +991,7 @@ describe("multiple selection", () => {
|
||||||
expect(image.width).toBeCloseTo(100 * -scaleX);
|
expect(image.width).toBeCloseTo(100 * -scaleX);
|
||||||
expect(image.height).toBeCloseTo(100 * scaleY);
|
expect(image.height).toBeCloseTo(100 * scaleY);
|
||||||
expect(image.angle).toBeCloseTo((Math.PI * 5) / 6);
|
expect(image.angle).toBeCloseTo((Math.PI * 5) / 6);
|
||||||
expect(image.scale).toEqual([1, 1]);
|
expect(image.scale).toEqual([-1, 1]);
|
||||||
|
|
||||||
expect(rectangle.x).toBeCloseTo((180 + 160) * scaleX);
|
expect(rectangle.x).toBeCloseTo((180 + 160) * scaleX);
|
||||||
expect(rectangle.y).toBeCloseTo(60 * scaleY);
|
expect(rectangle.y).toBeCloseTo(60 * scaleY);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue