Compare commits

...

39 Commits

Author SHA1 Message Date
Ryan Di 9954e5f3c2 use crop hash in svg export to fix multi instance crop error 2024-10-22 17:16:19 +08:00
Ryan Di 0cc70d07c7 add test for ratio cropping 2024-10-15 17:34:53 +08:00
Ryan Di 359a2f23ae remove rounding 2024-10-15 16:34:48 +08:00
Ryan Di 467a4a2a6a shift to crop with initial aspect ratio 2024-10-15 16:23:00 +08:00
Ryan Di a7b55da49e remove todo 2024-10-15 14:09:03 +08:00
Ryan Di 4f82c32861 fix image hit and increase mini size to 10 2024-10-11 17:58:03 +08:00
dwelle 8701a5f843 fix: actionCropEditor not working 2024-10-10 23:58:52 +02:00
dwelle c498247e0f fix: undo/redo 2024-10-10 23:58:28 +02:00
Ryan Di 81e1d61d42 demo: image selection with no padding 2024-10-09 19:44:08 +08:00
Ryan Di 741c8dec31 apply projection to angle 0 drag move as well 2024-10-09 19:17:57 +08:00
Ryan Di da0481683a change cropElement to id to work with undo/redo 2024-10-09 19:07:40 +08:00
Ryan Di 58307a96dc normalize before projection to improve rotated crop region drag 2024-10-09 18:16:09 +08:00
Ryan Di 1a0755cd56 add shortcut to context menu 2024-10-09 17:53:53 +08:00
Ryan Di c742b4847f center icon 2024-10-09 17:46:08 +08:00
Ryan Di bcb50188b8 add diagram link 2024-10-09 14:54:18 +08:00
Ryan Di bdb1ec387d add missing type 2024-10-09 13:33:22 +08:00
Ryan Di b0375fe5db refactor to simplify tests 2024-10-09 13:28:17 +08:00
Ryan Di 940099f85d lint 2024-10-09 13:27:45 +08:00
Ryan Di e30fd9960d account for rotation when moving the crop region 2024-10-08 12:04:51 +08:00
Ryan Di 80ff1562b8 improve discoverability 2024-10-07 21:21:06 +08:00
Ryan Di 3f00762a77 svg export 2024-10-07 17:56:37 +08:00
Ryan Di 7b012b1cad merge with master 2024-10-04 17:08:36 +08:00
Ryan Di a02c4cb140 adjust crop handles 2024-10-04 16:50:42 +08:00
Ryan Di 3a01122093 make cropping work with flipping 2024-10-04 14:27:13 +08:00
David Luzar 47ee8a0094 refactor: point() -> pointFrom() to fix compiler issue (#8578) 2024-10-01 21:27:17 +02:00
Ryan Di bccd2bf30d simplify properties further 2024-09-30 16:11:00 +08:00
Subhadeep Sengupta a977dd1bf5 feat: Added reddit links as embeddable (#8099)
feat: #8063 Added reddit links as embeddable

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2024-09-28 11:49:18 +05:30
Ryan Di 50e4a0b37d set a min width and height for crop 2024-09-25 16:04:18 +08:00
Ryan Di 9e80c9e3dd do not resize when in crop mode 2024-09-25 15:59:00 +08:00
Ryan Di afdf38e47c init width and height when dragging to create img 2024-09-25 15:38:01 +08:00
Ryan Di 06a7a51baa simplify app and pointer down state 2024-09-25 15:33:19 +08:00
Aakansha Doshi 3fe1883f3f feat: prefer user defined coords and dimensions over calculated for for frame (#8517)
* feat: prefer user defined coords and dimensions over calculated for frame

* update changelog

* lint

* show the info only in dev mode and when children present
2024-09-24 21:09:15 +05:30
Marcel Mraz a80cb5896a feat: self-hosting existing google fonts (#8540) 2024-09-24 17:30:21 +02:00
David Luzar 6dfa18414a test: decrease min coverage thresholds (#8541) 2024-09-24 12:01:28 +00:00
Ryan Di 064bede0c5 simplify crop properties 2024-09-24 15:59:09 +08:00
Ryan Di 997fec6c75 manipulate and update a crop 2024-09-21 14:09:47 +08:00
Ryan Di 71ed96eabb render crop 2024-09-21 14:09:23 +08:00
Ryan Di f4bebaaa50 add properties for crop 2024-09-21 14:07:09 +08:00
Ryan Di 3d92a3bf1f double click to start cropping 2024-09-09 18:34:13 +08:00
103 changed files with 3224 additions and 1072 deletions
-9
View File
@@ -130,15 +130,6 @@
</script> </script>
<% } %> <% } %>
<!-- For Nunito only preload the latin range, which should be good enough for now -->
<link
rel="preload"
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<!-- Register Assistant as the UI font, before the scene inits --> <!-- Register Assistant as the UI font, before the scene inits -->
<link <link
rel="stylesheet" rel="stylesheet"
+2
View File
@@ -48,6 +48,8 @@ export default defineConfig({
}, },
}, },
sourcemap: true, sourcemap: true,
// don't auto-inline small assets (i.e. fonts hosted on CDN)
assetsInlineLimit: 0,
}, },
plugins: [ plugins: [
woff2BrowserPlugin(), woff2BrowserPlugin(),
+2
View File
@@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section.
### Features ### Features
- Prefer user defined coordinates and dimensions when creating a frame using [`convertToExcalidrawElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements) [#8517](https://github.com/excalidraw/excalidraw/pull/8517)
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135) - `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348) - Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
@@ -0,0 +1,55 @@
import { register } from "./register";
import { cropIcon } from "../components/icons";
import { StoreAction } from "../store";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { isImageElement } from "../element/typeChecks";
import type { ExcalidrawImageElement } from "../element/types";
export const actionToggleCropEditor = register({
name: "cropEditor",
label: "helpDialog.cropStart",
icon: cropIcon,
viewMode: true,
trackEvent: { category: "menu" },
keywords: ["image", "crop"],
perform(elements, appState, _, app) {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})[0] as ExcalidrawImageElement;
return {
appState: {
...appState,
isCropping: false,
croppingElementId: selectedElement.id,
},
storeAction: StoreAction.CAPTURE,
};
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
if (
!appState.croppingElementId &&
selectedElements.length === 1 &&
isImageElement(selectedElements[0])
) {
return true;
}
return false;
},
PanelComponent: ({ appState, updateData, app }) => {
const label = t("helpDialog.cropStart");
return (
<ToolButton
type="button"
icon={cropIcon}
title={label}
aria-label={label}
onClick={() => updateData(null)}
/>
);
},
});
@@ -15,7 +15,7 @@ import { isBindingElement, isLinearElement } from "../element/typeChecks";
import type { AppState } from "../types"; import type { AppState } from "../types";
import { resetCursor } from "../cursor"; import { resetCursor } from "../cursor";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import { point } from "../../math"; import { pointFrom } from "../../math";
import { isPathALoop } from "../shapes"; import { isPathALoop } from "../shapes";
export const actionFinalize = register({ export const actionFinalize = register({
@@ -115,7 +115,7 @@ export const actionFinalize = register({
mutateElement(multiPointElement, { mutateElement(multiPointElement, {
points: linePoints.map((p, index) => points: linePoints.map((p, index) =>
index === linePoints.length - 1 index === linePoints.length - 1
? point(firstPoint[0], firstPoint[1]) ? pointFrom(firstPoint[0], firstPoint[1])
: p, : p,
), ),
}); });
@@ -2,7 +2,7 @@ import React from "react";
import { Excalidraw } from "../index"; import { Excalidraw } from "../index";
import { render } from "../tests/test-utils"; import { render } from "../tests/test-utils";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { point } from "../../math"; import { pointFrom } from "../../math";
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip"; import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
const { h } = window; const { h } = window;
@@ -50,11 +50,11 @@ describe("flipping re-centers selection", () => {
startArrowhead: null, startArrowhead: null,
endArrowhead: "arrow", endArrowhead: "arrow",
points: [ points: [
point(0, 0), pointFrom(0, 0),
point(0, -35), pointFrom(0, -35),
point(-90.9, -35), pointFrom(-90.9, -35),
point(-90.9, 204.9), pointFrom(-90.9, 204.9),
point(65.1, 204.9), pointFrom(65.1, 204.9),
], ],
elbowed: true, elbowed: true,
}), }),
@@ -116,7 +116,7 @@ import {
import { mutateElbowArrow } from "../element/routing"; import { mutateElbowArrow } from "../element/routing";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { point, vector } from "../../math"; import { pointFrom, vector } from "../../math";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@@ -1651,7 +1651,7 @@ export const actionChangeArrowType = register({
elementsMap, elementsMap,
[finalStartPoint, finalEndPoint].map( [finalStartPoint, finalEndPoint].map(
(p): LocalPoint => (p): LocalPoint =>
point(p[0] - newElement.x, p[1] - newElement.y), pointFrom(p[0] - newElement.x, p[1] - newElement.y),
), ),
vector(0, 0), vector(0, 0),
{ {
+2
View File
@@ -88,3 +88,5 @@ export { actionToggleElementLock } from "./actionElementLock";
export { actionToggleLinearEditor } from "./actionLinearEditor"; export { actionToggleLinearEditor } from "./actionLinearEditor";
export { actionToggleSearchMenu } from "./actionToggleSearchMenu"; export { actionToggleSearchMenu } from "./actionToggleSearchMenu";
export { actionToggleCropEditor } from "./actionCropEditor";
+2 -1
View File
@@ -138,7 +138,8 @@ export type ActionName =
| "commandPalette" | "commandPalette"
| "autoResize" | "autoResize"
| "elementStats" | "elementStats"
| "searchMenu"; | "searchMenu"
| "cropEditor";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
+4
View File
@@ -116,6 +116,8 @@ export const getDefaultAppState = (): Omit<
objectsSnapModeEnabled: false, objectsSnapModeEnabled: false,
userToFollow: null, userToFollow: null,
followedBy: new Set(), followedBy: new Set(),
isCropping: false,
croppingElementId: null,
searchMatches: [], searchMatches: [],
}; };
}; };
@@ -237,6 +239,8 @@ const APP_STATE_STORAGE_CONF = (<
objectsSnapModeEnabled: { browser: true, export: false, server: false }, objectsSnapModeEnabled: { browser: true, export: false, server: false },
userToFollow: { browser: false, export: false, server: false }, userToFollow: { browser: false, export: false, server: false },
followedBy: { browser: false, export: false, server: false }, followedBy: { browser: false, export: false, server: false },
isCropping: { browser: false, export: false, server: false },
croppingElementId: { browser: false, export: false, server: false },
searchMatches: { browser: false, export: false, server: false }, searchMatches: { browser: false, export: false, server: false },
}); });
+32 -1
View File
@@ -17,13 +17,16 @@ import {
hasBoundTextElement, hasBoundTextElement,
isBindableElement, isBindableElement,
isBoundToContainer, isBoundToContainer,
isImageElement,
isTextElement, isTextElement,
} from "./element/typeChecks"; } from "./element/typeChecks";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElement, ExcalidrawTextElement,
NonDeleted, NonDeleted,
Ordered,
OrderedExcalidrawElement, OrderedExcalidrawElement,
SceneElementsMap, SceneElementsMap,
} from "./element/types"; } from "./element/types";
@@ -626,6 +629,18 @@ export class AppStateChange implements Change<AppState> {
); );
break; break;
case "croppingElementId": {
const croppingElementId = nextAppState[key];
const element =
croppingElementId && nextElements.get(croppingElementId);
if (element && !element.isDeleted) {
visibleDifferenceFlag.value = true;
} else {
nextAppState[key] = null;
}
break;
}
case "editingGroupId": case "editingGroupId":
const editingGroupId = nextAppState[key]; const editingGroupId = nextAppState[key];
@@ -756,6 +771,7 @@ export class AppStateChange implements Change<AppState> {
selectedElementIds, selectedElementIds,
editingLinearElementId, editingLinearElementId,
selectedLinearElementId, selectedLinearElementId,
croppingElementId,
...standaloneProps ...standaloneProps
} = delta as ObservedAppState; } = delta as ObservedAppState;
@@ -779,7 +795,10 @@ export class AppStateChange implements Change<AppState> {
} }
} }
type ElementPartial = Omit<ElementUpdate<OrderedExcalidrawElement>, "seed">; type ElementPartial<T extends ExcalidrawElement = ExcalidrawElement> = Omit<
ElementUpdate<Ordered<T>>,
"seed"
>;
/** /**
* Elements change is a low level primitive to capture a change between two sets of elements. * Elements change is a low level primitive to capture a change between two sets of elements.
@@ -1216,6 +1235,18 @@ export class ElementsChange implements Change<SceneElementsMap> {
}); });
} }
if (isImageElement(element)) {
const _delta = delta as Delta<ElementPartial<ExcalidrawImageElement>>;
// we want to override `crop` only if modified so that we don't reset
// when undoing/redoing unrelated change
if (_delta.deleted.crop || _delta.inserted.crop) {
Object.assign(directlyApplicablePartial, {
// apply change verbatim
crop: _delta.inserted.crop ?? null,
});
}
}
if (!flags.containsVisibleDifference) { if (!flags.containsVisibleDifference) {
// strip away fractional as even if it would be different, it doesn't have to result in visible change // strip away fractional as even if it would be different, it doesn't have to result in visible change
const { index, ...rest } = directlyApplicablePartial; const { index, ...rest } = directlyApplicablePartial;
+5 -5
View File
@@ -1,5 +1,5 @@
import type { Radians } from "../math"; import type { Radians } from "../math";
import { point } from "../math"; import { pointFrom } from "../math";
import { import {
COLOR_PALETTE, COLOR_PALETTE,
DEFAULT_CHART_COLOR_INDEX, DEFAULT_CHART_COLOR_INDEX,
@@ -260,7 +260,7 @@ const chartLines = (
x, x,
y, y,
width: chartWidth, width: chartWidth,
points: [point(0, 0), point(chartWidth, 0)], points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
}); });
const yLine = newLinearElement({ const yLine = newLinearElement({
@@ -271,7 +271,7 @@ const chartLines = (
x, x,
y, y,
height: chartHeight, height: chartHeight,
points: [point(0, 0), point(0, -chartHeight)], points: [pointFrom(0, 0), pointFrom(0, -chartHeight)],
}); });
const maxLine = newLinearElement({ const maxLine = newLinearElement({
@@ -284,7 +284,7 @@ const chartLines = (
strokeStyle: "dotted", strokeStyle: "dotted",
width: chartWidth, width: chartWidth,
opacity: GRID_OPACITY, opacity: GRID_OPACITY,
points: [point(0, 0), point(chartWidth, 0)], points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
}); });
return [xLine, yLine, maxLine]; return [xLine, yLine, maxLine];
@@ -441,7 +441,7 @@ const chartTypeLine = (
height: cy, height: cy,
strokeStyle: "dotted", strokeStyle: "dotted",
opacity: GRID_OPACITY, opacity: GRID_OPACITY,
points: [point(0, 0), point(0, cy)], points: [pointFrom(0, 0), pointFrom(0, cy)],
}); });
}); });
@@ -26,6 +26,7 @@ import { trackEvent } from "../analytics";
import { import {
hasBoundTextElement, hasBoundTextElement,
isElbowArrow, isElbowArrow,
isImageElement,
isLinearElement, isLinearElement,
isTextElement, isTextElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
@@ -127,6 +128,11 @@ export const SelectedShapeActions = ({
isLinearElement(targetElements[0]) && isLinearElement(targetElements[0]) &&
!isElbowArrow(targetElements[0]); !isElbowArrow(targetElements[0]);
const showCropEditorAction =
!appState.croppingElementId &&
targetElements.length === 1 &&
isImageElement(targetElements[0]);
return ( return (
<div className="panelColumn"> <div className="panelColumn">
<div> <div>
@@ -245,6 +251,7 @@ export const SelectedShapeActions = ({
{renderAction("group")} {renderAction("group")}
{renderAction("ungroup")} {renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")} {showLinkIcon && renderAction("hyperlink")}
{showCropEditorAction && renderAction("cropEditor")}
{showLineEditorAction && renderAction("toggleLinearEditor")} {showLineEditorAction && renderAction("toggleLinearEditor")}
</div> </div>
</fieldset> </fieldset>
+333 -42
View File
@@ -35,6 +35,7 @@ import {
actionToggleElementLock, actionToggleElementLock,
actionToggleLinearEditor, actionToggleLinearEditor,
actionToggleObjectsSnapMode, actionToggleObjectsSnapMode,
actionToggleCropEditor,
} from "../actions"; } from "../actions";
import { createRedoAction, createUndoAction } from "../actions/actionHistory"; import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
@@ -445,7 +446,19 @@ import {
} from "../element/flowchart"; } from "../element/flowchart";
import { searchItemInFocusAtom } from "./SearchMenu"; import { searchItemInFocusAtom } from "./SearchMenu";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { point, pointDistance, vector } from "../../math"; import {
clamp,
pointFrom,
pointDistance,
vector,
pointRotateRads,
vectorScale,
vectorFromPoint,
vectorSubtract,
vectorDot,
vectorNormalize,
} from "../../math";
import { cropElement } from "../element/cropElement";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
@@ -589,6 +602,7 @@ class App extends React.Component<AppProps, AppState> {
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null = lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
null; null;
lastPointerMoveEvent: PointerEvent | null = null; lastPointerMoveEvent: PointerEvent | null = null;
lastPointerMoveCoords: { x: number; y: number } | null = null;
lastViewportPosition = { x: 0, y: 0 }; lastViewportPosition = { x: 0, y: 0 };
animationFrameHandler = new AnimationFrameHandler(); animationFrameHandler = new AnimationFrameHandler();
@@ -3923,6 +3937,28 @@ class App extends React.Component<AppProps, AppState> {
} }
if (!isInputLike(event.target)) { if (!isInputLike(event.target)) {
if (
(event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
this.state.croppingElementId
) {
this.finishImageCropping();
return;
}
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElementsMap(),
this.state,
);
if (
selectedElements.length === 1 &&
isImageElement(selectedElements[0]) &&
event.key === KEYS.ENTER
) {
this.startImageCropping(selectedElements[0]);
return;
}
if ( if (
event.key === KEYS.ESCAPE && event.key === KEYS.ESCAPE &&
this.flowChartCreator.isCreatingChart this.flowChartCreator.isCreatingChart
@@ -4907,10 +4943,10 @@ class App extends React.Component<AppProps, AppState> {
const selectionShape = getSelectionBoxShape( const selectionShape = getSelectionBoxShape(
element, element,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.getElementHitThreshold(), isImageElement(element) ? 0 : this.getElementHitThreshold(),
); );
return isPointInShape(point(x, y), selectionShape); return isPointInShape(pointFrom(x, y), selectionShape);
} }
// take bound text element into consideration for hit collision as well // take bound text element into consideration for hit collision as well
@@ -5136,6 +5172,22 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private startImageCropping = (image: ExcalidrawImageElement) => {
this.store.shouldCaptureIncrement();
this.setState({
croppingElementId: image.id,
});
};
private finishImageCropping = () => {
if (this.state.croppingElementId) {
this.store.shouldCaptureIncrement();
this.setState({
croppingElementId: null,
});
}
};
private handleCanvasDoubleClick = ( private handleCanvasDoubleClick = (
event: React.MouseEvent<HTMLCanvasElement>, event: React.MouseEvent<HTMLCanvasElement>,
) => { ) => {
@@ -5167,6 +5219,11 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
this.startImageCropping(selectedElements[0]);
return;
}
resetCursor(this.interactiveCanvas); resetCursor(this.interactiveCanvas);
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
@@ -5269,7 +5326,7 @@ class App extends React.Component<AppProps, AppState> {
element, element,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state, this.state,
point(scenePointer.x, scenePointer.y), pointFrom(scenePointer.x, scenePointer.y),
this.device.editor.isMobile, this.device.editor.isMobile,
) )
); );
@@ -5281,11 +5338,14 @@ class App extends React.Component<AppProps, AppState> {
isTouchScreen: boolean, isTouchScreen: boolean,
) => { ) => {
const draggedDistance = pointDistance( const draggedDistance = pointDistance(
point( pointFrom(
this.lastPointerDownEvent!.clientX, this.lastPointerDownEvent!.clientX,
this.lastPointerDownEvent!.clientY, this.lastPointerDownEvent!.clientY,
), ),
point(this.lastPointerUpEvent!.clientX, this.lastPointerUpEvent!.clientY), pointFrom(
this.lastPointerUpEvent!.clientX,
this.lastPointerUpEvent!.clientY,
),
); );
if ( if (
!this.hitLinkElement || !this.hitLinkElement ||
@@ -5304,7 +5364,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement, this.hitLinkElement,
elementsMap, elementsMap,
this.state, this.state,
point(lastPointerDownCoords.x, lastPointerDownCoords.y), pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y),
this.device.editor.isMobile, this.device.editor.isMobile,
); );
const lastPointerUpCoords = viewportCoordsToSceneCoords( const lastPointerUpCoords = viewportCoordsToSceneCoords(
@@ -5315,7 +5375,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement, this.hitLinkElement,
elementsMap, elementsMap,
this.state, this.state,
point(lastPointerUpCoords.x, lastPointerUpCoords.y), pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y),
this.device.editor.isMobile, this.device.editor.isMobile,
); );
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) { if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
@@ -5565,7 +5625,7 @@ class App extends React.Component<AppProps, AppState> {
// threshold, add a point // threshold, add a point
if ( if (
pointDistance( pointDistance(
point(scenePointerX - rx, scenePointerY - ry), pointFrom(scenePointerX - rx, scenePointerY - ry),
lastPoint, lastPoint,
) >= LINE_CONFIRM_THRESHOLD ) >= LINE_CONFIRM_THRESHOLD
) { ) {
@@ -5574,7 +5634,7 @@ class App extends React.Component<AppProps, AppState> {
{ {
points: [ points: [
...points, ...points,
point<LocalPoint>(scenePointerX - rx, scenePointerY - ry), pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
], ],
}, },
false, false,
@@ -5588,7 +5648,7 @@ class App extends React.Component<AppProps, AppState> {
points.length > 2 && points.length > 2 &&
lastCommittedPoint && lastCommittedPoint &&
pointDistance( pointDistance(
point(scenePointerX - rx, scenePointerY - ry), pointFrom(scenePointerX - rx, scenePointerY - ry),
lastCommittedPoint, lastCommittedPoint,
) < LINE_CONFIRM_THRESHOLD ) < LINE_CONFIRM_THRESHOLD
) { ) {
@@ -5636,7 +5696,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
[ [
...points.slice(0, -1), ...points.slice(0, -1),
point<LocalPoint>( pointFrom<LocalPoint>(
lastCommittedX + dxFromLastCommitted, lastCommittedX + dxFromLastCommitted,
lastCommittedY + dyFromLastCommitted, lastCommittedY + dyFromLastCommitted,
), ),
@@ -5655,7 +5715,7 @@ class App extends React.Component<AppProps, AppState> {
{ {
points: [ points: [
...points.slice(0, -1), ...points.slice(0, -1),
point<LocalPoint>( pointFrom<LocalPoint>(
lastCommittedX + dxFromLastCommitted, lastCommittedX + dxFromLastCommitted,
lastCommittedY + dyFromLastCommitted, lastCommittedY + dyFromLastCommitted,
), ),
@@ -5884,8 +5944,8 @@ class App extends React.Component<AppProps, AppState> {
}; };
const distance = pointDistance( const distance = pointDistance(
point(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y), pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
point(scenePointer.x, scenePointer.y), pointFrom(scenePointer.x, scenePointer.y),
); );
const threshold = this.getElementHitThreshold(); const threshold = this.getElementHitThreshold();
const p = { ...pointerDownState.lastCoords }; const p = { ...pointerDownState.lastCoords };
@@ -6397,7 +6457,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement, this.hitLinkElement,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.state, this.state,
point(scenePointer.x, scenePointer.y), pointFrom(scenePointer.x, scenePointer.y),
) )
) { ) {
this.handleEmbeddableCenterClick(this.hitLinkElement); this.handleEmbeddableCenterClick(this.hitLinkElement);
@@ -6733,11 +6793,24 @@ class App extends React.Component<AppProps, AppState> {
this.device, this.device,
); );
if (elementWithTransformHandleType != null) { if (elementWithTransformHandleType != null) {
this.setState({ if (
resizingElement: elementWithTransformHandleType.element, elementWithTransformHandleType.transformHandleType === "rotation"
}); ) {
pointerDownState.resize.handleType = this.setState({
elementWithTransformHandleType.transformHandleType; resizingElement: elementWithTransformHandleType.element,
});
pointerDownState.resize.handleType =
elementWithTransformHandleType.transformHandleType;
} else if (this.state.croppingElementId) {
pointerDownState.resize.handleType =
elementWithTransformHandleType.transformHandleType;
} else {
this.setState({
resizingElement: elementWithTransformHandleType.element,
});
pointerDownState.resize.handleType =
elementWithTransformHandleType.transformHandleType;
}
} }
} else if (selectedElements.length > 1) { } else if (selectedElements.length > 1) {
pointerDownState.resize.handleType = getTransformHandleTypeFromCoords( pointerDownState.resize.handleType = getTransformHandleTypeFromCoords(
@@ -6804,6 +6877,13 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
); );
if (
this.state.croppingElementId &&
pointerDownState.hit.element?.id !== this.state.croppingElementId
) {
this.finishImageCropping();
}
if (pointerDownState.hit.element) { if (pointerDownState.hit.element) {
// Early return if pointer is hitting link icon // Early return if pointer is hitting link icon
const hitLinkElement = this.getElementLinkAtPosition( const hitLinkElement = this.getElementLinkAtPosition(
@@ -7088,7 +7168,7 @@ class App extends React.Component<AppProps, AppState> {
simulatePressure, simulatePressure,
locked: false, locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
points: [point<LocalPoint>(0, 0)], points: [pointFrom<LocalPoint>(0, 0)],
pressures: simulatePressure ? [] : [event.pressure], pressures: simulatePressure ? [] : [event.pressure],
}); });
@@ -7297,7 +7377,10 @@ class App extends React.Component<AppProps, AppState> {
multiElement.points.length > 1 && multiElement.points.length > 1 &&
lastCommittedPoint && lastCommittedPoint &&
pointDistance( pointDistance(
point(pointerDownState.origin.x - rx, pointerDownState.origin.y - ry), pointFrom(
pointerDownState.origin.x - rx,
pointerDownState.origin.y - ry,
),
lastCommittedPoint, lastCommittedPoint,
) < LINE_CONFIRM_THRESHOLD ) < LINE_CONFIRM_THRESHOLD
) { ) {
@@ -7399,7 +7482,7 @@ class App extends React.Component<AppProps, AppState> {
}; };
}); });
mutateElement(element, { mutateElement(element, {
points: [...element.points, point<LocalPoint>(0, 0)], points: [...element.points, pointFrom<LocalPoint>(0, 0)],
}); });
const boundElement = getHoveredElementForBinding( const boundElement = getHoveredElementForBinding(
pointerDownState.origin, pointerDownState.origin,
@@ -7602,6 +7685,11 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
) { ) {
return withBatchedUpdatesThrottled((event: PointerEvent) => { return withBatchedUpdatesThrottled((event: PointerEvent) => {
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
const lastPointerCoords =
this.lastPointerMoveCoords ?? pointerDownState.origin;
this.lastPointerMoveCoords = pointerCoords;
// We need to initialize dragOffsetXY only after we've updated // We need to initialize dragOffsetXY only after we've updated
// `state.selectedElementIds` on pointerDown. Doing it here in pointerMove // `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
// event handler should hopefully ensure we're already working with // event handler should hopefully ensure we're already working with
@@ -7624,8 +7712,6 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
if (isEraserActive(this.state)) { if (isEraserActive(this.state)) {
this.handleEraser(event, pointerDownState, pointerCoords); this.handleEraser(event, pointerDownState, pointerCoords);
return; return;
@@ -7652,8 +7738,8 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
if ( if (
pointDistance( pointDistance(
point(pointerCoords.x, pointerCoords.y), pointFrom(pointerCoords.x, pointerCoords.y),
point(pointerDownState.origin.x, pointerDownState.origin.y), pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
) < DRAGGING_THRESHOLD ) < DRAGGING_THRESHOLD
) { ) {
return; return;
@@ -7662,6 +7748,9 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.resize.isResizing) { if (pointerDownState.resize.isResizing) {
pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y; pointerDownState.lastCoords.y = pointerCoords.y;
if (this.maybeHandleCrop(pointerDownState, event)) {
return true;
}
if (this.maybeHandleResize(pointerDownState, event)) { if (this.maybeHandleResize(pointerDownState, event)) {
return true; return true;
} }
@@ -7835,6 +7924,96 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
// #region move crop region
if (this.state.croppingElementId) {
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(this.state.croppingElementId);
if (
croppingElement &&
isImageElement(croppingElement) &&
croppingElement.crop !== null &&
pointerDownState.hit.element === croppingElement
) {
const crop = croppingElement.crop;
const image =
isInitializedImageElement(croppingElement) &&
this.imageCache.get(croppingElement.fileId)?.image;
if (image && !(image instanceof Promise)) {
const instantDragOffset = vectorScale(
vector(
pointerCoords.x - lastPointerCoords.x,
pointerCoords.y - lastPointerCoords.y,
),
Math.max(this.state.zoom.value, 2),
);
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
croppingElement,
elementsMap,
);
const topLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y1),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const topRight = vectorFromPoint(
pointRotateRads(
pointFrom(x2, y1),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const bottomLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y2),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const topEdge = vectorNormalize(
vectorSubtract(topRight, topLeft),
);
const leftEdge = vectorNormalize(
vectorSubtract(bottomLeft, topLeft),
);
// project instantDrafOffset onto leftEdge and topEdge to decompose
const offsetVector = vector(
vectorDot(instantDragOffset, topEdge),
vectorDot(instantDragOffset, leftEdge),
);
const nextCrop = {
...crop,
x: clamp(
crop.x -
offsetVector[0] * Math.sign(croppingElement.scale[0]),
0,
image.naturalWidth - crop.width,
),
y: clamp(
crop.y -
offsetVector[1] * Math.sign(croppingElement.scale[1]),
0,
image.naturalHeight - crop.height,
),
};
mutateElement(croppingElement, {
crop: nextCrop,
});
return;
}
}
}
// Snap cache *must* be synchronously popuplated before initial drag, // Snap cache *must* be synchronously popuplated before initial drag,
// otherwise the first drag even will not snap, causing a jump before // otherwise the first drag even will not snap, causing a jump before
// it snaps to its position if previously snapped already. // it snaps to its position if previously snapped already.
@@ -7968,6 +8147,7 @@ class App extends React.Component<AppProps, AppState> {
this.maybeCacheVisibleGaps(event, selectedElements, true); this.maybeCacheVisibleGaps(event, selectedElements, true);
this.maybeCacheReferenceSnapPoints(event, selectedElements, true); this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
} }
return; return;
} }
} }
@@ -8002,7 +8182,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points, point<LocalPoint>(dx, dy)], points: [...points, pointFrom<LocalPoint>(dx, dy)],
pressures, pressures,
}, },
false, false,
@@ -8031,7 +8211,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points, point<LocalPoint>(dx, dy)], points: [...points, pointFrom<LocalPoint>(dx, dy)],
}, },
false, false,
); );
@@ -8039,7 +8219,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElbowArrow( mutateElbowArrow(
newElement, newElement,
elementsMap, elementsMap,
[...points.slice(0, -1), point<LocalPoint>(dx, dy)], [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
vector(0, 0), vector(0, 0),
undefined, undefined,
{ {
@@ -8051,7 +8231,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points.slice(0, -1), point<LocalPoint>(dx, dy)], points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
}, },
false, false,
); );
@@ -8216,15 +8396,18 @@ class App extends React.Component<AppProps, AppState> {
const { const {
newElement, newElement,
resizingElement, resizingElement,
croppingElementId,
multiElement, multiElement,
activeTool, activeTool,
isResizing, isResizing,
isRotating, isRotating,
isCropping,
} = this.state; } = this.state;
this.setState((prevState) => ({ this.setState((prevState) => ({
isResizing: false, isResizing: false,
isRotating: false, isRotating: false,
isCropping: false,
resizingElement: null, resizingElement: null,
selectionElement: null, selectionElement: null,
frameToHighlight: null, frameToHighlight: null,
@@ -8234,6 +8417,8 @@ class App extends React.Component<AppProps, AppState> {
originSnapOffset: null, originSnapOffset: null,
})); }));
this.lastPointerMoveCoords = null;
SnapCache.setReferenceSnapPoints(null); SnapCache.setReferenceSnapPoints(null);
SnapCache.setVisibleGaps(null); SnapCache.setVisibleGaps(null);
@@ -8360,9 +8545,9 @@ class App extends React.Component<AppProps, AppState> {
: [...newElement.pressures, childEvent.pressure]; : [...newElement.pressures, childEvent.pressure];
mutateElement(newElement, { mutateElement(newElement, {
points: [...points, point<LocalPoint>(dx, dy)], points: [...points, pointFrom<LocalPoint>(dx, dy)],
pressures, pressures,
lastCommittedPoint: point<LocalPoint>(dx, dy), lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
}); });
this.actionManager.executeAction(actionFinalize); this.actionManager.executeAction(actionFinalize);
@@ -8409,7 +8594,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement(newElement, { mutateElement(newElement, {
points: [ points: [
...newElement.points, ...newElement.points,
point<LocalPoint>( pointFrom<LocalPoint>(
pointerCoords.x - newElement.x, pointerCoords.x - newElement.x,
pointerCoords.y - newElement.y, pointerCoords.y - newElement.y,
), ),
@@ -8716,6 +8901,20 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
// click outside the cropping region to exit
if (
// not in the cropping mode at all
!croppingElementId ||
// in the cropping mode
(croppingElementId &&
// not cropping and no hit element
((!hitElement && !isCropping) ||
// hitting something else
(hitElement && hitElement.id !== croppingElementId)))
) {
this.finishImageCropping();
}
const pointerStart = this.lastPointerDownEvent; const pointerStart = this.lastPointerDownEvent;
const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent; const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent;
@@ -8723,8 +8922,8 @@ class App extends React.Component<AppProps, AppState> {
this.eraserTrail.endPath(); this.eraserTrail.endPath();
const draggedDistance = pointDistance( const draggedDistance = pointDistance(
point(pointerStart.clientX, pointerStart.clientY), pointFrom(pointerStart.clientX, pointerStart.clientY),
point(pointerEnd.clientX, pointerEnd.clientY), pointFrom(pointerEnd.clientX, pointerEnd.clientY),
); );
if (draggedDistance === 0) { if (draggedDistance === 0) {
@@ -8971,7 +9170,12 @@ class App extends React.Component<AppProps, AppState> {
this.store.shouldCaptureIncrement(); this.store.shouldCaptureIncrement();
} }
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) { if (
pointerDownState.drag.hasOccurred ||
isResizing ||
isRotating ||
isCropping
) {
// We only allow binding via linear elements, specifically via dragging // We only allow binding via linear elements, specifically via dragging
// the endpoints ("start" or "end"). // the endpoints ("start" or "end").
const linearElements = this.scene const linearElements = this.scene
@@ -9185,7 +9389,7 @@ class App extends React.Component<AppProps, AppState> {
/** /**
* inserts image into elements array and rerenders * inserts image into elements array and rerenders
*/ */
private insertImageElement = async ( insertImageElement = async (
imageElement: ExcalidrawImageElement, imageElement: ExcalidrawImageElement,
imageFile: File, imageFile: File,
showCursorImagePreview?: boolean, showCursorImagePreview?: boolean,
@@ -9338,7 +9542,7 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private initializeImageDimensions = ( initializeImageDimensions = (
imageElement: ExcalidrawImageElement, imageElement: ExcalidrawImageElement,
forceNaturalSize = false, forceNaturalSize = false,
) => { ) => {
@@ -9386,7 +9590,13 @@ class App extends React.Component<AppProps, AppState> {
const x = imageElement.x + imageElement.width / 2 - width / 2; const x = imageElement.x + imageElement.width / 2 - width / 2;
const y = imageElement.y + imageElement.height / 2 - height / 2; const y = imageElement.y + imageElement.height / 2 - height / 2;
mutateElement(imageElement, { x, y, width, height }); mutateElement(imageElement, {
x,
y,
width,
height,
crop: null,
});
} }
}; };
@@ -9925,6 +10135,83 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private maybeHandleCrop = (
pointerDownState: PointerDownState,
event: MouseEvent | KeyboardEvent,
): boolean => {
// to crop, we must already be in the cropping mode, where croppingElement has been set
if (!this.state.croppingElementId) {
return false;
}
const transformHandleType = pointerDownState.resize.handleType;
const pointerCoords = pointerDownState.lastCoords;
const [x, y] = getGridPoint(
pointerCoords.x - pointerDownState.resize.offset.x,
pointerCoords.y - pointerDownState.resize.offset.y,
this.getEffectiveGridSize(),
);
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(this.state.croppingElementId);
if (
transformHandleType &&
croppingElement &&
isImageElement(croppingElement)
) {
const croppingAtStateStart = pointerDownState.originalElements.get(
croppingElement.id,
);
const image =
isInitializedImageElement(croppingElement) &&
this.imageCache.get(croppingElement.fileId)?.image;
if (
croppingAtStateStart &&
isImageElement(croppingAtStateStart) &&
image &&
!(image instanceof Promise)
) {
mutateElement(
croppingElement,
cropElement(
croppingElement,
transformHandleType,
image.naturalWidth,
image.naturalHeight,
x,
y,
event.shiftKey
? croppingAtStateStart.width / croppingAtStateStart.height
: undefined,
),
);
updateBoundElements(
croppingElement,
this.scene.getNonDeletedElementsMap(),
{
oldSize: {
width: croppingElement.width,
height: croppingElement.height,
},
},
);
this.setState({
isCropping: transformHandleType && transformHandleType !== "rotation",
});
}
return true;
}
return false;
};
private maybeHandleResize = ( private maybeHandleResize = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
event: MouseEvent | KeyboardEvent, event: MouseEvent | KeyboardEvent,
@@ -9941,7 +10228,9 @@ class App extends React.Component<AppProps, AppState> {
// Frames cannot be rotated. // Frames cannot be rotated.
(selectedFrames.length > 0 && transformHandleType === "rotation") || (selectedFrames.length > 0 && transformHandleType === "rotation") ||
// Elbow arrows cannot be transformed (resized or rotated). // Elbow arrows cannot be transformed (resized or rotated).
(selectedElements.length === 1 && isElbowArrow(selectedElements[0])) (selectedElements.length === 1 && isElbowArrow(selectedElements[0])) ||
// Do not resize when in crop mode
this.state.croppingElementId
) { ) {
return false; return false;
} }
@@ -10116,6 +10405,8 @@ class App extends React.Component<AppProps, AppState> {
actionSelectAllElementsInFrame, actionSelectAllElementsInFrame,
actionRemoveAllElementsFromFrame, actionRemoveAllElementsFromFrame,
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionToggleCropEditor,
CONTEXT_MENU_SEPARATOR,
...options, ...options,
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionCopyStyles, actionCopyStyles,
@@ -279,6 +279,7 @@ function CommandPaletteInner({
actionManager.actions.increaseFontSize, actionManager.actions.increaseFontSize,
actionManager.actions.decreaseFontSize, actionManager.actions.decreaseFontSize,
actionManager.actions.toggleLinearEditor, actionManager.actions.toggleLinearEditor,
actionManager.actions.cropEditor,
actionLink, actionLink,
].map((action: Action) => ].map((action: Action) =>
actionToCommand( actionToCommand(
@@ -222,6 +222,16 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
]} ]}
isOr={false} isOr={false}
/> />
<Shortcut
label={t("helpDialog.cropStart")}
shortcuts={[t("helpDialog.doubleClick"), getShortcutKey("Enter")]}
isOr={true}
/>
<Shortcut
label={t("helpDialog.cropFinish")}
shortcuts={[getShortcutKey("Enter"), getShortcutKey("Escape")]}
isOr={true}
/>
<Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} /> <Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} />
<Shortcut <Shortcut
label={t("helpDialog.preventBinding")} label={t("helpDialog.preventBinding")}
@@ -100,6 +100,14 @@ const getHints = ({
return t("hints.text_editing"); return t("hints.text_editing");
} }
if (appState.croppingElementId) {
return t("hints.leaveCropEditor");
}
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
return t("hints.enterCropEditor");
}
if (activeTool.type === "selection") { if (activeTool.type === "selection") {
if ( if (
appState.selectionElement && appState.selectionElement &&
@@ -20,7 +20,7 @@ import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
import { getElementsInAtomicUnit, resizeElement } from "./utils"; import { getElementsInAtomicUnit, resizeElement } from "./utils";
import type { AtomicUnit } from "./utils"; import type { AtomicUnit } from "./utils";
import { MIN_WIDTH_OR_HEIGHT } from "../../constants"; import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
import { point, type GlobalPoint } from "../../../math"; import { pointFrom, type GlobalPoint } from "../../../math";
interface MultiDimensionProps { interface MultiDimensionProps {
property: "width" | "height"; property: "width" | "height";
@@ -182,7 +182,7 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
initialHeight, initialHeight,
aspectRatio, aspectRatio,
point(x1, y1), pointFrom(x1, y1),
property, property,
latestElements, latestElements,
originalElements, originalElements,
@@ -287,7 +287,7 @@ const handleDimensionChange: DragInputCallbackType<
nextHeight, nextHeight,
initialHeight, initialHeight,
aspectRatio, aspectRatio,
point(x1, y1), pointFrom(x1, y1),
property, property,
latestElements, latestElements,
originalElements, originalElements,
@@ -13,7 +13,7 @@ import { useMemo } from "react";
import { getElementsInAtomicUnit, moveElement } from "./utils"; import { getElementsInAtomicUnit, moveElement } from "./utils";
import type { AtomicUnit } from "./utils"; import type { AtomicUnit } from "./utils";
import type { AppState } from "../../types"; import type { AppState } from "../../types";
import { point, pointRotateRads } from "../../../math"; import { pointFrom, pointRotateRads } from "../../../math";
interface MultiPositionProps { interface MultiPositionProps {
property: "x" | "y"; property: "x" | "y";
@@ -44,8 +44,8 @@ const moveElements = (
origElement.y + origElement.height / 2, origElement.y + origElement.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(origElement.x, origElement.y), pointFrom(origElement.x, origElement.y),
point(cx, cy), pointFrom(cx, cy),
origElement.angle, origElement.angle,
); );
@@ -97,8 +97,8 @@ const moveGroupTo = (
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(latestElement.x, latestElement.y), pointFrom(latestElement.x, latestElement.y),
point(cx, cy), pointFrom(cx, cy),
latestElement.angle, latestElement.angle,
); );
@@ -171,8 +171,8 @@ const handlePositionChange: DragInputCallbackType<
origElement.y + origElement.height / 2, origElement.y + origElement.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(origElement.x, origElement.y), pointFrom(origElement.x, origElement.y),
point(cx, cy), pointFrom(cx, cy),
origElement.angle, origElement.angle,
); );
@@ -241,8 +241,8 @@ const MultiPosition = ({
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2]; const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(el.x, el.y), pointFrom(el.x, el.y),
point(cx, cy), pointFrom(cx, cy),
el.angle, el.angle,
); );
@@ -4,7 +4,7 @@ import type { DragInputCallbackType } from "./DragInput";
import { getStepSizedValue, moveElement } from "./utils"; import { getStepSizedValue, moveElement } from "./utils";
import type Scene from "../../scene/Scene"; import type Scene from "../../scene/Scene";
import type { AppState } from "../../types"; import type { AppState } from "../../types";
import { point, pointRotateRads } from "../../../math"; import { pointFrom, pointRotateRads } from "../../../math";
interface PositionProps { interface PositionProps {
property: "x" | "y"; property: "x" | "y";
@@ -33,8 +33,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
origElement.y + origElement.height / 2, origElement.y + origElement.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(origElement.x, origElement.y), pointFrom(origElement.x, origElement.y),
point(cx, cy), pointFrom(cx, cy),
origElement.angle, origElement.angle,
); );
@@ -93,8 +93,8 @@ const Position = ({
appState, appState,
}: PositionProps) => { }: PositionProps) => {
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(element.x, element.y), pointFrom(element.x, element.y),
point(element.x + element.width / 2, element.y + element.height / 2), pointFrom(element.x + element.width / 2, element.y + element.height / 2),
element.angle, element.angle,
); );
const value = const value =
@@ -25,7 +25,7 @@ import { API } from "../../tests/helpers/api";
import { actionGroup } from "../../actions"; import { actionGroup } from "../../actions";
import { isInGroup } from "../../groups"; import { isInGroup } from "../../groups";
import type { Degrees } from "../../../math"; import type { Degrees } from "../../../math";
import { degreesToRadians, point, pointRotateRads } from "../../../math"; import { degreesToRadians, pointFrom, pointRotateRads } from "../../../math";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@@ -264,8 +264,8 @@ describe("stats for a generic element", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(rectangle.x, rectangle.y), pointFrom(rectangle.x, rectangle.y),
point(cx, cy), pointFrom(cx, cy),
rectangle.angle, rectangle.angle,
); );
@@ -283,8 +283,8 @@ describe("stats for a generic element", () => {
testInputProperty(rectangle, "angle", "A", 0, 45); testInputProperty(rectangle, "angle", "A", 0, 45);
let [newTopLeftX, newTopLeftY] = pointRotateRads( let [newTopLeftX, newTopLeftY] = pointRotateRads(
point(rectangle.x, rectangle.y), pointFrom(rectangle.x, rectangle.y),
point(cx, cy), pointFrom(cx, cy),
rectangle.angle, rectangle.angle,
); );
@@ -294,8 +294,8 @@ describe("stats for a generic element", () => {
testInputProperty(rectangle, "angle", "A", 45, 66); testInputProperty(rectangle, "angle", "A", 45, 66);
[newTopLeftX, newTopLeftY] = pointRotateRads( [newTopLeftX, newTopLeftY] = pointRotateRads(
point(rectangle.x, rectangle.y), pointFrom(rectangle.x, rectangle.y),
point(cx, cy), pointFrom(cx, cy),
rectangle.angle, rectangle.angle,
); );
expect(newTopLeftX.toString()).not.toEqual(xInput.value); expect(newTopLeftX.toString()).not.toEqual(xInput.value);
@@ -311,8 +311,8 @@ describe("stats for a generic element", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(rectangle.x, rectangle.y), pointFrom(rectangle.x, rectangle.y),
point(cx, cy), pointFrom(cx, cy),
rectangle.angle, rectangle.angle,
); );
testInputProperty(rectangle, "width", "W", rectangle.width, 400); testInputProperty(rectangle, "width", "W", rectangle.width, 400);
@@ -321,8 +321,8 @@ describe("stats for a generic element", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
]; ];
let [currentTopLeftX, currentTopLeftY] = pointRotateRads( let [currentTopLeftX, currentTopLeftY] = pointRotateRads(
point(rectangle.x, rectangle.y), pointFrom(rectangle.x, rectangle.y),
point(cx, cy), pointFrom(cx, cy),
rectangle.angle, rectangle.angle,
); );
expect(currentTopLeftX).toBeCloseTo(topLeftX, 4); expect(currentTopLeftX).toBeCloseTo(topLeftX, 4);
@@ -334,8 +334,8 @@ describe("stats for a generic element", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
]; ];
[currentTopLeftX, currentTopLeftY] = pointRotateRads( [currentTopLeftX, currentTopLeftY] = pointRotateRads(
point(rectangle.x, rectangle.y), pointFrom(rectangle.x, rectangle.y),
point(cx, cy), pointFrom(cx, cy),
rectangle.angle, rectangle.angle,
); );
@@ -1,5 +1,5 @@
import type { Radians } from "../../../math"; import type { Radians } from "../../../math";
import { point, pointRotateRads } from "../../../math"; import { pointFrom, pointRotateRads } from "../../../math";
import { import {
bindOrUnbindLinearElements, bindOrUnbindLinearElements,
updateBoundElements, updateBoundElements,
@@ -231,8 +231,8 @@ export const moveElement = (
originalElement.y + originalElement.height / 2, originalElement.y + originalElement.height / 2,
]; ];
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(originalElement.x, originalElement.y), pointFrom(originalElement.x, originalElement.y),
point(cx, cy), pointFrom(cx, cy),
originalElement.angle, originalElement.angle,
); );
@@ -240,8 +240,8 @@ export const moveElement = (
const changeInY = newTopLeftY - topLeftY; const changeInY = newTopLeftY - topLeftY;
const [x, y] = pointRotateRads( const [x, y] = pointRotateRads(
point(newTopLeftX, newTopLeftY), pointFrom(newTopLeftX, newTopLeftY),
point(cx + changeInX, cy + changeInY), pointFrom(cx + changeInX, cy + changeInY),
-originalElement.angle as Radians, -originalElement.angle as Radians,
); );
@@ -203,6 +203,8 @@ const getRelevantAppStateProps = (
snapLines: appState.snapLines, snapLines: appState.snapLines,
zenModeEnabled: appState.zenModeEnabled, zenModeEnabled: appState.zenModeEnabled,
editingTextElement: appState.editingTextElement, editingTextElement: appState.editingTextElement,
isCropping: appState.isCropping,
croppingElementId: appState.croppingElementId,
searchMatches: appState.searchMatches, searchMatches: appState.searchMatches,
}); });
@@ -107,6 +107,7 @@ const getRelevantAppStateProps = (
frameToHighlight: appState.frameToHighlight, frameToHighlight: appState.frameToHighlight,
editingGroupId: appState.editingGroupId, editingGroupId: appState.editingGroupId,
currentHoveredFontFamily: appState.currentHoveredFontFamily, currentHoveredFontFamily: appState.currentHoveredFontFamily,
croppingElementId: appState.croppingElementId,
}); });
const areEqual = ( const areEqual = (
@@ -36,7 +36,7 @@ import { trackEvent } from "../../analytics";
import { useAppProps, useExcalidrawAppState } from "../App"; import { useAppProps, useExcalidrawAppState } from "../App";
import { isEmbeddableElement } from "../../element/typeChecks"; import { isEmbeddableElement } from "../../element/typeChecks";
import { getLinkHandleFromCoords } from "./helpers"; import { getLinkHandleFromCoords } from "./helpers";
import { point, type GlobalPoint } from "../../../math"; import { pointFrom, type GlobalPoint } from "../../../math";
const CONTAINER_WIDTH = 320; const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85; const SPACE_BOTTOM = 85;
@@ -181,7 +181,7 @@ export const Hyperlink = ({
element, element,
elementsMap, elementsMap,
appState, appState,
point(event.clientX, event.clientY), pointFrom(event.clientX, event.clientY),
) as boolean; ) as boolean;
if (shouldHide) { if (shouldHide) {
timeoutId = window.setTimeout(() => { timeoutId = window.setTimeout(() => {
@@ -1,5 +1,5 @@
import type { GlobalPoint, Radians } from "../../../math"; import type { GlobalPoint, Radians } from "../../../math";
import { point, pointRotateRads } from "../../../math"; import { pointFrom, pointRotateRads } from "../../../math";
import { MIME_TYPES } from "../../constants"; import { MIME_TYPES } from "../../constants";
import type { Bounds } from "../../element/bounds"; import type { Bounds } from "../../element/bounds";
import { getElementAbsoluteCoords } from "../../element/bounds"; import { getElementAbsoluteCoords } from "../../element/bounds";
@@ -35,8 +35,8 @@ export const getLinkHandleFromCoords = (
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset; const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
const [rotatedX, rotatedY] = pointRotateRads( const [rotatedX, rotatedY] = pointRotateRads(
point(x + linkWidth / 2, y + linkHeight / 2), pointFrom(x + linkWidth / 2, y + linkHeight / 2),
point(centerX, centerY), pointFrom(centerX, centerY),
angle, angle,
); );
return [ return [
@@ -85,5 +85,10 @@ export const isPointHittingLink = (
) { ) {
return true; return true;
} }
return isPointHittingLinkIcon(element, elementsMap, appState, point(x, y)); return isPointHittingLinkIcon(
element,
elementsMap,
appState,
pointFrom(x, y),
);
}; };
+9
View File
@@ -2147,3 +2147,12 @@ export const upIcon = createIcon(
</g>, </g>,
tablerIconProps, tablerIconProps,
); );
export const cropIcon = createIcon(
<g strokeWidth="1.25">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8 5v10a1 1 0 0 0 1 1h10" />
<path d="M5 8h10a1 1 0 0 1 1 1v10" />
</g>,
tablerIconProps,
);
@@ -6,11 +6,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "#d8f5a2", "backgroundColor": "#d8f5a2",
"boundElements": [ "boundElements": [
{ {
"id": "id45", "id": "id47",
"type": "arrow", "type": "arrow",
}, },
{ {
"id": "id46", "id": "id48",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -47,7 +47,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id46", "id": "id48",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -118,7 +118,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id47", "elementId": "id49",
"fixedPoint": null, "fixedPoint": null,
"focus": -0.08139534883720931, "focus": -0.08139534883720931,
"gap": 1, "gap": 1,
@@ -200,7 +200,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id45", "id": "id47",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -238,7 +238,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id48", "id": "id50",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -284,7 +284,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id48", "id": "id50",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -329,7 +329,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id49", "id": "id51",
"type": "text", "type": "text",
}, },
], ],
@@ -392,7 +392,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"autoResize": true, "autoResize": true,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"containerId": "id48", "containerId": "id50",
"customData": undefined, "customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 5, "fontFamily": 5,
@@ -433,7 +433,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id38", "id": "id40",
"type": "text", "type": "text",
}, },
], ],
@@ -441,7 +441,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"elbowed": false, "elbowed": false,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "id40", "elementId": "id42",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
@@ -472,7 +472,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id39", "elementId": "id41",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
@@ -496,7 +496,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"autoResize": true, "autoResize": true,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"containerId": "id37", "containerId": "id39",
"customData": undefined, "customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 5, "fontFamily": 5,
@@ -537,7 +537,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id37", "id": "id39",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -574,7 +574,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id37", "id": "id39",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -611,7 +611,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id42", "id": "id44",
"type": "text", "type": "text",
}, },
], ],
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"elbowed": false, "elbowed": false,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "id44", "elementId": "id46",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
@@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id43", "elementId": "id45",
"fixedPoint": null, "fixedPoint": null,
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
@@ -674,7 +674,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"autoResize": true, "autoResize": true,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"containerId": "id41", "containerId": "id43",
"customData": undefined, "customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 5, "fontFamily": 5,
@@ -716,7 +716,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id41", "id": "id43",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -762,7 +762,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id41", "id": "id43",
"type": "arrow", "type": "arrow",
}, },
], ],
@@ -1303,7 +1303,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id54", "id": "id56",
"type": "text", "type": "text",
}, },
{ {
@@ -1346,7 +1346,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id55", "id": "id57",
"type": "text", "type": "text",
}, },
], ],
@@ -1385,7 +1385,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id56", "id": "id58",
"type": "text", "type": "text",
}, },
{ {
@@ -1428,7 +1428,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id57", "id": "id59",
"type": "text", "type": "text",
}, },
{ {
@@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id58", "id": "id60",
"type": "text", "type": "text",
}, },
], ],
@@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id59", "id": "id61",
"type": "text", "type": "text",
}, },
], ],
+4 -3
View File
@@ -57,7 +57,7 @@ import {
getNormalizedZoom, getNormalizedZoom,
} from "../scene"; } from "../scene";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { isFiniteNumber, point } from "../../math"; import { isFiniteNumber, pointFrom } from "../../math";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@@ -258,6 +258,7 @@ const restoreElement = (
status: element.status || "pending", status: element.status || "pending",
fileId: element.fileId, fileId: element.fileId,
scale: element.scale || [1, 1], scale: element.scale || [1, 1],
crop: element.crop ?? null,
}); });
case "line": case "line":
// @ts-ignore LEGACY type // @ts-ignore LEGACY type
@@ -268,7 +269,7 @@ const restoreElement = (
let y = element.y; let y = element.y;
let points = // migrate old arrow model to new one let points = // migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2 !Array.isArray(element.points) || element.points.length < 2
? [point(0, 0), point(element.width, element.height)] ? [pointFrom(0, 0), pointFrom(element.width, element.height)]
: element.points; : element.points;
if (points[0][0] !== 0 || points[0][1] !== 0) { if (points[0][0] !== 0 || points[0][1] !== 0) {
@@ -296,7 +297,7 @@ const restoreElement = (
let y: number | undefined = element.y; let y: number | undefined = element.y;
let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one let points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2 !Array.isArray(element.points) || element.points.length < 2
? [point(0, 0), point(element.width, element.height)] ? [pointFrom(0, 0), pointFrom(element.width, element.height)]
: element.points; : element.points;
if (points[0][0] !== 0 || points[0][1] !== 0) { if (points[0][0] !== 0 || points[0][1] !== 0) {
+50 -45
View File
@@ -2,7 +2,7 @@ import { vi } from "vitest";
import type { ExcalidrawElementSkeleton } from "./transform"; import type { ExcalidrawElementSkeleton } from "./transform";
import { convertToExcalidrawElements } from "./transform"; import { convertToExcalidrawElements } from "./transform";
import type { ExcalidrawArrowElement } from "../element/types"; import type { ExcalidrawArrowElement } from "../element/types";
import { point } from "../../math"; import { pointFrom } from "../../math";
const opts = { regenerateIds: false }; const opts = { regenerateIds: false };
@@ -309,28 +309,32 @@ describe("Test Transform", () => {
}); });
describe("Test Frames", () => { describe("Test Frames", () => {
const elements: ExcalidrawElementSkeleton[] = [
{
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
];
it("should transform frames and update frame ids when regenerated", () => { it("should transform frames and update frame ids when regenerated", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [ const elementsSkeleton: ExcalidrawElementSkeleton[] = [
{ ...elements,
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
{ {
type: "frame", type: "frame",
children: ["1", "2"], children: ["1", "2"],
@@ -352,28 +356,9 @@ describe("Test Transform", () => {
}); });
}); });
it("should consider max of calculated and frame dimensions when provided", () => { it("should consider user defined frame dimensions over calculated when provided", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [ const elementsSkeleton: ExcalidrawElementSkeleton[] = [
{ ...elements,
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
{ {
type: "frame", type: "frame",
children: ["1", "2"], children: ["1", "2"],
@@ -388,7 +373,27 @@ describe("Test Transform", () => {
); );
const frame = excalidrawElements.find((ele) => ele.type === "frame")!; const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
expect(frame.width).toBe(800); expect(frame.width).toBe(800);
expect(frame.height).toBe(126); expect(frame.height).toBe(100);
});
it("should consider user defined frame coordinates calculated when provided", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
...elements,
{
type: "frame",
children: ["1", "2"],
name: "My frame",
x: 100,
y: 300,
},
];
const excalidrawElements = convertToExcalidrawElements(
elementsSkeleton,
opts,
);
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
expect(frame.x).toBe(100);
expect(frame.y).toBe(300);
}); });
}); });
@@ -912,7 +917,7 @@ describe("Test Transform", () => {
x: 111.262, x: 111.262,
y: 57, y: 57,
strokeWidth: 2, strokeWidth: 2,
points: [point(0, 0), point(272.985, 0)], points: [pointFrom(0, 0), pointFrom(272.985, 0)],
label: { label: {
text: "How are you?", text: "How are you?",
fontSize: 20, fontSize: 20,
@@ -935,7 +940,7 @@ describe("Test Transform", () => {
x: 77.017, x: 77.017,
y: 79, y: 79,
strokeWidth: 2, strokeWidth: 2,
points: [point(0, 0)], points: [pointFrom(0, 0)],
label: { label: {
text: "Friendship", text: "Friendship",
fontSize: 20, fontSize: 20,
+25 -8
View File
@@ -46,6 +46,7 @@ import {
assertNever, assertNever,
cloneJSON, cloneJSON,
getFontString, getFontString,
isDevEnv,
toBrandedType, toBrandedType,
} from "../utils"; } from "../utils";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
@@ -53,7 +54,7 @@ import { randomId } from "../random";
import { syncInvalidIndices } from "../fractionalIndex"; import { syncInvalidIndices } from "../fractionalIndex";
import { getLineHeight } from "../fonts"; import { getLineHeight } from "../fonts";
import { isArrowElement } from "../element/typeChecks"; import { isArrowElement } from "../element/typeChecks";
import { point, type LocalPoint } from "../../math"; import { pointFrom, type LocalPoint } from "../../math";
export type ValidLinearElement = { export type ValidLinearElement = {
type: "arrow" | "line"; type: "arrow" | "line";
@@ -536,7 +537,7 @@ export const convertToExcalidrawElements = (
excalidrawElement = newLinearElement({ excalidrawElement = newLinearElement({
width, width,
height, height,
points: [point(0, 0), point(width, height)], points: [pointFrom(0, 0), pointFrom(width, height)],
...element, ...element,
}); });
@@ -549,7 +550,7 @@ export const convertToExcalidrawElements = (
width, width,
height, height,
endArrowhead: "arrow", endArrowhead: "arrow",
points: [point(0, 0), point(width, height)], points: [pointFrom(0, 0), pointFrom(width, height)],
...element, ...element,
type: "arrow", type: "arrow",
}); });
@@ -717,7 +718,7 @@ export const convertToExcalidrawElements = (
} }
// Once all the excalidraw elements are created, we can add frames since we // Once all the excalidraw elements are created, we can add frames since we
// need to calculate coordinates and dimensions of frame which is possibe after all // need to calculate coordinates and dimensions of frame which is possible after all
// frame children are processed. // frame children are processed.
for (const [id, element] of elementsWithIds) { for (const [id, element] of elementsWithIds) {
if (element.type !== "frame" && element.type !== "magicframe") { if (element.type !== "frame" && element.type !== "magicframe") {
@@ -764,10 +765,26 @@ export const convertToExcalidrawElements = (
maxX = maxX + PADDING; maxX = maxX + PADDING;
maxY = maxY + PADDING; maxY = maxY + PADDING;
// Take the max of calculated and provided frame dimensions, whichever is higher const frameX = frame?.x || minX;
const width = Math.max(frame?.width, maxX - minX); const frameY = frame?.y || minY;
const height = Math.max(frame?.height, maxY - minY); const frameWidth = frame?.width || maxX - minX;
Object.assign(frame, { x: minX, y: minY, width, height }); const frameHeight = frame?.height || maxY - minY;
Object.assign(frame, {
x: frameX,
y: frameY,
width: frameWidth,
height: frameHeight,
});
if (
isDevEnv() &&
element.children.length &&
(frame?.x || frame?.y || frame?.width || frame?.height)
) {
console.info(
"User provided frame attributes are being considered, if you find this inaccurate, please remove any of the attributes - x, y, width and height so frame coordinates and dimensions are calculated automatically",
);
}
} }
return elementStore.getElements(); return elementStore.getElements();
+42 -33
View File
@@ -66,7 +66,7 @@ import {
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { import {
lineSegment, lineSegment,
point, pointFrom,
pointRotateRads, pointRotateRads,
type GlobalPoint, type GlobalPoint,
vectorFromPoint, vectorFromPoint,
@@ -720,7 +720,7 @@ export const getHeadingForElbowArrowSnap = (
return vectorToHeading( return vectorToHeading(
vectorFromPoint( vectorFromPoint(
p, p,
point<GlobalPoint>( pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2, bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2, bindableElement.y + bindableElement.height / 2,
), ),
@@ -766,15 +766,15 @@ export const bindPointToSnapToElementOutline = (
const intersections = [ const intersections = [
...(intersectElementWithLine( ...(intersectElementWithLine(
bindableElement, bindableElement,
point(p[0], p[1] - 2 * bindableElement.height), pointFrom(p[0], p[1] - 2 * bindableElement.height),
point(p[0], p[1] + 2 * bindableElement.height), pointFrom(p[0], p[1] + 2 * bindableElement.height),
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
elementsMap, elementsMap,
) ?? []), ) ?? []),
...(intersectElementWithLine( ...(intersectElementWithLine(
bindableElement, bindableElement,
point(p[0] - 2 * bindableElement.width, p[1]), pointFrom(p[0] - 2 * bindableElement.width, p[1]),
point(p[0] + 2 * bindableElement.width, p[1]), pointFrom(p[0] + 2 * bindableElement.width, p[1]),
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
elementsMap, elementsMap,
) ?? []), ) ?? []),
@@ -815,25 +815,25 @@ const headingToMidBindPoint = (
switch (true) { switch (true) {
case compareHeading(heading, HEADING_UP): case compareHeading(heading, HEADING_UP):
return pointRotateRads( return pointRotateRads(
point((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]), pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
center, center,
bindableElement.angle, bindableElement.angle,
); );
case compareHeading(heading, HEADING_RIGHT): case compareHeading(heading, HEADING_RIGHT):
return pointRotateRads( return pointRotateRads(
point(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1), pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
center, center,
bindableElement.angle, bindableElement.angle,
); );
case compareHeading(heading, HEADING_DOWN): case compareHeading(heading, HEADING_DOWN):
return pointRotateRads( return pointRotateRads(
point((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]), pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
center, center,
bindableElement.angle, bindableElement.angle,
); );
default: default:
return pointRotateRads( return pointRotateRads(
point(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1), pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
center, center,
bindableElement.angle, bindableElement.angle,
); );
@@ -844,7 +844,7 @@ export const avoidRectangularCorner = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
p: GlobalPoint, p: GlobalPoint,
): GlobalPoint => { ): GlobalPoint => {
const center = point<GlobalPoint>( const center = pointFrom<GlobalPoint>(
element.x + element.width / 2, element.x + element.width / 2,
element.y + element.height / 2, element.y + element.height / 2,
); );
@@ -854,13 +854,13 @@ export const avoidRectangularCorner = (
// Top left // Top left
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) { if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
return pointRotateRads<GlobalPoint>( return pointRotateRads<GlobalPoint>(
point(element.x - FIXED_BINDING_DISTANCE, element.y), pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
center, center,
element.angle, element.angle,
); );
} }
return pointRotateRads( return pointRotateRads(
point(element.x, element.y - FIXED_BINDING_DISTANCE), pointFrom(element.x, element.y - FIXED_BINDING_DISTANCE),
center, center,
element.angle, element.angle,
); );
@@ -871,13 +871,16 @@ export const avoidRectangularCorner = (
// Bottom left // Bottom left
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) { if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
return pointRotateRads( return pointRotateRads(
point(element.x, element.y + element.height + FIXED_BINDING_DISTANCE), pointFrom(
element.x,
element.y + element.height + FIXED_BINDING_DISTANCE,
),
center, center,
element.angle, element.angle,
); );
} }
return pointRotateRads( return pointRotateRads(
point(element.x - FIXED_BINDING_DISTANCE, element.y + element.height), pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y + element.height),
center, center,
element.angle, element.angle,
); );
@@ -891,7 +894,7 @@ export const avoidRectangularCorner = (
element.width + FIXED_BINDING_DISTANCE element.width + FIXED_BINDING_DISTANCE
) { ) {
return pointRotateRads( return pointRotateRads(
point( pointFrom(
element.x + element.width, element.x + element.width,
element.y + element.height + FIXED_BINDING_DISTANCE, element.y + element.height + FIXED_BINDING_DISTANCE,
), ),
@@ -900,7 +903,7 @@ export const avoidRectangularCorner = (
); );
} }
return pointRotateRads( return pointRotateRads(
point( pointFrom(
element.x + element.width + FIXED_BINDING_DISTANCE, element.x + element.width + FIXED_BINDING_DISTANCE,
element.y + element.height, element.y + element.height,
), ),
@@ -917,13 +920,16 @@ export const avoidRectangularCorner = (
element.width + FIXED_BINDING_DISTANCE element.width + FIXED_BINDING_DISTANCE
) { ) {
return pointRotateRads( return pointRotateRads(
point(element.x + element.width, element.y - FIXED_BINDING_DISTANCE), pointFrom(
element.x + element.width,
element.y - FIXED_BINDING_DISTANCE,
),
center, center,
element.angle, element.angle,
); );
} }
return pointRotateRads( return pointRotateRads(
point(element.x + element.width + FIXED_BINDING_DISTANCE, element.y), pointFrom(element.x + element.width + FIXED_BINDING_DISTANCE, element.y),
center, center,
element.angle, element.angle,
); );
@@ -938,7 +944,10 @@ export const snapToMid = (
tolerance: number = 0.05, tolerance: number = 0.05,
): GlobalPoint => { ): GlobalPoint => {
const { x, y, width, height, angle } = element; const { x, y, width, height, angle } = element;
const center = point<GlobalPoint>(x + width / 2 - 0.1, y + height / 2 - 0.1); const center = pointFrom<GlobalPoint>(
x + width / 2 - 0.1,
y + height / 2 - 0.1,
);
const nonRotated = pointRotateRads(p, center, -angle as Radians); const nonRotated = pointRotateRads(p, center, -angle as Radians);
// snap-to-center point is adaptive to element size, but we don't want to go // snap-to-center point is adaptive to element size, but we don't want to go
@@ -953,7 +962,7 @@ export const snapToMid = (
) { ) {
// LEFT // LEFT
return pointRotateRads( return pointRotateRads(
point(x - FIXED_BINDING_DISTANCE, center[1]), pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
center, center,
angle, angle,
); );
@@ -964,7 +973,7 @@ export const snapToMid = (
) { ) {
// TOP // TOP
return pointRotateRads( return pointRotateRads(
point(center[0], y - FIXED_BINDING_DISTANCE), pointFrom(center[0], y - FIXED_BINDING_DISTANCE),
center, center,
angle, angle,
); );
@@ -975,7 +984,7 @@ export const snapToMid = (
) { ) {
// RIGHT // RIGHT
return pointRotateRads( return pointRotateRads(
point(x + width + FIXED_BINDING_DISTANCE, center[1]), pointFrom(x + width + FIXED_BINDING_DISTANCE, center[1]),
center, center,
angle, angle,
); );
@@ -986,7 +995,7 @@ export const snapToMid = (
) { ) {
// DOWN // DOWN
return pointRotateRads( return pointRotateRads(
point(center[0], y + height + FIXED_BINDING_DISTANCE), pointFrom(center[0], y + height + FIXED_BINDING_DISTANCE),
center, center,
angle, angle,
); );
@@ -1023,11 +1032,11 @@ const updateBoundPoint = (
startOrEnd === "startBinding" ? "start" : "end", startOrEnd === "startBinding" ? "start" : "end",
elementsMap, elementsMap,
).fixedPoint; ).fixedPoint;
const globalMidPoint = point<GlobalPoint>( const globalMidPoint = pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2, bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2, bindableElement.y + bindableElement.height / 2,
); );
const global = point<GlobalPoint>( const global = pointFrom<GlobalPoint>(
bindableElement.x + fixedPoint[0] * bindableElement.width, bindableElement.x + fixedPoint[0] * bindableElement.width,
bindableElement.y + fixedPoint[1] * bindableElement.height, bindableElement.y + fixedPoint[1] * bindableElement.height,
); );
@@ -1118,7 +1127,7 @@ export const calculateFixedPointForElbowArrowBinding = (
hoveredElement, hoveredElement,
elementsMap, elementsMap,
); );
const globalMidPoint = point( const globalMidPoint = pointFrom(
bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[0] + (bounds[2] - bounds[0]) / 2,
bounds[1] + (bounds[3] - bounds[1]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2,
); );
@@ -1337,9 +1346,9 @@ export const bindingBorderTest = (
const threshold = maxBindingGap(element, element.width, element.height); const threshold = maxBindingGap(element, element.width, element.height);
const shape = getElementShape(element, elementsMap); const shape = getElementShape(element, elementsMap);
return ( return (
isPointOnShape(point(x, y), shape, threshold) || isPointOnShape(pointFrom(x, y), shape, threshold) ||
(fullShape === true && (fullShape === true &&
pointInsideBounds(point(x, y), aabbForElement(element))) pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
); );
}; };
@@ -2197,11 +2206,11 @@ export const getGlobalFixedPointForBindableElement = (
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio); const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
return pointRotateRads( return pointRotateRads(
point( pointFrom(
element.x + element.width * fixedX, element.x + element.width * fixedX,
element.y + element.height * fixedY, element.y + element.height * fixedY,
), ),
point<GlobalPoint>( pointFrom<GlobalPoint>(
element.x + element.width / 2, element.x + element.width / 2,
element.y + element.height / 2, element.y + element.height / 2,
), ),
@@ -2229,7 +2238,7 @@ const getGlobalFixedPoints = (
arrow.startBinding.fixedPoint, arrow.startBinding.fixedPoint,
startElement as ExcalidrawBindableElement, startElement as ExcalidrawBindableElement,
) )
: point<GlobalPoint>( : pointFrom<GlobalPoint>(
arrow.x + arrow.points[0][0], arrow.x + arrow.points[0][0],
arrow.y + arrow.points[0][1], arrow.y + arrow.points[0][1],
); );
@@ -2239,7 +2248,7 @@ const getGlobalFixedPoints = (
arrow.endBinding.fixedPoint, arrow.endBinding.fixedPoint,
endElement as ExcalidrawBindableElement, endElement as ExcalidrawBindableElement,
) )
: point<GlobalPoint>( : pointFrom<GlobalPoint>(
arrow.x + arrow.points[arrow.points.length - 1][0], arrow.x + arrow.points[arrow.points.length - 1][0],
arrow.y + arrow.points[arrow.points.length - 1][1], arrow.y + arrow.points[arrow.points.length - 1][1],
); );
+4 -4
View File
@@ -1,5 +1,5 @@
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { point } from "../../math"; import { pointFrom } from "../../math";
import { ROUNDNESS } from "../constants"; import { ROUNDNESS } from "../constants";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { getElementAbsoluteCoords, getElementBounds } from "./bounds"; import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
@@ -125,9 +125,9 @@ describe("getElementBounds", () => {
a: 0.6447741904932416, a: 0.6447741904932416,
}), }),
points: [ points: [
point<LocalPoint>(0, 0), pointFrom<LocalPoint>(0, 0),
point<LocalPoint>(67.33984375, 92.48828125), pointFrom<LocalPoint>(67.33984375, 92.48828125),
point<LocalPoint>(-102.7890625, 52.15625), pointFrom<LocalPoint>(-102.7890625, 52.15625),
], ],
} as ExcalidrawLinearElement; } as ExcalidrawLinearElement;
+46 -46
View File
@@ -34,7 +34,7 @@ import type {
import { import {
degreesToRadians, degreesToRadians,
lineSegment, lineSegment,
point, pointFrom,
pointDistance, pointDistance,
pointFromArray, pointFromArray,
pointRotateRads, pointRotateRads,
@@ -113,8 +113,8 @@ export class ElementBounds {
const [minX, minY, maxX, maxY] = getBoundsFromPoints( const [minX, minY, maxX, maxY] = getBoundsFromPoints(
element.points.map(([x, y]) => element.points.map(([x, y]) =>
pointRotateRads( pointRotateRads(
point(x, y), pointFrom(x, y),
point(cx - element.x, cy - element.y), pointFrom(cx - element.x, cy - element.y),
element.angle, element.angle,
), ),
), ),
@@ -130,23 +130,23 @@ export class ElementBounds {
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap); bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
} else if (element.type === "diamond") { } else if (element.type === "diamond") {
const [x11, y11] = pointRotateRads( const [x11, y11] = pointRotateRads(
point(cx, y1), pointFrom(cx, y1),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const [x12, y12] = pointRotateRads( const [x12, y12] = pointRotateRads(
point(cx, y2), pointFrom(cx, y2),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const [x22, y22] = pointRotateRads( const [x22, y22] = pointRotateRads(
point(x1, cy), pointFrom(x1, cy),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const [x21, y21] = pointRotateRads( const [x21, y21] = pointRotateRads(
point(x2, cy), pointFrom(x2, cy),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const minX = Math.min(x11, x12, x22, x21); const minX = Math.min(x11, x12, x22, x21);
@@ -164,23 +164,23 @@ export class ElementBounds {
bounds = [cx - ww, cy - hh, cx + ww, cy + hh]; bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
} else { } else {
const [x11, y11] = pointRotateRads( const [x11, y11] = pointRotateRads(
point(x1, y1), pointFrom(x1, y1),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const [x12, y12] = pointRotateRads( const [x12, y12] = pointRotateRads(
point(x1, y2), pointFrom(x1, y2),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const [x22, y22] = pointRotateRads( const [x22, y22] = pointRotateRads(
point(x2, y2), pointFrom(x2, y2),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const [x21, y21] = pointRotateRads( const [x21, y21] = pointRotateRads(
point(x2, y1), pointFrom(x2, y1),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const minX = Math.min(x11, x12, x22, x21); const minX = Math.min(x11, x12, x22, x21);
@@ -255,7 +255,7 @@ export const getElementLineSegments = (
elementsMap, elementsMap,
); );
const center: GlobalPoint = point(cx, cy); const center: GlobalPoint = pointFrom(cx, cy);
if (isLinearElement(element) || isFreeDrawElement(element)) { if (isLinearElement(element) || isFreeDrawElement(element)) {
const segments: LineSegment<GlobalPoint>[] = []; const segments: LineSegment<GlobalPoint>[] = [];
@@ -266,7 +266,7 @@ export const getElementLineSegments = (
segments.push( segments.push(
lineSegment( lineSegment(
pointRotateRads( pointRotateRads(
point( pointFrom(
element.points[i][0] + element.x, element.points[i][0] + element.x,
element.points[i][1] + element.y, element.points[i][1] + element.y,
), ),
@@ -274,7 +274,7 @@ export const getElementLineSegments = (
element.angle, element.angle,
), ),
pointRotateRads( pointRotateRads(
point( pointFrom(
element.points[i + 1][0] + element.x, element.points[i + 1][0] + element.x,
element.points[i + 1][1] + element.y, element.points[i + 1][1] + element.y,
), ),
@@ -470,7 +470,7 @@ export const getMinMaxXYFromCurvePathOps = (
ops: Op[], ops: Op[],
transformXY?: (p: GlobalPoint) => GlobalPoint, transformXY?: (p: GlobalPoint) => GlobalPoint,
): Bounds => { ): Bounds => {
let currentP: GlobalPoint = point(0, 0); let currentP: GlobalPoint = pointFrom(0, 0);
const { minX, minY, maxX, maxY } = ops.reduce( const { minX, minY, maxX, maxY } = ops.reduce(
(limits, { op, data }) => { (limits, { op, data }) => {
@@ -484,9 +484,9 @@ export const getMinMaxXYFromCurvePathOps = (
// move operation does not draw anything; so, it always // move operation does not draw anything; so, it always
// returns false // returns false
} else if (op === "bcurveTo") { } else if (op === "bcurveTo") {
const _p1 = point<GlobalPoint>(data[0], data[1]); const _p1 = pointFrom<GlobalPoint>(data[0], data[1]);
const _p2 = point<GlobalPoint>(data[2], data[3]); const _p2 = pointFrom<GlobalPoint>(data[2], data[3]);
const _p3 = point<GlobalPoint>(data[4], data[5]); const _p3 = pointFrom<GlobalPoint>(data[4], data[5]);
const p1 = transformXY ? transformXY(_p1) : _p1; const p1 = transformXY ? transformXY(_p1) : _p1;
const p2 = transformXY ? transformXY(_p2) : _p2; const p2 = transformXY ? transformXY(_p2) : _p2;
@@ -591,21 +591,21 @@ export const getArrowheadPoints = (
invariant(data.length === 6, "Op data length is not 6"); invariant(data.length === 6, "Op data length is not 6");
const p3 = point(data[4], data[5]); const p3 = pointFrom(data[4], data[5]);
const p2 = point(data[2], data[3]); const p2 = pointFrom(data[2], data[3]);
const p1 = point(data[0], data[1]); const p1 = pointFrom(data[0], data[1]);
// We need to find p0 of the bezier curve. // We need to find p0 of the bezier curve.
// It is typically the last point of the previous // It is typically the last point of the previous
// curve; it can also be the position of moveTo operation. // curve; it can also be the position of moveTo operation.
const prevOp = ops[index - 1]; const prevOp = ops[index - 1];
let p0 = point(0, 0); let p0 = pointFrom(0, 0);
if (prevOp.op === "move") { if (prevOp.op === "move") {
const p = pointFromArray(prevOp.data); const p = pointFromArray(prevOp.data);
invariant(p != null, "Op data is not a point"); invariant(p != null, "Op data is not a point");
p0 = p; p0 = p;
} else if (prevOp.op === "bcurveTo") { } else if (prevOp.op === "bcurveTo") {
p0 = point(prevOp.data[4], prevOp.data[5]); p0 = pointFrom(prevOp.data[4], prevOp.data[5]);
} }
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3 // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
@@ -671,13 +671,13 @@ export const getArrowheadPoints = (
// Return points // Return points
const [x3, y3] = pointRotateRads( const [x3, y3] = pointRotateRads(
point(xs, ys), pointFrom(xs, ys),
point(x2, y2), pointFrom(x2, y2),
((-angle * Math.PI) / 180) as Radians, ((-angle * Math.PI) / 180) as Radians,
); );
const [x4, y4] = pointRotateRads( const [x4, y4] = pointRotateRads(
point(xs, ys), pointFrom(xs, ys),
point(x2, y2), pointFrom(x2, y2),
degreesToRadians(angle), degreesToRadians(angle),
); );
@@ -690,8 +690,8 @@ export const getArrowheadPoints = (
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0]; const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
[ox, oy] = pointRotateRads( [ox, oy] = pointRotateRads(
point(x2 + minSize * 2, y2), pointFrom(x2 + minSize * 2, y2),
point(x2, y2), pointFrom(x2, y2),
Math.atan2(py - y2, px - x2) as Radians, Math.atan2(py - y2, px - x2) as Radians,
); );
} else { } else {
@@ -701,8 +701,8 @@ export const getArrowheadPoints = (
: [0, 0]; : [0, 0];
[ox, oy] = pointRotateRads( [ox, oy] = pointRotateRads(
point(x2 - minSize * 2, y2), pointFrom(x2 - minSize * 2, y2),
point(x2, y2), pointFrom(x2, y2),
Math.atan2(y2 - py, x2 - px) as Radians, Math.atan2(y2 - py, x2 - px) as Radians,
); );
} }
@@ -746,8 +746,8 @@ const getLinearElementRotatedBounds = (
if (element.points.length < 2) { if (element.points.length < 2) {
const [pointX, pointY] = element.points[0]; const [pointX, pointY] = element.points[0];
const [x, y] = pointRotateRads( const [x, y] = pointRotateRads(
point(element.x + pointX, element.y + pointY), pointFrom(element.x + pointX, element.y + pointY),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
@@ -775,8 +775,8 @@ const getLinearElementRotatedBounds = (
const ops = getCurvePathOps(shape); const ops = getCurvePathOps(shape);
const transformXY = ([x, y]: GlobalPoint) => const transformXY = ([x, y]: GlobalPoint) =>
pointRotateRads<GlobalPoint>( pointRotateRads<GlobalPoint>(
point(element.x + x, element.y + y), pointFrom(element.x + x, element.y + y),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const res = getMinMaxXYFromCurvePathOps(ops, transformXY); const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
@@ -931,8 +931,8 @@ export const getClosestElementBounds = (
elements.forEach((element) => { elements.forEach((element) => {
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
const distance = pointDistance( const distance = pointDistance(
point((x1 + x2) / 2, (y1 + y2) / 2), pointFrom((x1 + x2) / 2, (y1 + y2) / 2),
point(from.x, from.y), pointFrom(from.x, from.y),
); );
if (distance < minDistance) { if (distance < minDistance) {
@@ -990,7 +990,7 @@ export const getVisibleSceneBounds = ({
}; };
export const getCenterForBounds = (bounds: Bounds): GlobalPoint => export const getCenterForBounds = (bounds: Bounds): GlobalPoint =>
point( pointFrom(
bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[0] + (bounds[2] - bounds[0]) / 2,
bounds[1] + (bounds[3] - bounds[1]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2,
); );
+11 -7
View File
@@ -17,7 +17,7 @@ import {
} from "./typeChecks"; } from "./typeChecks";
import { getBoundTextShape, isPathALoop } from "../shapes"; import { getBoundTextShape, isPathALoop } from "../shapes";
import type { GlobalPoint, LocalPoint, Polygon } from "../../math"; import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
import { isPointWithinBounds, point } from "../../math"; import { isPointWithinBounds, pointFrom } from "../../math";
export const shouldTestInside = (element: ExcalidrawElement) => { export const shouldTestInside = (element: ExcalidrawElement) => {
if (element.type === "arrow") { if (element.type === "arrow") {
@@ -61,13 +61,13 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
let hit = shouldTestInside(element) let hit = shouldTestInside(element)
? // Since `inShape` tests STRICTLY againt the insides of a shape ? // Since `inShape` tests STRICTLY againt the insides of a shape
// we would need `onShape` as well to include the "borders" // we would need `onShape` as well to include the "borders"
isPointInShape(point(x, y), shape) || isPointInShape(pointFrom(x, y), shape) ||
isPointOnShape(point(x, y), shape, threshold) isPointOnShape(pointFrom(x, y), shape, threshold)
: isPointOnShape(point(x, y), shape, threshold); : isPointOnShape(pointFrom(x, y), shape, threshold);
// hit test against a frame's name // hit test against a frame's name
if (!hit && frameNameBound) { if (!hit && frameNameBound) {
hit = isPointInShape(point(x, y), { hit = isPointInShape(pointFrom(x, y), {
type: "polygon", type: "polygon",
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement) data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
.data as Polygon<Point>, .data as Polygon<Point>,
@@ -89,7 +89,11 @@ export const hitElementBoundingBox = (
y1 -= tolerance; y1 -= tolerance;
x2 += tolerance; x2 += tolerance;
y2 += tolerance; y2 += tolerance;
return isPointWithinBounds(point(x1, y1), point(x, y), point(x2, y2)); return isPointWithinBounds(
pointFrom(x1, y1),
pointFrom(x, y),
pointFrom(x2, y2),
);
}; };
export const hitElementBoundingBoxOnly = < export const hitElementBoundingBoxOnly = <
@@ -115,5 +119,5 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
y: number, y: number,
textShape: GeometricShape<Point> | null, textShape: GeometricShape<Point> | null,
): boolean => { ): boolean => {
return !!textShape && isPointInShape(point(x, y), textShape); return !!textShape && isPointInShape(pointFrom(x, y), textShape);
}; };
+587
View File
@@ -0,0 +1,587 @@
import { type Point } from "points-on-curve";
import {
type Radians,
pointFrom,
pointCenter,
pointRotateRads,
vectorFromPoint,
vectorNormalize,
vectorSubtract,
vectorAdd,
vectorScale,
pointFromVector,
clamp,
isCloseTo,
} from "../../math";
import type { TransformHandleType } from "./transformHandles";
import type {
ElementsMap,
ExcalidrawElement,
ExcalidrawImageElement,
ImageCrop,
NonDeleted,
} from "./types";
import {
getElementAbsoluteCoords,
getResizedElementAbsoluteCoords,
} from "./bounds";
const MINIMAL_CROP_SIZE = 10;
export const cropElement = (
element: ExcalidrawImageElement,
transformHandle: TransformHandleType,
naturalWidth: number,
naturalHeight: number,
pointerX: number,
pointerY: number,
widthAspectRatio?: number,
) => {
const { width: uncroppedWidth, height: uncroppedHeight } =
getUncroppedWidthAndHeight(element);
const naturalWidthToUncropped = naturalWidth / uncroppedWidth;
const naturalHeightToUncropped = naturalHeight / uncroppedHeight;
const croppedLeft = (element.crop?.x ?? 0) / naturalWidthToUncropped;
const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped;
/**
* uncropped width
* *––––––––––––––––––––––––*
* | (x,y) (natural) |
* | *–––––––* |
* | |///////| height | uncropped height
* | *–––––––* |
* | width (natural) |
* *––––––––––––––––––––––––*
*/
const rotatedPointer = pointRotateRads(
pointFrom(pointerX, pointerY),
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
-element.angle as Radians,
);
pointerX = rotatedPointer[0];
pointerY = rotatedPointer[1];
let nextWidth = element.width;
let nextHeight = element.height;
let crop: ImageCrop | null = element.crop ?? {
x: 0,
y: 0,
width: naturalWidth,
height: naturalHeight,
naturalWidth,
naturalHeight,
};
const previousCropHeight = crop.height;
const previousCropWidth = crop.width;
const isFlippedByX = element.scale[0] === -1;
const isFlippedByY = element.scale[1] === -1;
let changeInHeight = pointerY - element.y;
let changeInWidth = pointerX - element.x;
if (transformHandle.includes("n")) {
nextHeight = clamp(
element.height - changeInHeight,
MINIMAL_CROP_SIZE,
isFlippedByY ? uncroppedHeight - croppedTop : element.height + croppedTop,
);
}
if (transformHandle.includes("s")) {
changeInHeight = pointerY - element.y - element.height;
nextHeight = clamp(
element.height + changeInHeight,
MINIMAL_CROP_SIZE,
isFlippedByY ? element.height + croppedTop : uncroppedHeight - croppedTop,
);
}
if (transformHandle.includes("e")) {
changeInWidth = pointerX - element.x - element.width;
nextWidth = clamp(
element.width + changeInWidth,
MINIMAL_CROP_SIZE,
isFlippedByX ? element.width + croppedLeft : uncroppedWidth - croppedLeft,
);
}
if (transformHandle.includes("w")) {
nextWidth = clamp(
element.width - changeInWidth,
MINIMAL_CROP_SIZE,
isFlippedByX ? uncroppedWidth - croppedLeft : element.width + croppedLeft,
);
}
const updateCropWidthAndHeight = (crop: ImageCrop) => {
crop.height = nextHeight * naturalHeightToUncropped;
crop.width = nextWidth * naturalWidthToUncropped;
};
updateCropWidthAndHeight(crop);
const adjustFlipForHandle = (
handle: TransformHandleType,
crop: ImageCrop,
) => {
updateCropWidthAndHeight(crop);
if (handle.includes("n")) {
if (!isFlippedByY) {
crop.y += previousCropHeight - crop.height;
}
}
if (handle.includes("s")) {
if (isFlippedByY) {
crop.y += previousCropHeight - crop.height;
}
}
if (handle.includes("e")) {
if (isFlippedByX) {
crop.x += previousCropWidth - crop.width;
}
}
if (handle.includes("w")) {
if (!isFlippedByX) {
crop.x += previousCropWidth - crop.width;
}
}
};
switch (transformHandle) {
case "n": {
if (widthAspectRatio) {
const distanceToLeft = croppedLeft + element.width / 2;
const distanceToRight =
uncroppedWidth - croppedLeft - element.width / 2;
const MAX_WIDTH = Math.min(distanceToLeft, distanceToRight) * 2;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
adjustFlipForHandle(transformHandle, crop);
if (widthAspectRatio) {
crop.x += (previousCropWidth - crop.width) / 2;
}
break;
}
case "s": {
if (widthAspectRatio) {
const distanceToLeft = croppedLeft + element.width / 2;
const distanceToRight =
uncroppedWidth - croppedLeft - element.width / 2;
const MAX_WIDTH = Math.min(distanceToLeft, distanceToRight) * 2;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
adjustFlipForHandle(transformHandle, crop);
if (widthAspectRatio) {
crop.x += (previousCropWidth - crop.width) / 2;
}
break;
}
case "w": {
if (widthAspectRatio) {
const distanceToTop = croppedTop + element.height / 2;
const distanceToBottom =
uncroppedHeight - croppedTop - element.height / 2;
const MAX_HEIGHT = Math.min(distanceToTop, distanceToBottom) * 2;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
}
adjustFlipForHandle(transformHandle, crop);
if (widthAspectRatio) {
crop.y += (previousCropHeight - crop.height) / 2;
}
break;
}
case "e": {
if (widthAspectRatio) {
const distanceToTop = croppedTop + element.height / 2;
const distanceToBottom =
uncroppedHeight - croppedTop - element.height / 2;
const MAX_HEIGHT = Math.min(distanceToTop, distanceToBottom) * 2;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
}
adjustFlipForHandle(transformHandle, crop);
if (widthAspectRatio) {
crop.y += (previousCropHeight - crop.height) / 2;
}
break;
}
case "ne": {
if (widthAspectRatio) {
if (changeInWidth > -changeInHeight) {
const MAX_HEIGHT = isFlippedByY
? uncroppedHeight - croppedTop
: croppedTop + element.height;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
} else {
const MAX_WIDTH = isFlippedByX
? croppedLeft + element.width
: uncroppedWidth - croppedLeft;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
}
adjustFlipForHandle(transformHandle, crop);
break;
}
case "nw": {
if (widthAspectRatio) {
if (changeInWidth < changeInHeight) {
const MAX_HEIGHT = isFlippedByY
? uncroppedHeight - croppedTop
: croppedTop + element.height;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
} else {
const MAX_WIDTH = isFlippedByX
? uncroppedWidth - croppedLeft
: croppedLeft + element.width;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
}
adjustFlipForHandle(transformHandle, crop);
break;
}
case "se": {
if (widthAspectRatio) {
if (changeInWidth > changeInHeight) {
const MAX_HEIGHT = isFlippedByY
? croppedTop + element.height
: uncroppedHeight - croppedTop;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
} else {
const MAX_WIDTH = isFlippedByX
? croppedLeft + element.width
: uncroppedWidth - croppedLeft;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
}
adjustFlipForHandle(transformHandle, crop);
break;
}
case "sw": {
if (widthAspectRatio) {
if (-changeInWidth > changeInHeight) {
const MAX_HEIGHT = isFlippedByY
? croppedTop + element.height
: uncroppedHeight - croppedTop;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
} else {
const MAX_WIDTH = isFlippedByX
? uncroppedWidth - croppedLeft
: croppedLeft + element.width;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
}
adjustFlipForHandle(transformHandle, crop);
break;
}
default:
break;
}
const newOrigin = recomputeOrigin(
element,
transformHandle,
nextWidth,
nextHeight,
!!widthAspectRatio,
);
// reset crop to null if we're back to orig size
if (
isCloseTo(crop.width, crop.naturalWidth) &&
isCloseTo(crop.height, crop.naturalHeight)
) {
crop = null;
}
return {
x: newOrigin[0],
y: newOrigin[1],
width: nextWidth,
height: nextHeight,
crop,
};
};
const recomputeOrigin = (
stateAtCropStart: NonDeleted<ExcalidrawElement>,
transformHandle: TransformHandleType,
width: number,
height: number,
shouldMaintainAspectRatio?: boolean,
) => {
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
stateAtCropStart,
stateAtCropStart.width,
stateAtCropStart.height,
true,
);
const startTopLeft = pointFrom(x1, y1);
const startBottomRight = pointFrom(x2, y2);
const startCenter: any = pointCenter(startTopLeft, startBottomRight);
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
getResizedElementAbsoluteCoords(stateAtCropStart, width, height, true);
const newBoundsWidth = newBoundsX2 - newBoundsX1;
const newBoundsHeight = newBoundsY2 - newBoundsY1;
// Calculate new topLeft based on fixed corner during resize
let newTopLeft = [...startTopLeft] as [number, number];
if (["n", "w", "nw"].includes(transformHandle)) {
newTopLeft = [
startBottomRight[0] - Math.abs(newBoundsWidth),
startBottomRight[1] - Math.abs(newBoundsHeight),
];
}
if (transformHandle === "ne") {
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
}
if (transformHandle === "sw") {
const topRight = [startBottomRight[0], startTopLeft[1]];
newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
}
if (shouldMaintainAspectRatio) {
if (["s", "n"].includes(transformHandle)) {
newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
}
if (["e", "w"].includes(transformHandle)) {
newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
}
}
// adjust topLeft to new rotation point
const angle = stateAtCropStart.angle;
const rotatedTopLeft = pointRotateRads(newTopLeft, startCenter, angle);
const newCenter: Point = [
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
];
const rotatedNewCenter = pointRotateRads(newCenter, startCenter, angle);
newTopLeft = pointRotateRads(
rotatedTopLeft,
rotatedNewCenter,
-angle as Radians,
);
const newOrigin = [...newTopLeft];
newOrigin[0] += stateAtCropStart.x - newBoundsX1;
newOrigin[1] += stateAtCropStart.y - newBoundsY1;
return newOrigin;
};
// refer to https://link.excalidraw.com/l/6rfy1007QOo/6stx5PmRn0k
export const getUncroppedImageElement = (
element: ExcalidrawImageElement,
elementsMap: ElementsMap,
) => {
if (element.crop) {
const { width, height } = getUncroppedWidthAndHeight(element);
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
);
const topLeftVector = vectorFromPoint(
pointRotateRads(pointFrom(x1, y1), pointFrom(cx, cy), element.angle),
);
const topRightVector = vectorFromPoint(
pointRotateRads(pointFrom(x2, y1), pointFrom(cx, cy), element.angle),
);
const topEdgeNormalized = vectorNormalize(
vectorSubtract(topRightVector, topLeftVector),
);
const bottomLeftVector = vectorFromPoint(
pointRotateRads(pointFrom(x1, y2), pointFrom(cx, cy), element.angle),
);
const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector);
const leftEdgeNormalized = vectorNormalize(leftEdgeVector);
const { cropX, cropY } = adjustCropPosition(element.crop, element.scale);
const rotatedTopLeft = vectorAdd(
vectorAdd(
topLeftVector,
vectorScale(
topEdgeNormalized,
(-cropX * width) / element.crop.naturalWidth,
),
),
vectorScale(
leftEdgeNormalized,
(-cropY * height) / element.crop.naturalHeight,
),
);
const center = pointFromVector(
vectorAdd(
vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)),
vectorScale(leftEdgeNormalized, height / 2),
),
);
const unrotatedTopLeft = pointRotateRads(
pointFromVector(rotatedTopLeft),
center,
-element.angle as Radians,
);
const uncroppedElement: ExcalidrawImageElement = {
...element,
x: unrotatedTopLeft[0],
y: unrotatedTopLeft[1],
width,
height,
crop: null,
};
return uncroppedElement;
}
return element;
};
export const getUncroppedWidthAndHeight = (element: ExcalidrawImageElement) => {
if (element.crop) {
const width =
element.width / (element.crop.width / element.crop.naturalWidth);
const height =
element.height / (element.crop.height / element.crop.naturalHeight);
return {
width,
height,
};
}
return {
width: element.width,
height: element.height,
};
};
const adjustCropPosition = (
crop: ImageCrop,
scale: ExcalidrawImageElement["scale"],
) => {
let cropX = crop.x;
let cropY = crop.y;
const flipX = scale[0] === -1;
const flipY = scale[1] === -1;
if (flipX) {
cropX = crop.naturalWidth - Math.abs(cropX) - crop.width;
}
if (flipY) {
cropY = crop.naturalHeight - Math.abs(cropY) - crop.height;
}
return {
cropX,
cropY,
};
};
@@ -16,6 +16,7 @@ import {
isArrowElement, isArrowElement,
isElbowArrow, isElbowArrow,
isFrameLikeElement, isFrameLikeElement,
isImageElement,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { getFontString } from "../utils"; import { getFontString } from "../utils";
@@ -251,6 +252,14 @@ export const dragNewElement = ({
} }
if (width !== 0 && height !== 0) { if (width !== 0 && height !== 0) {
let imageInitialDimension = null;
if (isImageElement(newElement)) {
imageInitialDimension = {
initialWidth: width,
initialHeight: height,
};
}
mutateElement( mutateElement(
newElement, newElement,
{ {
@@ -259,6 +268,7 @@ export const dragNewElement = ({
width, width,
height, height,
...textAutoResize, ...textAutoResize,
...imageInitialDimension,
}, },
informMutation, informMutation,
); );
+31
View File
@@ -45,6 +45,12 @@ const RE_GENERIC_EMBED =
const RE_GIPHY = const RE_GIPHY =
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/; /giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
const RE_REDDIT =
/^(?:http(?:s)?:\/\/)?(?:www\.)?reddit\.com\/r\/([a-zA-Z0-9_]+)\/comments\/([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)\/?(?:\?[^#\s]*)?(?:#[^\s]*)?$/;
const RE_REDDIT_EMBED =
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
const ALLOWED_DOMAINS = new Set([ const ALLOWED_DOMAINS = new Set([
"youtube.com", "youtube.com",
"youtu.be", "youtu.be",
@@ -59,6 +65,7 @@ const ALLOWED_DOMAINS = new Set([
"stackblitz.com", "stackblitz.com",
"val.town", "val.town",
"giphy.com", "giphy.com",
"reddit.com",
]); ]);
const ALLOW_SAME_ORIGIN = new Set([ const ALLOW_SAME_ORIGIN = new Set([
@@ -71,6 +78,7 @@ const ALLOW_SAME_ORIGIN = new Set([
"x.com", "x.com",
"*.simplepdf.eu", "*.simplepdf.eu",
"stackblitz.com", "stackblitz.com",
"reddit.com",
]); ]);
export const createSrcDoc = (body: string) => { export const createSrcDoc = (body: string) => {
@@ -218,6 +226,24 @@ export const getEmbedLink = (
return ret; return ret;
} }
if (RE_REDDIT.test(link)) {
const [, page, postId, title] = link.match(RE_REDDIT)!;
const safeURL = sanitizeHTMLAttribute(
`https://reddit.com/r/${page}/comments/${postId}/${title}`,
);
const ret: IframeDataWithSandbox = {
type: "document",
srcdoc: (theme: string) =>
createSrcDoc(
`<blockquote class="reddit-embed-bq" data-embed-theme="${theme}"><a href="${safeURL}"></a><br></blockquote><script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>`,
),
intrinsicSize: { w: 480, h: 480 },
sandbox: { allowSameOrigin },
};
embeddedLinkCache.set(originalLink, ret);
return ret;
}
if (RE_GH_GIST.test(link)) { if (RE_GH_GIST.test(link)) {
const [, user, gistId] = link.match(RE_GH_GIST)!; const [, user, gistId] = link.match(RE_GH_GIST)!;
const safeURL = sanitizeHTMLAttribute( const safeURL = sanitizeHTMLAttribute(
@@ -361,6 +387,11 @@ export const maybeParseEmbedSrc = (str: string): string => {
return twitterMatch[1]; return twitterMatch[1];
} }
const redditMatch = str.match(RE_REDDIT_EMBED);
if (redditMatch && redditMatch.length === 2) {
return redditMatch[1];
}
const gistMatch = str.match(RE_GH_GIST_EMBED); const gistMatch = str.match(RE_GH_GIST_EMBED);
if (gistMatch && gistMatch.length === 2) { if (gistMatch && gistMatch.length === 2) {
return gistMatch[1]; return gistMatch[1];
+2 -2
View File
@@ -29,7 +29,7 @@ import {
isFlowchartNodeElement, isFlowchartNodeElement,
} from "./typeChecks"; } from "./typeChecks";
import { invariant } from "../utils"; import { invariant } from "../utils";
import { point, type LocalPoint } from "../../math"; import { pointFrom, type LocalPoint } from "../../math";
import { aabbForElement } from "../shapes"; import { aabbForElement } from "../shapes";
type LinkDirection = "up" | "right" | "down" | "left"; type LinkDirection = "up" | "right" | "down" | "left";
@@ -421,7 +421,7 @@ const createBindingArrow = (
strokeColor: appState.currentItemStrokeColor, strokeColor: appState.currentItemStrokeColor,
strokeStyle: appState.currentItemStrokeStyle, strokeStyle: appState.currentItemStrokeStyle,
strokeWidth: appState.currentItemStrokeWidth, strokeWidth: appState.currentItemStrokeWidth,
points: [point(0, 0), point(endX, endY)], points: [pointFrom(0, 0), pointFrom(endX, endY)],
elbowed: true, elbowed: true,
}); });
+9 -9
View File
@@ -6,7 +6,7 @@ import type {
Radians, Radians,
} from "../../math"; } from "../../math";
import { import {
point, pointFrom,
pointRotateRads, pointRotateRads,
pointScaleFromOrigin, pointScaleFromOrigin,
radiansToDegrees, radiansToDegrees,
@@ -82,7 +82,7 @@ export const headingForPointFromElement = <
const top = pointRotateRads( const top = pointRotateRads(
pointScaleFromOrigin( pointScaleFromOrigin(
point(element.x + element.width / 2, element.y), pointFrom(element.x + element.width / 2, element.y),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
), ),
@@ -91,7 +91,7 @@ export const headingForPointFromElement = <
); );
const right = pointRotateRads( const right = pointRotateRads(
pointScaleFromOrigin( pointScaleFromOrigin(
point(element.x + element.width, element.y + element.height / 2), pointFrom(element.x + element.width, element.y + element.height / 2),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
), ),
@@ -100,7 +100,7 @@ export const headingForPointFromElement = <
); );
const bottom = pointRotateRads( const bottom = pointRotateRads(
pointScaleFromOrigin( pointScaleFromOrigin(
point(element.x + element.width / 2, element.y + element.height), pointFrom(element.x + element.width / 2, element.y + element.height),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
), ),
@@ -109,7 +109,7 @@ export const headingForPointFromElement = <
); );
const left = pointRotateRads( const left = pointRotateRads(
pointScaleFromOrigin( pointScaleFromOrigin(
point(element.x, element.y + element.height / 2), pointFrom(element.x, element.y + element.height / 2),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
), ),
@@ -133,22 +133,22 @@ export const headingForPointFromElement = <
} }
const topLeft = pointScaleFromOrigin( const topLeft = pointScaleFromOrigin(
point(aabb[0], aabb[1]), pointFrom(aabb[0], aabb[1]),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
) as Point; ) as Point;
const topRight = pointScaleFromOrigin( const topRight = pointScaleFromOrigin(
point(aabb[2], aabb[1]), pointFrom(aabb[2], aabb[1]),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
) as Point; ) as Point;
const bottomLeft = pointScaleFromOrigin( const bottomLeft = pointScaleFromOrigin(
point(aabb[0], aabb[3]), pointFrom(aabb[0], aabb[3]),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
) as Point; ) as Point;
const bottomRight = pointScaleFromOrigin( const bottomRight = pointScaleFromOrigin(
point(aabb[2], aabb[3]), pointFrom(aabb[2], aabb[3]),
midPoint, midPoint,
SEARCH_CONE_MULTIPLIER, SEARCH_CONE_MULTIPLIER,
) as Point; ) as Point;
@@ -49,7 +49,7 @@ import type Scene from "../scene/Scene";
import type { Radians } from "../../math"; import type { Radians } from "../../math";
import { import {
pointCenter, pointCenter,
point, pointFrom,
pointRotateRads, pointRotateRads,
pointsEqual, pointsEqual,
vector, vector,
@@ -108,7 +108,7 @@ export class LinearElementEditor {
this.elementId = element.id as string & { this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
}; };
if (!pointsEqual(element.points[0], point(0, 0))) { if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
console.error("Linear element is not normalized", Error().stack); console.error("Linear element is not normalized", Error().stack);
} }
@@ -287,7 +287,7 @@ export class LinearElementEditor {
element, element,
elementsMap, elementsMap,
referencePoint, referencePoint,
point(scenePointerX, scenePointerY), pointFrom(scenePointerX, scenePointerY),
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
); );
@@ -296,7 +296,7 @@ export class LinearElementEditor {
[ [
{ {
index: selectedIndex, index: selectedIndex,
point: point( point: pointFrom(
width + referencePoint[0], width + referencePoint[0],
height + referencePoint[1], height + referencePoint[1],
), ),
@@ -329,7 +329,7 @@ export class LinearElementEditor {
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - linearElementEditor.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
) )
: point( : pointFrom(
element.points[pointIndex][0] + deltaX, element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY, element.points[pointIndex][1] + deltaY,
); );
@@ -590,11 +590,11 @@ export class LinearElementEditor {
linearElementEditor.segmentMidPointHoveredCoords; linearElementEditor.segmentMidPointHoveredCoords;
if (existingSegmentMidpointHitCoords) { if (existingSegmentMidpointHitCoords) {
const distance = pointDistance( const distance = pointDistance(
point( pointFrom(
existingSegmentMidpointHitCoords[0], existingSegmentMidpointHitCoords[0],
existingSegmentMidpointHitCoords[1], existingSegmentMidpointHitCoords[1],
), ),
point(scenePointer.x, scenePointer.y), pointFrom(scenePointer.x, scenePointer.y),
); );
if (distance <= threshold) { if (distance <= threshold) {
return existingSegmentMidpointHitCoords; return existingSegmentMidpointHitCoords;
@@ -606,8 +606,8 @@ export class LinearElementEditor {
while (index < midPoints.length) { while (index < midPoints.length) {
if (midPoints[index] !== null) { if (midPoints[index] !== null) {
const distance = pointDistance( const distance = pointDistance(
point(midPoints[index]![0], midPoints[index]![1]), pointFrom(midPoints[index]![0], midPoints[index]![1]),
point(scenePointer.x, scenePointer.y), pointFrom(scenePointer.x, scenePointer.y),
); );
if (distance <= threshold) { if (distance <= threshold) {
return midPoints[index]; return midPoints[index];
@@ -626,8 +626,8 @@ export class LinearElementEditor {
zoom: AppState["zoom"], zoom: AppState["zoom"],
) { ) {
let distance = pointDistance( let distance = pointDistance(
point(startPoint[0], startPoint[1]), pointFrom(startPoint[0], startPoint[1]),
point(endPoint[0], endPoint[1]), pointFrom(endPoint[0], endPoint[1]),
); );
if (element.points.length > 2 && element.roundness) { if (element.points.length > 2 && element.roundness) {
distance = getBezierCurveLength(element, endPoint); distance = getBezierCurveLength(element, endPoint);
@@ -829,11 +829,11 @@ export class LinearElementEditor {
const targetPoint = const targetPoint =
clickedPointIndex > -1 && clickedPointIndex > -1 &&
pointRotateRads( pointRotateRads(
point( pointFrom(
element.x + element.points[clickedPointIndex][0], element.x + element.points[clickedPointIndex][0],
element.y + element.points[clickedPointIndex][1], element.y + element.points[clickedPointIndex][1],
), ),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
@@ -928,11 +928,11 @@ export class LinearElementEditor {
element, element,
elementsMap, elementsMap,
lastCommittedPoint, lastCommittedPoint,
point(scenePointerX, scenePointerY), pointFrom(scenePointerX, scenePointerY),
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
); );
newPoint = point( newPoint = pointFrom(
width + lastCommittedPoint[0], width + lastCommittedPoint[0],
height + lastCommittedPoint[1], height + lastCommittedPoint[1],
); );
@@ -984,8 +984,8 @@ export class LinearElementEditor {
const { x, y } = element; const { x, y } = element;
return pointRotateRads( return pointRotateRads(
point(x + p[0], y + p[1]), pointFrom(x + p[0], y + p[1]),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
} }
@@ -1001,8 +1001,8 @@ export class LinearElementEditor {
return element.points.map((p) => { return element.points.map((p) => {
const { x, y } = element; const { x, y } = element;
return pointRotateRads( return pointRotateRads(
point(x + p[0], y + p[1]), pointFrom(x + p[0], y + p[1]),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
}); });
@@ -1025,8 +1025,12 @@ export class LinearElementEditor {
const { x, y } = element; const { x, y } = element;
return p return p
? pointRotateRads(point(x + p[0], y + p[1]), point(cx, cy), element.angle) ? pointRotateRads(
: pointRotateRads(point(x, y), point(cx, cy), element.angle); pointFrom(x + p[0], y + p[1]),
pointFrom(cx, cy),
element.angle,
)
: pointRotateRads(pointFrom(x, y), pointFrom(cx, cy), element.angle);
} }
static pointFromAbsoluteCoords( static pointFromAbsoluteCoords(
@@ -1036,7 +1040,7 @@ export class LinearElementEditor {
): LocalPoint { ): LocalPoint {
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
// No rotation for elbow arrows // No rotation for elbow arrows
return point( return pointFrom(
absoluteCoords[0] - element.x, absoluteCoords[0] - element.x,
absoluteCoords[1] - element.y, absoluteCoords[1] - element.y,
); );
@@ -1046,11 +1050,11 @@ export class LinearElementEditor {
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const [x, y] = pointRotateRads( const [x, y] = pointRotateRads(
point(absoluteCoords[0], absoluteCoords[1]), pointFrom(absoluteCoords[0], absoluteCoords[1]),
point(cx, cy), pointFrom(cx, cy),
-element.angle as Radians, -element.angle as Radians,
); );
return point(x - element.x, y - element.y); return pointFrom(x - element.x, y - element.y);
} }
static getPointIndexUnderCursor( static getPointIndexUnderCursor(
@@ -1071,7 +1075,7 @@ export class LinearElementEditor {
while (--idx > -1) { while (--idx > -1) {
const p = pointHandles[idx]; const p = pointHandles[idx];
if ( if (
pointDistance(point(x, y), point(p[0], p[1])) * zoom.value < pointDistance(pointFrom(x, y), pointFrom(p[0], p[1])) * zoom.value <
// +1px to account for outline stroke // +1px to account for outline stroke
LinearElementEditor.POINT_HANDLE_SIZE + 1 LinearElementEditor.POINT_HANDLE_SIZE + 1
) { ) {
@@ -1093,12 +1097,12 @@ export class LinearElementEditor {
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const [rotatedX, rotatedY] = pointRotateRads( const [rotatedX, rotatedY] = pointRotateRads(
point(pointerOnGrid[0], pointerOnGrid[1]), pointFrom(pointerOnGrid[0], pointerOnGrid[1]),
point(cx, cy), pointFrom(cx, cy),
-element.angle as Radians, -element.angle as Radians,
); );
return point(rotatedX - element.x, rotatedY - element.y); return pointFrom(rotatedX - element.x, rotatedY - element.y);
} }
/** /**
@@ -1118,7 +1122,7 @@ export class LinearElementEditor {
return { return {
points: points.map((p) => { points: points.map((p) => {
return point(p[0] - offsetX, p[1] - offsetY); return pointFrom(p[0] - offsetX, p[1] - offsetY);
}), }),
x: element.x + offsetX, x: element.x + offsetX,
y: element.y + offsetY, y: element.y + offsetY,
@@ -1172,8 +1176,8 @@ export class LinearElementEditor {
} }
acc.push( acc.push(
nextPoint nextPoint
? point((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2) ? pointFrom((p[0] + nextPoint[0]) / 2, (p[1] + nextPoint[1]) / 2)
: point(p[0], p[1]), : pointFrom(p[0], p[1]),
); );
nextSelectedIndices.push(indexCursor + 1); nextSelectedIndices.push(indexCursor + 1);
@@ -1194,7 +1198,7 @@ export class LinearElementEditor {
[ [
{ {
index: element.points.length - 1, index: element.points.length - 1,
point: point(lastPoint[0] + 30, lastPoint[1] + 30), point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
}, },
], ],
elementsMap, elementsMap,
@@ -1235,7 +1239,9 @@ export class LinearElementEditor {
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => { const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => {
if (!pointIndices.includes(idx)) { if (!pointIndices.includes(idx)) {
acc.push( acc.push(
!acc.length ? point(0, 0) : point(p[0] - offsetX, p[1] - offsetY), !acc.length
? pointFrom(0, 0)
: pointFrom(p[0] - offsetX, p[1] - offsetY),
); );
} }
return acc; return acc;
@@ -1312,9 +1318,9 @@ export class LinearElementEditor {
const deltaY = const deltaY =
selectedPointData.point[1] - points[selectedPointData.index][1]; selectedPointData.point[1] - points[selectedPointData.index][1];
return point(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY); return pointFrom(p[0] + deltaX - offsetX, p[1] + deltaY - offsetY);
} }
return offsetX || offsetY ? point(p[0] - offsetX, p[1] - offsetY) : p; return offsetX || offsetY ? pointFrom(p[0] - offsetX, p[1] - offsetY) : p;
}); });
LinearElementEditor._updatePoints( LinearElementEditor._updatePoints(
@@ -1368,8 +1374,8 @@ export class LinearElementEditor {
const origin = linearElementEditor.pointerDownState.origin!; const origin = linearElementEditor.pointerDownState.origin!;
const dist = pointDistance( const dist = pointDistance(
point(origin.x, origin.y), pointFrom(origin.x, origin.y),
point(pointerCoords.x, pointerCoords.y), pointFrom(pointerCoords.x, pointerCoords.y),
); );
if ( if (
!appState.editingLinearElement && !appState.editingLinearElement &&
@@ -1493,8 +1499,8 @@ export class LinearElementEditor {
const dX = prevCenterX - nextCenterX; const dX = prevCenterX - nextCenterX;
const dY = prevCenterY - nextCenterY; const dY = prevCenterY - nextCenterY;
const rotated = pointRotateRads( const rotated = pointRotateRads(
point(offsetX, offsetY), pointFrom(offsetX, offsetY),
point(dX, dY), pointFrom(dX, dY),
element.angle, element.angle,
); );
mutateElement(element, { mutateElement(element, {
@@ -1540,8 +1546,8 @@ export class LinearElementEditor {
); );
return pointRotateRads( return pointRotateRads(
point(width, height), pointFrom(width, height),
point(0, 0), pointFrom(0, 0),
-element.angle as Radians, -element.angle as Radians,
); );
} }
@@ -1611,36 +1617,36 @@ export class LinearElementEditor {
); );
const boundTextX2 = boundTextX1 + boundTextElement.width; const boundTextX2 = boundTextX1 + boundTextElement.width;
const boundTextY2 = boundTextY1 + boundTextElement.height; const boundTextY2 = boundTextY1 + boundTextElement.height;
const centerPoint = point(cx, cy); const centerPoint = pointFrom(cx, cy);
const topLeftRotatedPoint = pointRotateRads( const topLeftRotatedPoint = pointRotateRads(
point(x1, y1), pointFrom(x1, y1),
centerPoint, centerPoint,
element.angle, element.angle,
); );
const topRightRotatedPoint = pointRotateRads( const topRightRotatedPoint = pointRotateRads(
point(x2, y1), pointFrom(x2, y1),
centerPoint, centerPoint,
element.angle, element.angle,
); );
const counterRotateBoundTextTopLeft = pointRotateRads( const counterRotateBoundTextTopLeft = pointRotateRads(
point(boundTextX1, boundTextY1), pointFrom(boundTextX1, boundTextY1),
centerPoint, centerPoint,
-element.angle as Radians, -element.angle as Radians,
); );
const counterRotateBoundTextTopRight = pointRotateRads( const counterRotateBoundTextTopRight = pointRotateRads(
point(boundTextX2, boundTextY1), pointFrom(boundTextX2, boundTextY1),
centerPoint, centerPoint,
-element.angle as Radians, -element.angle as Radians,
); );
const counterRotateBoundTextBottomLeft = pointRotateRads( const counterRotateBoundTextBottomLeft = pointRotateRads(
point(boundTextX1, boundTextY2), pointFrom(boundTextX1, boundTextY2),
centerPoint, centerPoint,
-element.angle as Radians, -element.angle as Radians,
); );
const counterRotateBoundTextBottomRight = pointRotateRads( const counterRotateBoundTextBottomRight = pointRotateRads(
point(boundTextX2, boundTextY2), pointFrom(boundTextX2, boundTextY2),
centerPoint, centerPoint,
-element.angle as Radians, -element.angle as Radians,
); );
@@ -5,7 +5,7 @@ import { FONT_FAMILY, ROUNDNESS } from "../constants";
import { isPrimitive } from "../utils"; import { isPrimitive } from "../utils";
import type { ExcalidrawLinearElement } from "./types"; import type { ExcalidrawLinearElement } from "./types";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { point } from "../../math"; import { pointFrom } from "../../math";
const assertCloneObjects = (source: any, clone: any) => { const assertCloneObjects = (source: any, clone: any) => {
for (const key in clone) { for (const key in clone) {
@@ -38,7 +38,7 @@ describe("duplicating single elements", () => {
element.__proto__ = { hello: "world" }; element.__proto__ = { hello: "world" };
mutateElement(element, { mutateElement(element, {
points: [point<LocalPoint>(1, 2), point<LocalPoint>(3, 4)], points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
}); });
const copy = duplicateElement(null, new Map(), element); const copy = duplicateElement(null, new Map(), element);
@@ -477,6 +477,7 @@ export const newImageElement = (
status?: ExcalidrawImageElement["status"]; status?: ExcalidrawImageElement["status"];
fileId?: ExcalidrawImageElement["fileId"]; fileId?: ExcalidrawImageElement["fileId"];
scale?: ExcalidrawImageElement["scale"]; scale?: ExcalidrawImageElement["scale"];
crop?: ExcalidrawImageElement["crop"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawImageElement> => { ): NonDeleted<ExcalidrawImageElement> => {
return { return {
@@ -487,6 +488,7 @@ export const newImageElement = (
status: opts.status ?? "pending", status: opts.status ?? "pending",
fileId: opts.fileId ?? null, fileId: opts.fileId ?? null,
scale: opts.scale ?? [1, 1], scale: opts.scale ?? [1, 1],
crop: opts.crop ?? null,
}; };
}; };
+54 -42
View File
@@ -58,7 +58,7 @@ import type { GlobalPoint } from "../../math";
import { import {
pointCenter, pointCenter,
normalizeRadians, normalizeRadians,
point, pointFrom,
pointFromPair, pointFromPair,
pointRotateRads, pointRotateRads,
type Radians, type Radians,
@@ -240,8 +240,8 @@ const resizeSingleTextElement = (
); );
// rotation pointer with reverse angle // rotation pointer with reverse angle
const [rotatedX, rotatedY] = pointRotateRads( const [rotatedX, rotatedY] = pointRotateRads(
point(pointerX, pointerY), pointFrom(pointerX, pointerY),
point(cx, cy), pointFrom(cx, cy),
-element.angle as Radians, -element.angle as Radians,
); );
let scaleX = 0; let scaleX = 0;
@@ -276,23 +276,23 @@ const resizeSingleTextElement = (
const startBottomRight = [x2, y2]; const startBottomRight = [x2, y2];
const startCenter = [cx, cy]; const startCenter = [cx, cy];
let newTopLeft = point<GlobalPoint>(x1, y1); let newTopLeft = pointFrom<GlobalPoint>(x1, y1);
if (["n", "w", "nw"].includes(transformHandleType)) { if (["n", "w", "nw"].includes(transformHandleType)) {
newTopLeft = point<GlobalPoint>( newTopLeft = pointFrom<GlobalPoint>(
startBottomRight[0] - Math.abs(nextWidth), startBottomRight[0] - Math.abs(nextWidth),
startBottomRight[1] - Math.abs(nextHeight), startBottomRight[1] - Math.abs(nextHeight),
); );
} }
if (transformHandleType === "ne") { if (transformHandleType === "ne") {
const bottomLeft = [startTopLeft[0], startBottomRight[1]]; const bottomLeft = [startTopLeft[0], startBottomRight[1]];
newTopLeft = point<GlobalPoint>( newTopLeft = pointFrom<GlobalPoint>(
bottomLeft[0], bottomLeft[0],
bottomLeft[1] - Math.abs(nextHeight), bottomLeft[1] - Math.abs(nextHeight),
); );
} }
if (transformHandleType === "sw") { if (transformHandleType === "sw") {
const topRight = [startBottomRight[0], startTopLeft[1]]; const topRight = [startBottomRight[0], startTopLeft[1]];
newTopLeft = point<GlobalPoint>( newTopLeft = pointFrom<GlobalPoint>(
topRight[0] - Math.abs(nextWidth), topRight[0] - Math.abs(nextWidth),
topRight[1], topRight[1],
); );
@@ -311,12 +311,20 @@ const resizeSingleTextElement = (
} }
const angle = element.angle; const angle = element.angle;
const rotatedTopLeft = pointRotateRads(newTopLeft, point(cx, cy), angle); const rotatedTopLeft = pointRotateRads(
const newCenter = point<GlobalPoint>( newTopLeft,
pointFrom(cx, cy),
angle,
);
const newCenter = pointFrom<GlobalPoint>(
newTopLeft[0] + Math.abs(nextWidth) / 2, newTopLeft[0] + Math.abs(nextWidth) / 2,
newTopLeft[1] + Math.abs(nextHeight) / 2, newTopLeft[1] + Math.abs(nextHeight) / 2,
); );
const rotatedNewCenter = pointRotateRads(newCenter, point(cx, cy), angle); const rotatedNewCenter = pointRotateRads(
newCenter,
pointFrom(cx, cy),
angle,
);
newTopLeft = pointRotateRads( newTopLeft = pointRotateRads(
rotatedTopLeft, rotatedTopLeft,
rotatedNewCenter, rotatedNewCenter,
@@ -341,12 +349,12 @@ const resizeSingleTextElement = (
stateAtResizeStart.height, stateAtResizeStart.height,
true, true,
); );
const startTopLeft = point<GlobalPoint>(x1, y1); const startTopLeft = pointFrom<GlobalPoint>(x1, y1);
const startBottomRight = point<GlobalPoint>(x2, y2); const startBottomRight = pointFrom<GlobalPoint>(x2, y2);
const startCenter = pointCenter(startTopLeft, startBottomRight); const startCenter = pointCenter(startTopLeft, startBottomRight);
const rotatedPointer = pointRotateRads( const rotatedPointer = pointRotateRads(
point(pointerX, pointerY), pointFrom(pointerX, pointerY),
startCenter, startCenter,
-stateAtResizeStart.angle as Radians, -stateAtResizeStart.angle as Radians,
); );
@@ -419,7 +427,7 @@ const resizeSingleTextElement = (
startCenter, startCenter,
angle, angle,
); );
const newCenter = point( const newCenter = pointFrom(
newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
newTopLeft[1] + Math.abs(newBoundsHeight) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
); );
@@ -461,13 +469,13 @@ export const resizeSingleElement = (
stateAtResizeStart.height, stateAtResizeStart.height,
true, true,
); );
const startTopLeft = point(x1, y1); const startTopLeft = pointFrom(x1, y1);
const startBottomRight = point(x2, y2); const startBottomRight = pointFrom(x2, y2);
const startCenter = pointCenter(startTopLeft, startBottomRight); const startCenter = pointCenter(startTopLeft, startBottomRight);
// Calculate new dimensions based on cursor position // Calculate new dimensions based on cursor position
const rotatedPointer = pointRotateRads( const rotatedPointer = pointRotateRads(
point(pointerX, pointerY), pointFrom(pointerX, pointerY),
startCenter, startCenter,
-stateAtResizeStart.angle as Radians, -stateAtResizeStart.angle as Radians,
); );
@@ -648,7 +656,7 @@ export const resizeSingleElement = (
startCenter, startCenter,
angle, angle,
); );
const newCenter = point( const newCenter = pointFrom(
newTopLeft[0] + Math.abs(newBoundsWidth) / 2, newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
newTopLeft[1] + Math.abs(newBoundsHeight) / 2, newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
); );
@@ -817,20 +825,20 @@ export const resizeMultipleElements = (
const direction = transformHandleType; const direction = transformHandleType;
const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = { const anchorsMap: Record<TransformHandleDirection, GlobalPoint> = {
ne: point(minX, maxY), ne: pointFrom(minX, maxY),
se: point(minX, minY), se: pointFrom(minX, minY),
sw: point(maxX, minY), sw: pointFrom(maxX, minY),
nw: point(maxX, maxY), nw: pointFrom(maxX, maxY),
e: point(minX, minY + height / 2), e: pointFrom(minX, minY + height / 2),
w: point(maxX, minY + height / 2), w: pointFrom(maxX, minY + height / 2),
n: point(minX + width / 2, maxY), n: pointFrom(minX + width / 2, maxY),
s: point(minX + width / 2, minY), s: pointFrom(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] = shouldResizeFromCenter const [anchorX, anchorY] = shouldResizeFromCenter
? point(midX, midY) ? pointFrom(midX, midY)
: anchorsMap[direction]; : anchorsMap[direction];
const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1; const resizeFromCenterScale = shouldResizeFromCenter ? 2 : 1;
@@ -1044,8 +1052,8 @@ const rotateMultipleElements = (
const origAngle = const origAngle =
originalElements.get(element.id)?.angle ?? element.angle; originalElements.get(element.id)?.angle ?? element.angle;
const [rotatedCX, rotatedCY] = pointRotateRads( const [rotatedCX, rotatedCY] = pointRotateRads(
point(cx, cy), pointFrom(cx, cy),
point(centerX, centerY), pointFrom(centerX, centerY),
(centerAngle + origAngle - element.angle) as Radians, (centerAngle + origAngle - element.angle) as Radians,
); );
@@ -1101,40 +1109,44 @@ export const getResizeOffsetXY = (
const angle = ( const angle = (
selectedElements.length === 1 ? selectedElements[0].angle : 0 selectedElements.length === 1 ? selectedElements[0].angle : 0
) as Radians; ) as Radians;
[x, y] = pointRotateRads(point(x, y), point(cx, cy), -angle as Radians); [x, y] = pointRotateRads(
pointFrom(x, y),
pointFrom(cx, cy),
-angle as Radians,
);
switch (transformHandleType) { switch (transformHandleType) {
case "n": case "n":
return pointRotateRads( return pointRotateRads(
point(x - (x1 + x2) / 2, y - y1), pointFrom(x - (x1 + x2) / 2, y - y1),
point(0, 0), pointFrom(0, 0),
angle, angle,
); );
case "s": case "s":
return pointRotateRads( return pointRotateRads(
point(x - (x1 + x2) / 2, y - y2), pointFrom(x - (x1 + x2) / 2, y - y2),
point(0, 0), pointFrom(0, 0),
angle, angle,
); );
case "w": case "w":
return pointRotateRads( return pointRotateRads(
point(x - x1, y - (y1 + y2) / 2), pointFrom(x - x1, y - (y1 + y2) / 2),
point(0, 0), pointFrom(0, 0),
angle, angle,
); );
case "e": case "e":
return pointRotateRads( return pointRotateRads(
point(x - x2, y - (y1 + y2) / 2), pointFrom(x - x2, y - (y1 + y2) / 2),
point(0, 0), pointFrom(0, 0),
angle, angle,
); );
case "nw": case "nw":
return pointRotateRads(point(x - x1, y - y1), point(0, 0), angle); return pointRotateRads(pointFrom(x - x1, y - y1), pointFrom(0, 0), angle);
case "ne": case "ne":
return pointRotateRads(point(x - x2, y - y1), point(0, 0), angle); return pointRotateRads(pointFrom(x - x2, y - y1), pointFrom(0, 0), angle);
case "sw": case "sw":
return pointRotateRads(point(x - x1, y - y2), point(0, 0), angle); return pointRotateRads(pointFrom(x - x1, y - y2), pointFrom(0, 0), angle);
case "se": case "se":
return pointRotateRads(point(x - x2, y - y2), point(0, 0), angle); return pointRotateRads(pointFrom(x - x2, y - y2), pointFrom(0, 0), angle);
default: default:
return [0, 0]; return [0, 0];
} }
+23 -15
View File
@@ -20,10 +20,10 @@ import type { AppState, Device, Zoom } from "../types";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
import { getElementAbsoluteCoords } from "./bounds"; import { getElementAbsoluteCoords } from "./bounds";
import { SIDE_RESIZING_THRESHOLD } from "../constants"; import { SIDE_RESIZING_THRESHOLD } from "../constants";
import { isLinearElement } from "./typeChecks"; import { isImageElement, isLinearElement } from "./typeChecks";
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math"; import type { GlobalPoint, LineSegment, LocalPoint } from "../../math";
import { import {
point, pointFrom,
pointOnLineSegment, pointOnLineSegment,
pointRotateRads, pointRotateRads,
type Radians, type Radians,
@@ -90,18 +90,26 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
// do not resize from the sides for linear elements with only two points // do not resize from the sides for linear elements with only two points
if (!(isLinearElement(element) && element.points.length <= 2)) { if (!(isLinearElement(element) && element.points.length <= 2)) {
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const SPACING = isImageElement(element)
? 0
: SIDE_RESIZING_THRESHOLD / zoom.value;
const ZOOMED_SIDE_RESIZING_THRESHOLD =
SIDE_RESIZING_THRESHOLD / zoom.value;
const sides = getSelectionBorders( const sides = getSelectionBorders(
point(x1 - SPACING, y1 - SPACING), pointFrom(x1 - SPACING, y1 - SPACING),
point(x2 + SPACING, y2 + SPACING), pointFrom(x2 + SPACING, y2 + SPACING),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
for (const [dir, side] of Object.entries(sides)) { for (const [dir, side] of Object.entries(sides)) {
// test to see if x, y are on the line segment // test to see if x, y are on the line segment
if ( if (
pointOnLineSegment(point(x, y), side as LineSegment<Point>, SPACING) pointOnLineSegment(
pointFrom(x, y),
side as LineSegment<Point>,
ZOOMED_SIDE_RESIZING_THRESHOLD,
)
) { ) {
return dir as TransformHandleType; return dir as TransformHandleType;
} }
@@ -178,9 +186,9 @@ export const getTransformHandleTypeFromCoords = <
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
const sides = getSelectionBorders( const sides = getSelectionBorders(
point(x1 - SPACING, y1 - SPACING), pointFrom(x1 - SPACING, y1 - SPACING),
point(x2 + SPACING, y2 + SPACING), pointFrom(x2 + SPACING, y2 + SPACING),
point(cx, cy), pointFrom(cx, cy),
0 as Radians, 0 as Radians,
); );
@@ -188,7 +196,7 @@ export const getTransformHandleTypeFromCoords = <
// test to see if x, y are on the line segment // test to see if x, y are on the line segment
if ( if (
pointOnLineSegment( pointOnLineSegment(
point(scenePointerX, scenePointerY), pointFrom(scenePointerX, scenePointerY),
side as LineSegment<Point>, side as LineSegment<Point>,
SPACING, SPACING,
) )
@@ -265,10 +273,10 @@ const getSelectionBorders = <Point extends LocalPoint | GlobalPoint>(
center: Point, center: Point,
angle: Radians, angle: Radians,
) => { ) => {
const topLeft = pointRotateRads(point(x1, y1), center, angle); const topLeft = pointRotateRads(pointFrom(x1, y1), center, angle);
const topRight = pointRotateRads(point(x2, y1), center, angle); const topRight = pointRotateRads(pointFrom(x2, y1), center, angle);
const bottomLeft = pointRotateRads(point(x1, y2), center, angle); const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, angle);
const bottomRight = pointRotateRads(point(x2, y2), center, angle); const bottomRight = pointRotateRads(pointFrom(x2, y2), center, angle);
return { return {
n: [topLeft, topRight], n: [topLeft, topRight],
+5 -5
View File
@@ -17,7 +17,7 @@ import type {
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
} from "./types"; } from "./types";
import { ARROW_TYPE } from "../constants"; import { ARROW_TYPE } from "../constants";
import { point } from "../../math"; import { pointFrom } from "../../math";
const { h } = window; const { h } = window;
@@ -32,8 +32,8 @@ describe("elbow arrow routing", () => {
}) as ExcalidrawElbowArrowElement; }) as ExcalidrawElbowArrowElement;
scene.insertElement(arrow); scene.insertElement(arrow);
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [ mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
point(-45 - arrow.x, -100.1 - arrow.y), pointFrom(-45 - arrow.x, -100.1 - arrow.y),
point(45 - arrow.x, 99.9 - arrow.y), pointFrom(45 - arrow.x, 99.9 - arrow.y),
]); ]);
expect(arrow.points).toEqual([ expect(arrow.points).toEqual([
[0, 0], [0, 0],
@@ -69,7 +69,7 @@ describe("elbow arrow routing", () => {
y: -100.1, y: -100.1,
width: 90, width: 90,
height: 200, height: 200,
points: [point(0, 0), point(90, 200)], points: [pointFrom(0, 0), pointFrom(90, 200)],
}) as ExcalidrawElbowArrowElement; }) as ExcalidrawElbowArrowElement;
scene.insertElement(rectangle1); scene.insertElement(rectangle1);
scene.insertElement(rectangle2); scene.insertElement(rectangle2);
@@ -81,7 +81,7 @@ describe("elbow arrow routing", () => {
expect(arrow.startBinding).not.toBe(null); expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null); expect(arrow.endBinding).not.toBe(null);
mutateElbowArrow(arrow, elementsMap, [point(0, 0), point(90, 200)]); mutateElbowArrow(arrow, elementsMap, [pointFrom(0, 0), pointFrom(90, 200)]);
expect(arrow.points).toEqual([ expect(arrow.points).toEqual([
[0, 0], [0, 0],
+5 -5
View File
@@ -1,6 +1,6 @@
import type { Radians } from "../../math"; import type { Radians } from "../../math";
import { import {
point, pointFrom,
pointScaleFromOrigin, pointScaleFromOrigin,
pointTranslate, pointTranslate,
vector, vector,
@@ -743,13 +743,13 @@ const getDonglePosition = (
): GlobalPoint => { ): GlobalPoint => {
switch (heading) { switch (heading) {
case HEADING_UP: case HEADING_UP:
return point(p[0], bounds[1]); return pointFrom(p[0], bounds[1]);
case HEADING_RIGHT: case HEADING_RIGHT:
return point(bounds[2], p[1]); return pointFrom(bounds[2], p[1]);
case HEADING_DOWN: case HEADING_DOWN:
return point(p[0], bounds[3]); return pointFrom(p[0], bounds[3]);
} }
return point(bounds[0], p[1]); return pointFrom(bounds[0], p[1]);
}; };
const estimateSegmentCount = ( const estimateSegmentCount = (
@@ -19,7 +19,7 @@ import type {
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { getOriginalContainerHeightFromCache } from "./containerCache"; import { getOriginalContainerHeightFromCache } from "./containerCache";
import { getTextEditor, updateTextEditor } from "../tests/queries/dom"; import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
import { point } from "../../math"; import { pointFrom } from "../../math";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -42,7 +42,7 @@ describe("textWysiwyg", () => {
type: "line", type: "line",
width: 100, width: 100,
height: 0, height: 0,
points: [point(0, 0), point(100, 0)], points: [pointFrom(0, 0), pointFrom(100, 0)],
}); });
const textSize = 20; const textSize = 20;
const text = API.createElement({ const text = API.createElement({
@@ -11,6 +11,7 @@ import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
import { import {
isElbowArrow, isElbowArrow,
isFrameLikeElement, isFrameLikeElement,
isImageElement,
isLinearElement, isLinearElement,
} from "./typeChecks"; } from "./typeChecks";
import { import {
@@ -19,7 +20,7 @@ import {
isIOS, isIOS,
} from "../constants"; } from "../constants";
import type { Radians } from "../../math"; import type { Radians } from "../../math";
import { point, pointRotateRads } from "../../math"; import { pointFrom, pointRotateRads } from "../../math";
export type TransformHandleDirection = export type TransformHandleDirection =
| "n" | "n"
@@ -95,8 +96,8 @@ const generateTransformHandle = (
angle: Radians, angle: Radians,
): TransformHandle => { ): TransformHandle => {
const [xx, yy] = pointRotateRads( const [xx, yy] = pointRotateRads(
point(x + width / 2, y + height / 2), pointFrom(x + width / 2, y + height / 2),
point(cx, cy), pointFrom(cx, cy),
angle, angle,
); );
return [xx - width / 2, yy - height / 2, width, height]; return [xx - width / 2, yy - height / 2, width, height];
@@ -129,6 +130,7 @@ export const getTransformHandlesFromCoords = (
pointerType: PointerType, pointerType: PointerType,
omitSides: { [T in TransformHandleType]?: boolean } = {}, omitSides: { [T in TransformHandleType]?: boolean } = {},
margin = 4, margin = 4,
spacing = DEFAULT_TRANSFORM_HANDLE_SPACING,
): TransformHandles => { ): TransformHandles => {
const size = transformHandleSizes[pointerType]; const size = transformHandleSizes[pointerType];
const handleWidth = size / zoom.value; const handleWidth = size / zoom.value;
@@ -140,8 +142,7 @@ export const getTransformHandlesFromCoords = (
const width = x2 - x1; const width = x2 - x1;
const height = y2 - y1; const height = y2 - y1;
const dashedLineMargin = margin / zoom.value; const dashedLineMargin = margin / zoom.value;
const centeringOffset = const centeringOffset = (size - spacing * 2) / (2 * zoom.value);
(size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value);
const transformHandles: TransformHandles = { const transformHandles: TransformHandles = {
nw: omitSides.nw nw: omitSides.nw
@@ -301,8 +302,10 @@ export const getTransformHandles = (
rotation: true, rotation: true,
}; };
} }
const dashedLineMargin = isLinearElement(element) const margin = isLinearElement(element)
? DEFAULT_TRANSFORM_HANDLE_SPACING + 8 ? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
: isImageElement(element)
? 0
: DEFAULT_TRANSFORM_HANDLE_SPACING; : DEFAULT_TRANSFORM_HANDLE_SPACING;
return getTransformHandlesFromCoords( return getTransformHandlesFromCoords(
getElementAbsoluteCoords(element, elementsMap, true), getElementAbsoluteCoords(element, elementsMap, true),
@@ -310,7 +313,8 @@ export const getTransformHandles = (
zoom, zoom,
pointerType, pointerType,
omitSides, omitSides,
dashedLineMargin, margin,
isImageElement(element) ? 0 : undefined,
); );
}; };
+11
View File
@@ -132,6 +132,15 @@ export type IframeData =
| { type: "document"; srcdoc: (theme: Theme) => string } | { type: "document"; srcdoc: (theme: Theme) => string }
); );
export type ImageCrop = {
x: number;
y: number;
width: number;
height: number;
naturalWidth: number;
naturalHeight: number;
};
export type ExcalidrawImageElement = _ExcalidrawElementBase & export type ExcalidrawImageElement = _ExcalidrawElementBase &
Readonly<{ Readonly<{
type: "image"; type: "image";
@@ -140,6 +149,8 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase &
status: "pending" | "saved" | "error"; status: "pending" | "saved" | "error";
/** X and Y scale factors <-1, 1>, used for image axis flipping */ /** X and Y scale factors <-1, 1>, used for image axis flipping */
scale: [number, number]; scale: [number, number];
/** whether an element is cropped */
crop: ImageCrop | null;
}>; }>;
export type InitializedExcalidrawImageElement = MarkNonNullable< export type InitializedExcalidrawImageElement = MarkNonNullable<
+7 -7
View File
@@ -24,14 +24,14 @@ import Cascadia from "./assets/CascadiaCode-Regular.woff2";
import ComicShanns from "./assets/ComicShanns-Regular.woff2"; import ComicShanns from "./assets/ComicShanns-Regular.woff2";
import LiberationSans from "./assets/LiberationSans-Regular.woff2"; import LiberationSans from "./assets/LiberationSans-Regular.woff2";
import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2"; import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2"; import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"; import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2"; import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2"; import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2"; import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2"; import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
export class Fonts { export class Fonts {
// it's ok to track fonts across multiple instances only once, so let's use // it's ok to track fonts across multiple instances only once, so let's use
+4 -4
View File
@@ -29,7 +29,7 @@ import { getElementLineSegments } from "./element/bounds";
import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/"; import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/";
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks"; import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
import type { ReadonlySetLike } from "./utility-types"; import type { ReadonlySetLike } from "./utility-types";
import { isPointWithinBounds, point } from "../math"; import { isPointWithinBounds, pointFrom } from "../math";
// --------------------------- Frame State ------------------------------------ // --------------------------- Frame State ------------------------------------
export const bindElementsToFramesAfterDuplication = ( export const bindElementsToFramesAfterDuplication = (
@@ -159,9 +159,9 @@ export const isCursorInFrame = (
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap); const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
return isPointWithinBounds( return isPointWithinBounds(
point(fx1, fy1), pointFrom(fx1, fy1),
point(cursorCoords.x, cursorCoords.y), pointFrom(cursorCoords.x, cursorCoords.y),
point(fx2, fy2), pointFrom(fx2, fy2),
); );
}; };
+6 -2
View File
@@ -328,7 +328,9 @@
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging", "deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
"eraserRevert": "Hold Alt to revert the elements marked for deletion", "eraserRevert": "Hold Alt to revert the elements marked for deletion",
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.", "firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.",
"disableSnapping": "Hold CtrlOrCmd to disable snapping" "disableSnapping": "Hold CtrlOrCmd to disable snapping",
"enterCropEditor": "Double click the image or press ENTER to crop the image",
"leaveCropEditor": "Click outside the image or press ENTER or ESCAPE to finish cropping"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Cannot show preview", "cannotShowPreview": "Cannot show preview",
@@ -399,7 +401,9 @@
"zoomToSelection": "Zoom to selection", "zoomToSelection": "Zoom to selection",
"toggleElementLock": "Lock/unlock selection", "toggleElementLock": "Lock/unlock selection",
"movePageUpDown": "Move page up/down", "movePageUpDown": "Move page up/down",
"movePageLeftRight": "Move page left/right" "movePageLeftRight": "Move page left/right",
"cropStart": "Crop image",
"cropFinish": "Finish image cropping"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Clear canvas" "title": "Clear canvas"
+179 -57
View File
@@ -54,6 +54,7 @@ import oc from "open-color";
import { import {
isElbowArrow, isElbowArrow,
isFrameLikeElement, isFrameLikeElement,
isImageElement,
isLinearElement, isLinearElement,
isTextElement, isTextElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
@@ -62,6 +63,7 @@ import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
ExcalidrawImageElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElement, ExcalidrawTextElement,
GroupId, GroupId,
@@ -307,38 +309,42 @@ const renderBindingHighlightForSuggestedPointBinding = (
}); });
}; };
type ElementSelectionBorder = {
angle: number;
x1: number;
y1: number;
x2: number;
y2: number;
selectionColors: string[];
dashed?: boolean;
cx: number;
cy: number;
activeEmbeddable: boolean;
padding?: number;
};
const renderSelectionBorder = ( const renderSelectionBorder = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
elementProperties: { elementProperties: ElementSelectionBorder,
angle: number;
elementX1: number;
elementY1: number;
elementX2: number;
elementY2: number;
selectionColors: string[];
dashed?: boolean;
cx: number;
cy: number;
activeEmbeddable: boolean;
},
) => { ) => {
const { const {
angle, angle,
elementX1, x1,
elementY1, y1,
elementX2, x2,
elementY2, y2,
selectionColors, selectionColors,
cx, cx,
cy, cy,
dashed, dashed,
activeEmbeddable, activeEmbeddable,
} = elementProperties; } = elementProperties;
const elementWidth = elementX2 - elementX1; const elementWidth = x2 - x1;
const elementHeight = elementY2 - elementY1; const elementHeight = y2 - y1;
const padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2; const padding =
elementProperties.padding ?? DEFAULT_TRANSFORM_HANDLE_SPACING * 2;
const linePadding = padding / appState.zoom.value; const linePadding = padding / appState.zoom.value;
const lineWidth = 8 / appState.zoom.value; const lineWidth = 8 / appState.zoom.value;
@@ -360,8 +366,8 @@ const renderSelectionBorder = (
context.lineDashOffset = (lineWidth + spaceWidth) * index; context.lineDashOffset = (lineWidth + spaceWidth) * index;
strokeRectWithRotation( strokeRectWithRotation(
context, context,
elementX1 - linePadding, x1 - linePadding,
elementY1 - linePadding, y1 - linePadding,
elementWidth + linePadding * 2, elementWidth + linePadding * 2,
elementHeight + linePadding * 2, elementHeight + linePadding * 2,
cx, cx,
@@ -433,18 +439,17 @@ const renderElementsBoxHighlight = (
); );
const getSelectionFromElements = (elements: ExcalidrawElement[]) => { const getSelectionFromElements = (elements: ExcalidrawElement[]) => {
const [elementX1, elementY1, elementX2, elementY2] = const [x1, y1, x2, y2] = getCommonBounds(elements);
getCommonBounds(elements);
return { return {
angle: 0, angle: 0,
elementX1, x1,
elementX2, x2,
elementY1, y1,
elementY2, y2,
selectionColors: ["rgb(0,118,255)"], selectionColors: ["rgb(0,118,255)"],
dashed: false, dashed: false,
cx: elementX1 + (elementX2 - elementX1) / 2, cx: x1 + (x2 - x1) / 2,
cy: elementY1 + (elementY2 - elementY1) / 2, cy: y1 + (y2 - y1) / 2,
activeEmbeddable: false, activeEmbeddable: false,
}; };
}; };
@@ -594,6 +599,111 @@ const renderTransformHandles = (
}); });
}; };
const renderCropHandles = (
context: CanvasRenderingContext2D,
renderConfig: InteractiveCanvasRenderConfig,
appState: InteractiveCanvasAppState,
croppingElement: ExcalidrawImageElement,
elementsMap: ElementsMap,
): void => {
const [x1, y1, , , cx, cy] = getElementAbsoluteCoords(
croppingElement,
elementsMap,
);
const LINE_WIDTH = 3;
const LINE_LENGTH = 20;
const ZOOMED_LINE_WIDTH = LINE_WIDTH / appState.zoom.value;
const ZOOMED_HALF_LINE_WIDTH = ZOOMED_LINE_WIDTH / 2;
const HALF_WIDTH = cx - x1 + ZOOMED_LINE_WIDTH;
const HALF_HEIGHT = cy - y1 + ZOOMED_LINE_WIDTH;
const HORIZONTAL_LINE_LENGTH = Math.min(
LINE_LENGTH / appState.zoom.value,
HALF_WIDTH,
);
const VERTICAL_LINE_LENGTH = Math.min(
LINE_LENGTH / appState.zoom.value,
HALF_HEIGHT,
);
context.save();
context.fillStyle = renderConfig.selectionColor;
context.strokeStyle = renderConfig.selectionColor;
context.lineWidth = ZOOMED_LINE_WIDTH;
const handles: Array<
[
[number, number],
[number, number],
[number, number],
[number, number],
[number, number],
]
> = [
[
// x, y
[-HALF_WIDTH, -HALF_HEIGHT],
// horizontal line: first start and to
[0, ZOOMED_HALF_LINE_WIDTH],
[HORIZONTAL_LINE_LENGTH, ZOOMED_HALF_LINE_WIDTH],
// vertical line: second start and to
[ZOOMED_HALF_LINE_WIDTH, 0],
[ZOOMED_HALF_LINE_WIDTH, VERTICAL_LINE_LENGTH],
],
[
[HALF_WIDTH - ZOOMED_HALF_LINE_WIDTH, -HALF_HEIGHT],
[ZOOMED_HALF_LINE_WIDTH, ZOOMED_HALF_LINE_WIDTH],
[
-HORIZONTAL_LINE_LENGTH + ZOOMED_HALF_LINE_WIDTH,
ZOOMED_HALF_LINE_WIDTH,
],
[0, 0],
[0, VERTICAL_LINE_LENGTH],
],
[
[-HALF_WIDTH, HALF_HEIGHT],
[0, -ZOOMED_HALF_LINE_WIDTH],
[HORIZONTAL_LINE_LENGTH, -ZOOMED_HALF_LINE_WIDTH],
[ZOOMED_HALF_LINE_WIDTH, 0],
[ZOOMED_HALF_LINE_WIDTH, -VERTICAL_LINE_LENGTH],
],
[
[HALF_WIDTH - ZOOMED_HALF_LINE_WIDTH, HALF_HEIGHT],
[ZOOMED_HALF_LINE_WIDTH, -ZOOMED_HALF_LINE_WIDTH],
[
-HORIZONTAL_LINE_LENGTH + ZOOMED_HALF_LINE_WIDTH,
-ZOOMED_HALF_LINE_WIDTH,
],
[0, 0],
[0, -VERTICAL_LINE_LENGTH],
],
];
handles.forEach((handle) => {
const [[x, y], [x1s, y1s], [x1t, y1t], [x2s, y2s], [x2t, y2t]] = handle;
context.save();
context.translate(cx, cy);
context.rotate(croppingElement.angle);
context.beginPath();
context.moveTo(x + x1s, y + y1s);
context.lineTo(x + x1t, y + y1t);
context.stroke();
context.beginPath();
context.moveTo(x + x2s, y + y2s);
context.lineTo(x + x2t, y + y2t);
context.stroke();
context.restore();
});
context.restore();
};
const renderTextBox = ( const renderTextBox = (
text: NonDeleted<ExcalidrawTextElement>, text: NonDeleted<ExcalidrawTextElement>,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
@@ -671,7 +781,7 @@ const _renderInteractiveScene = ({
} }
// Paint selection element // Paint selection element
if (appState.selectionElement) { if (appState.selectionElement && !appState.isCropping) {
try { try {
renderSelectionElement( renderSelectionElement(
appState.selectionElement, appState.selectionElement,
@@ -783,18 +893,7 @@ const _renderInteractiveScene = ({
// Optimisation for finding quickly relevant element ids // Optimisation for finding quickly relevant element ids
const locallySelectedIds = arrayToMap(selectedElements); const locallySelectedIds = arrayToMap(selectedElements);
const selections: { const selections: ElementSelectionBorder[] = [];
angle: number;
elementX1: number;
elementY1: number;
elementX2: number;
elementY2: number;
selectionColors: string[];
dashed?: boolean;
cx: number;
cy: number;
activeEmbeddable: boolean;
}[] = [];
for (const element of elementsMap.values()) { for (const element of elementsMap.values()) {
const selectionColors = []; const selectionColors = [];
@@ -833,14 +932,17 @@ const _renderInteractiveScene = ({
} }
if (selectionColors.length) { if (selectionColors.length) {
const [elementX1, elementY1, elementX2, elementY2, cx, cy] = const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
getElementAbsoluteCoords(element, elementsMap, true); element,
elementsMap,
true,
);
selections.push({ selections.push({
angle: element.angle, angle: element.angle,
elementX1, x1,
elementY1, y1,
elementX2, x2,
elementY2, y2,
selectionColors, selectionColors,
dashed: !!remoteClients, dashed: !!remoteClients,
cx, cx,
@@ -848,24 +950,28 @@ const _renderInteractiveScene = ({
activeEmbeddable: activeEmbeddable:
appState.activeEmbeddable?.element === element && appState.activeEmbeddable?.element === element &&
appState.activeEmbeddable.state === "active", appState.activeEmbeddable.state === "active",
padding:
element.id === appState.croppingElementId ||
isImageElement(element)
? 0
: undefined,
}); });
} }
} }
const addSelectionForGroupId = (groupId: GroupId) => { const addSelectionForGroupId = (groupId: GroupId) => {
const groupElements = getElementsInGroup(elementsMap, groupId); const groupElements = getElementsInGroup(elementsMap, groupId);
const [elementX1, elementY1, elementX2, elementY2] = const [x1, y1, x2, y2] = getCommonBounds(groupElements);
getCommonBounds(groupElements);
selections.push({ selections.push({
angle: 0, angle: 0,
elementX1, x1,
elementX2, x2,
elementY1, y1,
elementY2, y2,
selectionColors: [oc.black], selectionColors: [oc.black],
dashed: true, dashed: true,
cx: elementX1 + (elementX2 - elementX1) / 2, cx: x1 + (x2 - x1) / 2,
cy: elementY1 + (elementY2 - elementY1) / 2, cy: y1 + (y2 - y1) / 2,
activeEmbeddable: false, activeEmbeddable: false,
}); });
}; };
@@ -900,7 +1006,9 @@ const _renderInteractiveScene = ({
!appState.viewModeEnabled && !appState.viewModeEnabled &&
showBoundingBox && showBoundingBox &&
// do not show transform handles when text is being edited // do not show transform handles when text is being edited
!isTextElement(appState.editingTextElement) !isTextElement(appState.editingTextElement) &&
// do not show transform handles when image is being cropped
!appState.croppingElementId
) { ) {
renderTransformHandles( renderTransformHandles(
context, context,
@@ -910,6 +1018,20 @@ const _renderInteractiveScene = ({
selectedElements[0].angle, selectedElements[0].angle,
); );
} }
if (appState.croppingElementId && !appState.isCropping) {
const croppingElement = elementsMap.get(appState.croppingElementId);
if (croppingElement && isImageElement(croppingElement)) {
renderCropHandles(
context,
renderConfig,
appState,
croppingElement,
elementsMap,
);
}
}
} else if (selectedElements.length > 1 && !appState.isRotating) { } else if (selectedElements.length > 1 && !appState.isRotating) {
const dashedLinePadding = const dashedLinePadding =
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value; (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
+59 -4
View File
@@ -17,6 +17,7 @@ import {
isArrowElement, isArrowElement,
hasBoundTextElement, hasBoundTextElement,
isMagicFrameElement, isMagicFrameElement,
isImageElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
import { getElementAbsoluteCoords } from "../element/bounds"; import { getElementAbsoluteCoords } from "../element/bounds";
import type { RoughCanvas } from "roughjs/bin/canvas"; import type { RoughCanvas } from "roughjs/bin/canvas";
@@ -61,6 +62,7 @@ import { ShapeCache } from "../scene/ShapeCache";
import { getVerticalOffset } from "../fonts"; import { getVerticalOffset } from "../fonts";
import { isRightAngleRads } from "../../math"; import { isRightAngleRads } from "../../math";
import { getCornerRadius } from "../shapes"; import { getCornerRadius } from "../shapes";
import { getUncroppedImageElement } from "../element/cropElement";
// using a stronger invert (100% vs our regular 93%) and saturate // using a stronger invert (100% vs our regular 93%) and saturate
// as a temp hack to make images in dark theme look closer to original // as a temp hack to make images in dark theme look closer to original
@@ -434,8 +436,22 @@ const drawElementOnCanvas = (
); );
context.clip(); context.clip();
} }
const { x, y, width, height } = element.crop
? element.crop
: {
x: 0,
y: 0,
width: img.naturalWidth,
height: img.naturalHeight,
};
context.drawImage( context.drawImage(
img, img,
x,
y,
width,
height,
0 /* hardcoded for the selection box*/, 0 /* hardcoded for the selection box*/,
0, 0,
element.width, element.width,
@@ -921,14 +937,53 @@ export const renderElement = (
context.imageSmoothingEnabled = false; context.imageSmoothingEnabled = false;
} }
drawElementFromCanvas( if (
elementWithCanvas, element.id === appState.croppingElementId &&
context, isImageElement(elementWithCanvas.element) &&
elementWithCanvas.element.crop !== null
) {
context.save();
context.globalAlpha = 0.1;
const uncroppedElementCanvas = generateElementCanvas(
getUncroppedImageElement(elementWithCanvas.element, elementsMap),
allElementsMap,
appState.zoom,
renderConfig,
appState,
);
if (uncroppedElementCanvas) {
drawElementFromCanvas(
uncroppedElementCanvas,
context,
renderConfig,
appState,
allElementsMap,
);
}
context.restore();
}
const _elementWithCanvas = generateElementCanvas(
elementWithCanvas.element,
allElementsMap,
appState.zoom,
renderConfig, renderConfig,
appState, appState,
allElementsMap,
); );
if (_elementWithCanvas) {
drawElementFromCanvas(
_elementWithCanvas,
context,
renderConfig,
appState,
allElementsMap,
);
}
// reset // reset
context.imageSmoothingEnabled = currentImageSmoothingStatus; context.imageSmoothingEnabled = currentImageSmoothingStatus;
} }
+23 -15
View File
@@ -1,4 +1,4 @@
import { point, type GlobalPoint, type LocalPoint } from "../../math"; import { pointFrom, type GlobalPoint, type LocalPoint } from "../../math";
import { THEME } from "../constants"; import { THEME } from "../constants";
import type { PointSnapLine, PointerSnapLine } from "../snapping"; import type { PointSnapLine, PointerSnapLine } from "../snapping";
import type { InteractiveCanvasAppState } from "../types"; import type { InteractiveCanvasAppState } from "../types";
@@ -140,27 +140,31 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
// (1) // (1)
if (!appState.zenModeEnabled) { if (!appState.zenModeEnabled) {
drawLine( drawLine(
point(from[0], from[1] - FULL), pointFrom(from[0], from[1] - FULL),
point(from[0], from[1] + FULL), pointFrom(from[0], from[1] + FULL),
context, context,
); );
} }
// (3) // (3)
drawLine( drawLine(
point(halfPoint[0] - QUARTER, halfPoint[1] - HALF), pointFrom(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
point(halfPoint[0] - QUARTER, halfPoint[1] + HALF), pointFrom(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
context, context,
); );
drawLine( drawLine(
point(halfPoint[0] + QUARTER, halfPoint[1] - HALF), pointFrom(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
point(halfPoint[0] + QUARTER, halfPoint[1] + HALF), pointFrom(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
context, context,
); );
if (!appState.zenModeEnabled) { if (!appState.zenModeEnabled) {
// (4) // (4)
drawLine(point(to[0], to[1] - FULL), point(to[0], to[1] + FULL), context); drawLine(
pointFrom(to[0], to[1] - FULL),
pointFrom(to[0], to[1] + FULL),
context,
);
// (2) // (2)
drawLine(from, to, context); drawLine(from, to, context);
@@ -170,27 +174,31 @@ const drawGapLine = <Point extends LocalPoint | GlobalPoint>(
// (1) // (1)
if (!appState.zenModeEnabled) { if (!appState.zenModeEnabled) {
drawLine( drawLine(
point(from[0] - FULL, from[1]), pointFrom(from[0] - FULL, from[1]),
point(from[0] + FULL, from[1]), pointFrom(from[0] + FULL, from[1]),
context, context,
); );
} }
// (3) // (3)
drawLine( drawLine(
point(halfPoint[0] - HALF, halfPoint[1] - QUARTER), pointFrom(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
point(halfPoint[0] + HALF, halfPoint[1] - QUARTER), pointFrom(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
context, context,
); );
drawLine( drawLine(
point(halfPoint[0] - HALF, halfPoint[1] + QUARTER), pointFrom(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
point(halfPoint[0] + HALF, halfPoint[1] + QUARTER), pointFrom(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
context, context,
); );
if (!appState.zenModeEnabled) { if (!appState.zenModeEnabled) {
// (4) // (4)
drawLine(point(to[0] - FULL, to[1]), point(to[0] + FULL, to[1]), context); drawLine(
pointFrom(to[0] - FULL, to[1]),
pointFrom(to[0] + FULL, to[1]),
context,
);
// (2) // (2)
drawLine(from, to, context); drawLine(from, to, context);
+23 -5
View File
@@ -7,7 +7,7 @@ import {
SVG_NS, SVG_NS,
} from "../constants"; } from "../constants";
import { normalizeLink, toValidURL } from "../data/url"; import { normalizeLink, toValidURL } from "../data/url";
import { getElementAbsoluteCoords } from "../element"; import { getElementAbsoluteCoords, hashString } from "../element";
import { import {
createPlaceholderEmbeddableLabel, createPlaceholderEmbeddableLabel,
getEmbedLink, getEmbedLink,
@@ -37,6 +37,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement"; import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
import { getVerticalOffset } from "../fonts"; import { getVerticalOffset } from "../fonts";
import { getCornerRadius, isPathALoop } from "../shapes"; import { getCornerRadius, isPathALoop } from "../shapes";
import { getUncroppedWidthAndHeight } from "../element/cropElement";
const roughSVGDrawWithPrecision = ( const roughSVGDrawWithPrecision = (
rsvg: RoughSVG, rsvg: RoughSVG,
@@ -410,19 +411,36 @@ const renderElementToSvg = (
const fileData = const fileData =
isInitializedImageElement(element) && files[element.fileId]; isInitializedImageElement(element) && files[element.fileId];
if (fileData) { if (fileData) {
const symbolId = `image-${fileData.id}`; const cropHash = hashString(JSON.stringify(element.crop));
const symbolId = `image-${fileData.id}-${cropHash}`;
let symbol = svgRoot.querySelector(`#${symbolId}`); let symbol = svgRoot.querySelector(`#${symbolId}`);
if (!symbol) { if (!symbol) {
symbol = svgRoot.ownerDocument!.createElementNS(SVG_NS, "symbol"); symbol = svgRoot.ownerDocument!.createElementNS(SVG_NS, "symbol");
symbol.id = symbolId; symbol.id = symbolId;
const image = svgRoot.ownerDocument!.createElementNS(SVG_NS, "image"); const image = svgRoot.ownerDocument!.createElementNS(SVG_NS, "image");
image.setAttribute("width", "100%");
image.setAttribute("height", "100%");
image.setAttribute("href", fileData.dataURL); image.setAttribute("href", fileData.dataURL);
image.setAttribute("preserveAspectRatio", "none"); image.setAttribute("preserveAspectRatio", "none");
if (element.crop) {
const { width: uncroppedWidth, height: uncroppedHeight } =
getUncroppedWidthAndHeight(element);
symbol.setAttribute(
"viewBox",
`${
element.crop.x / (element.crop.naturalWidth / uncroppedWidth)
} ${
element.crop.y / (element.crop.naturalHeight / uncroppedHeight)
} ${width} ${height}`,
);
image.setAttribute("width", `${uncroppedWidth}`);
image.setAttribute("height", `${uncroppedHeight}`);
} else {
image.setAttribute("width", "100%");
image.setAttribute("height", "100%");
}
symbol.appendChild(image); symbol.appendChild(image);
root.prepend(symbol); root.prepend(symbol);
+2 -2
View File
@@ -24,7 +24,7 @@ import {
import { canChangeRoundness } from "./comparisons"; import { canChangeRoundness } from "./comparisons";
import type { EmbedsValidationStatus } from "../types"; import type { EmbedsValidationStatus } from "../types";
import { import {
point, pointFrom,
pointDistance, pointDistance,
type GlobalPoint, type GlobalPoint,
type LocalPoint, type LocalPoint,
@@ -408,7 +408,7 @@ export const _generateElementShape = (
// initial position to it // initial position to it
const points = element.points.length const points = element.points.length
? element.points ? element.points
: [point<LocalPoint>(0, 0)]; : [pointFrom<LocalPoint>(0, 0)];
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
shape = [ shape = [
+26 -26
View File
@@ -1,6 +1,6 @@
import { import {
isPoint, isPoint,
point, pointFrom,
pointDistance, pointDistance,
pointFromPair, pointFromPair,
pointRotateRads, pointRotateRads,
@@ -167,15 +167,15 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
? getClosedCurveShape<Point>( ? getClosedCurveShape<Point>(
element, element,
roughShape, roughShape,
point<Point>(element.x, element.y), pointFrom<Point>(element.x, element.y),
element.angle, element.angle,
point(cx, cy), pointFrom(cx, cy),
) )
: getCurveShape<Point>( : getCurveShape<Point>(
roughShape, roughShape,
point<Point>(element.x, element.y), pointFrom<Point>(element.x, element.y),
element.angle, element.angle,
point(cx, cy), pointFrom(cx, cy),
); );
} }
@@ -186,7 +186,7 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
return getFreedrawShape( return getFreedrawShape(
element, element,
point(cx, cy), pointFrom(cx, cy),
shouldTestInside(element), shouldTestInside(element),
); );
} }
@@ -233,7 +233,7 @@ export const getControlPointsForBezierCurve = <
} }
const ops = getCurvePathOps(shape[0]); const ops = getCurvePathOps(shape[0]);
let currentP = point<P>(0, 0); let currentP = pointFrom<P>(0, 0);
let index = 0; let index = 0;
let minDistance = Infinity; let minDistance = Infinity;
let controlPoints: P[] | null = null; let controlPoints: P[] | null = null;
@@ -249,9 +249,9 @@ export const getControlPointsForBezierCurve = <
} }
if (op === "bcurveTo") { if (op === "bcurveTo") {
const p0 = currentP; const p0 = currentP;
const p1 = point<P>(data[0], data[1]); const p1 = pointFrom<P>(data[0], data[1]);
const p2 = point<P>(data[2], data[3]); const p2 = pointFrom<P>(data[2], data[3]);
const p3 = point<P>(data[4], data[5]); const p3 = pointFrom<P>(data[4], data[5]);
const distance = pointDistance(p3, endPoint); const distance = pointDistance(p3, endPoint);
if (distance < minDistance) { if (distance < minDistance) {
minDistance = distance; minDistance = distance;
@@ -279,7 +279,7 @@ export const getBezierXY = <P extends GlobalPoint | LocalPoint>(
p0[idx] * Math.pow(t, 3); p0[idx] * Math.pow(t, 3);
const tx = equation(t, 0); const tx = equation(t, 0);
const ty = equation(t, 1); const ty = equation(t, 1);
return point(tx, ty); return pointFrom(tx, ty);
}; };
const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>( const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
@@ -301,12 +301,12 @@ const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
controlPoints[3], controlPoints[3],
t, t,
); );
pointsOnCurve.push(point(p[0], p[1])); pointsOnCurve.push(pointFrom(p[0], p[1]));
t -= 0.05; t -= 0.05;
} }
if (pointsOnCurve.length) { if (pointsOnCurve.length) {
if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) { if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
pointsOnCurve.push(point(endPoint[0], endPoint[1])); pointsOnCurve.push(pointFrom(endPoint[0], endPoint[1]));
} }
} }
return pointsOnCurve; return pointsOnCurve;
@@ -393,24 +393,24 @@ export const aabbForElement = (
midY: element.y + element.height / 2, midY: element.y + element.height / 2,
}; };
const center = point(bbox.midX, bbox.midY); const center = pointFrom(bbox.midX, bbox.midY);
const [topLeftX, topLeftY] = pointRotateRads( const [topLeftX, topLeftY] = pointRotateRads(
point(bbox.minX, bbox.minY), pointFrom(bbox.minX, bbox.minY),
center, center,
element.angle, element.angle,
); );
const [topRightX, topRightY] = pointRotateRads( const [topRightX, topRightY] = pointRotateRads(
point(bbox.maxX, bbox.minY), pointFrom(bbox.maxX, bbox.minY),
center, center,
element.angle, element.angle,
); );
const [bottomRightX, bottomRightY] = pointRotateRads( const [bottomRightX, bottomRightY] = pointRotateRads(
point(bbox.maxX, bbox.maxY), pointFrom(bbox.maxX, bbox.maxY),
center, center,
element.angle, element.angle,
); );
const [bottomLeftX, bottomLeftY] = pointRotateRads( const [bottomLeftX, bottomLeftY] = pointRotateRads(
point(bbox.minX, bbox.maxY), pointFrom(bbox.minX, bbox.maxY),
center, center,
element.angle, element.angle,
); );
@@ -442,14 +442,14 @@ export const pointInsideBounds = <P extends GlobalPoint | LocalPoint>(
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
export const aabbsOverlapping = (a: Bounds, b: Bounds) => export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
pointInsideBounds(point(a[0], a[1]), b) || pointInsideBounds(pointFrom(a[0], a[1]), b) ||
pointInsideBounds(point(a[2], a[1]), b) || pointInsideBounds(pointFrom(a[2], a[1]), b) ||
pointInsideBounds(point(a[2], a[3]), b) || pointInsideBounds(pointFrom(a[2], a[3]), b) ||
pointInsideBounds(point(a[0], a[3]), b) || pointInsideBounds(pointFrom(a[0], a[3]), b) ||
pointInsideBounds(point(b[0], b[1]), a) || pointInsideBounds(pointFrom(b[0], b[1]), a) ||
pointInsideBounds(point(b[2], b[1]), a) || pointInsideBounds(pointFrom(b[2], b[1]), a) ||
pointInsideBounds(point(b[2], b[3]), a) || pointInsideBounds(pointFrom(b[2], b[3]), a) ||
pointInsideBounds(point(b[0], b[3]), a); pointInsideBounds(pointFrom(b[0], b[3]), a);
export const getCornerRadius = (x: number, element: ExcalidrawElement) => { export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
if ( if (
+89 -65
View File
@@ -1,6 +1,6 @@
import type { InclusiveRange } from "../math"; import type { InclusiveRange } from "../math";
import { import {
point, pointFrom,
pointRotateRads, pointRotateRads,
rangeInclusive, rangeInclusive,
rangeIntersection, rangeIntersection,
@@ -228,52 +228,52 @@ export const getElementsCorners = (
!boundingBoxCorners !boundingBoxCorners
) { ) {
const leftMid = pointRotateRads<GlobalPoint>( const leftMid = pointRotateRads<GlobalPoint>(
point(x1, y1 + halfHeight), pointFrom(x1, y1 + halfHeight),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const topMid = pointRotateRads<GlobalPoint>( const topMid = pointRotateRads<GlobalPoint>(
point(x1 + halfWidth, y1), pointFrom(x1 + halfWidth, y1),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const rightMid = pointRotateRads<GlobalPoint>( const rightMid = pointRotateRads<GlobalPoint>(
point(x2, y1 + halfHeight), pointFrom(x2, y1 + halfHeight),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const bottomMid = pointRotateRads<GlobalPoint>( const bottomMid = pointRotateRads<GlobalPoint>(
point(x1 + halfWidth, y2), pointFrom(x1 + halfWidth, y2),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const center = point<GlobalPoint>(cx, cy); const center = pointFrom<GlobalPoint>(cx, cy);
result = omitCenter result = omitCenter
? [leftMid, topMid, rightMid, bottomMid] ? [leftMid, topMid, rightMid, bottomMid]
: [leftMid, topMid, rightMid, bottomMid, center]; : [leftMid, topMid, rightMid, bottomMid, center];
} else { } else {
const topLeft = pointRotateRads<GlobalPoint>( const topLeft = pointRotateRads<GlobalPoint>(
point(x1, y1), pointFrom(x1, y1),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const topRight = pointRotateRads<GlobalPoint>( const topRight = pointRotateRads<GlobalPoint>(
point(x2, y1), pointFrom(x2, y1),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const bottomLeft = pointRotateRads<GlobalPoint>( const bottomLeft = pointRotateRads<GlobalPoint>(
point(x1, y2), pointFrom(x1, y2),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const bottomRight = pointRotateRads<GlobalPoint>( const bottomRight = pointRotateRads<GlobalPoint>(
point(x2, y2), pointFrom(x2, y2),
point(cx, cy), pointFrom(cx, cy),
element.angle, element.angle,
); );
const center = point<GlobalPoint>(cx, cy); const center = pointFrom<GlobalPoint>(cx, cy);
result = omitCenter result = omitCenter
? [topLeft, topRight, bottomLeft, bottomRight] ? [topLeft, topRight, bottomLeft, bottomRight]
@@ -287,18 +287,18 @@ export const getElementsCorners = (
const width = maxX - minX; const width = maxX - minX;
const height = maxY - minY; const height = maxY - minY;
const topLeft = point<GlobalPoint>(minX, minY); const topLeft = pointFrom<GlobalPoint>(minX, minY);
const topRight = point<GlobalPoint>(maxX, minY); const topRight = pointFrom<GlobalPoint>(maxX, minY);
const bottomLeft = point<GlobalPoint>(minX, maxY); const bottomLeft = pointFrom<GlobalPoint>(minX, maxY);
const bottomRight = point<GlobalPoint>(maxX, maxY); const bottomRight = pointFrom<GlobalPoint>(maxX, maxY);
const center = point<GlobalPoint>(minX + width / 2, minY + height / 2); const center = pointFrom<GlobalPoint>(minX + width / 2, minY + height / 2);
result = omitCenter result = omitCenter
? [topLeft, topRight, bottomLeft, bottomRight] ? [topLeft, topRight, bottomLeft, bottomRight]
: [topLeft, topRight, bottomLeft, bottomRight, center]; : [topLeft, topRight, bottomLeft, bottomRight, center];
} }
return result.map((p) => point(round(p[0]), round(p[1]))); return result.map((p) => pointFrom(round(p[0]), round(p[1])));
}; };
const getReferenceElements = ( const getReferenceElements = (
@@ -375,8 +375,11 @@ export const getVisibleGaps = (
horizontalGaps.push({ horizontalGaps.push({
startBounds, startBounds,
endBounds, endBounds,
startSide: [point(startMaxX, startMinY), point(startMaxX, startMaxY)], startSide: [
endSide: [point(endMinX, endMinY), point(endMinX, endMaxY)], pointFrom(startMaxX, startMinY),
pointFrom(startMaxX, startMaxY),
],
endSide: [pointFrom(endMinX, endMinY), pointFrom(endMinX, endMaxY)],
length: endMinX - startMaxX, length: endMinX - startMaxX,
overlap: rangeIntersection( overlap: rangeIntersection(
rangeInclusive(startMinY, startMaxY), rangeInclusive(startMinY, startMaxY),
@@ -415,8 +418,11 @@ export const getVisibleGaps = (
verticalGaps.push({ verticalGaps.push({
startBounds, startBounds,
endBounds, endBounds,
startSide: [point(startMinX, startMaxY), point(startMaxX, startMaxY)], startSide: [
endSide: [point(endMinX, endMinY), point(endMaxX, endMinY)], pointFrom(startMinX, startMaxY),
pointFrom(startMaxX, startMaxY),
],
endSide: [pointFrom(endMinX, endMinY), pointFrom(endMaxX, endMinY)],
length: endMinY - startMaxY, length: endMinY - startMaxY,
overlap: rangeIntersection( overlap: rangeIntersection(
rangeInclusive(startMinX, startMaxX), rangeInclusive(startMinX, startMaxX),
@@ -832,7 +838,7 @@ const createPointSnapLines = (
} }
snapsX[key].push( snapsX[key].push(
...snap.points.map((p) => ...snap.points.map((p) =>
point<GlobalPoint>(round(p[0]), round(p[1])), pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
), ),
); );
} }
@@ -849,7 +855,7 @@ const createPointSnapLines = (
} }
snapsY[key].push( snapsY[key].push(
...snap.points.map((p) => ...snap.points.map((p) =>
point<GlobalPoint>(round(p[0]), round(p[1])), pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
), ),
); );
} }
@@ -863,7 +869,7 @@ const createPointSnapLines = (
points: dedupePoints( points: dedupePoints(
points points
.map((p) => { .map((p) => {
return point<GlobalPoint>(Number(key), p[1]); return pointFrom<GlobalPoint>(Number(key), p[1]);
}) })
.sort((a, b) => a[1] - b[1]), .sort((a, b) => a[1] - b[1]),
), ),
@@ -876,7 +882,7 @@ const createPointSnapLines = (
points: dedupePoints( points: dedupePoints(
points points
.map((p) => { .map((p) => {
return point<GlobalPoint>(p[0], Number(key)); return pointFrom<GlobalPoint>(p[0], Number(key));
}) })
.sort((a, b) => a[0] - b[0]), .sort((a, b) => a[0] - b[0]),
), ),
@@ -940,16 +946,16 @@ const createGapSnapLines = (
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [ points: [
point(gapSnap.gap.startSide[0][0], gapLineY), pointFrom(gapSnap.gap.startSide[0][0], gapLineY),
point(minX, gapLineY), pointFrom(minX, gapLineY),
], ],
}, },
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [ points: [
point(maxX, gapLineY), pointFrom(maxX, gapLineY),
point(gapSnap.gap.endSide[0][0], gapLineY), pointFrom(gapSnap.gap.endSide[0][0], gapLineY),
], ],
}, },
); );
@@ -966,16 +972,16 @@ const createGapSnapLines = (
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [ points: [
point(gapLineX, gapSnap.gap.startSide[0][1]), pointFrom(gapLineX, gapSnap.gap.startSide[0][1]),
point(gapLineX, minY), pointFrom(gapLineX, minY),
], ],
}, },
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [ points: [
point(gapLineX, maxY), pointFrom(gapLineX, maxY),
point(gapLineX, gapSnap.gap.endSide[0][1]), pointFrom(gapLineX, gapSnap.gap.endSide[0][1]),
], ],
}, },
); );
@@ -991,12 +997,15 @@ const createGapSnapLines = (
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)], points: [
pointFrom(startMaxX, gapLineY),
pointFrom(endMinX, gapLineY),
],
}, },
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [point(endMaxX, gapLineY), point(minX, gapLineY)], points: [pointFrom(endMaxX, gapLineY), pointFrom(minX, gapLineY)],
}, },
); );
} }
@@ -1011,12 +1020,18 @@ const createGapSnapLines = (
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [point(maxX, gapLineY), point(startMinX, gapLineY)], points: [
pointFrom(maxX, gapLineY),
pointFrom(startMinX, gapLineY),
],
}, },
{ {
type: "gap", type: "gap",
direction: "horizontal", direction: "horizontal",
points: [point(startMaxX, gapLineY), point(endMinX, gapLineY)], points: [
pointFrom(startMaxX, gapLineY),
pointFrom(endMinX, gapLineY),
],
}, },
); );
} }
@@ -1031,12 +1046,18 @@ const createGapSnapLines = (
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [point(gapLineX, maxY), point(gapLineX, startMinY)], points: [
pointFrom(gapLineX, maxY),
pointFrom(gapLineX, startMinY),
],
}, },
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)], points: [
pointFrom(gapLineX, startMaxY),
pointFrom(gapLineX, endMinY),
],
}, },
); );
} }
@@ -1051,12 +1072,15 @@ const createGapSnapLines = (
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [point(gapLineX, startMaxY), point(gapLineX, endMinY)], points: [
pointFrom(gapLineX, startMaxY),
pointFrom(gapLineX, endMinY),
],
}, },
{ {
type: "gap", type: "gap",
direction: "vertical", direction: "vertical",
points: [point(gapLineX, endMaxY), point(gapLineX, minY)], points: [pointFrom(gapLineX, endMaxY), pointFrom(gapLineX, minY)],
}, },
); );
} }
@@ -1070,7 +1094,7 @@ const createGapSnapLines = (
return { return {
...gapSnapLine, ...gapSnapLine,
points: gapSnapLine.points.map((p) => points: gapSnapLine.points.map((p) =>
point(round(p[0]), round(p[1])), pointFrom(round(p[0]), round(p[1])),
) as PointPair, ) as PointPair,
}; };
}), }),
@@ -1120,35 +1144,35 @@ export const snapResizingElements = (
if (transformHandle) { if (transformHandle) {
switch (transformHandle) { switch (transformHandle) {
case "e": { case "e": {
selectionSnapPoints.push(point(maxX, minY), point(maxX, maxY)); selectionSnapPoints.push(pointFrom(maxX, minY), pointFrom(maxX, maxY));
break; break;
} }
case "w": { case "w": {
selectionSnapPoints.push(point(minX, minY), point(minX, maxY)); selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(minX, maxY));
break; break;
} }
case "n": { case "n": {
selectionSnapPoints.push(point(minX, minY), point(maxX, minY)); selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(maxX, minY));
break; break;
} }
case "s": { case "s": {
selectionSnapPoints.push(point(minX, maxY), point(maxX, maxY)); selectionSnapPoints.push(pointFrom(minX, maxY), pointFrom(maxX, maxY));
break; break;
} }
case "ne": { case "ne": {
selectionSnapPoints.push(point(maxX, minY)); selectionSnapPoints.push(pointFrom(maxX, minY));
break; break;
} }
case "nw": { case "nw": {
selectionSnapPoints.push(point(minX, minY)); selectionSnapPoints.push(pointFrom(minX, minY));
break; break;
} }
case "se": { case "se": {
selectionSnapPoints.push(point(maxX, maxY)); selectionSnapPoints.push(pointFrom(maxX, maxY));
break; break;
} }
case "sw": { case "sw": {
selectionSnapPoints.push(point(minX, maxY)); selectionSnapPoints.push(pointFrom(minX, maxY));
break; break;
} }
} }
@@ -1191,10 +1215,10 @@ export const snapResizingElements = (
); );
const corners: GlobalPoint[] = [ const corners: GlobalPoint[] = [
point(x1, y1), pointFrom(x1, y1),
point(x1, y2), pointFrom(x1, y2),
point(x2, y1), pointFrom(x2, y1),
point(x2, y2), pointFrom(x2, y2),
]; ];
getPointSnaps( getPointSnaps(
@@ -1231,7 +1255,7 @@ export const snapNewElement = (
} }
const selectionSnapPoints: GlobalPoint[] = [ const selectionSnapPoints: GlobalPoint[] = [
point(origin.x + dragOffset.x, origin.y + dragOffset.y), pointFrom(origin.x + dragOffset.x, origin.y + dragOffset.y),
]; ];
const snapDistance = getSnapDistance(app.state.zoom.value); const snapDistance = getSnapDistance(app.state.zoom.value);
@@ -1331,7 +1355,7 @@ export const getSnapLinesAtPointer = (
verticalSnapLines.push({ verticalSnapLines.push({
type: "pointer", type: "pointer",
points: [corner, point(corner[0], pointer.y)], points: [corner, pointFrom(corner[0], pointer.y)],
direction: "vertical", direction: "vertical",
}); });
@@ -1347,7 +1371,7 @@ export const getSnapLinesAtPointer = (
horizontalSnapLines.push({ horizontalSnapLines.push({
type: "pointer", type: "pointer",
points: [corner, point(pointer.x, corner[1])], points: [corner, pointFrom(pointer.x, corner[1])],
direction: "horizontal", direction: "horizontal",
}); });
+1
View File
@@ -21,6 +21,7 @@ export const getObservedAppState = (appState: AppState): ObservedAppState => {
selectedGroupIds: appState.selectedGroupIds, selectedGroupIds: appState.selectedGroupIds,
editingLinearElementId: appState.editingLinearElement?.elementId || null, editingLinearElementId: appState.editingLinearElement?.elementId || null,
selectedLinearElementId: appState.selectedLinearElement?.elementId || null, selectedLinearElementId: appState.selectedLinearElement?.elementId || null,
croppingElementId: appState.croppingElementId,
}; };
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, { Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {
@@ -116,6 +116,50 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@@ -794,6 +838,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"left": 30, "left": 30,
"top": 40, "top": 40,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -836,6 +881,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1000,6 +1046,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1042,6 +1089,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1216,6 +1264,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1258,6 +1307,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1547,6 +1597,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1589,6 +1640,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1878,6 +1930,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1920,6 +1973,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2094,6 +2148,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2136,6 +2191,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2334,6 +2390,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2376,6 +2433,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2635,6 +2693,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2677,6 +2736,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3004,6 +3064,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3046,6 +3107,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3479,6 +3541,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3521,6 +3584,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3802,6 +3866,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3844,6 +3909,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4125,6 +4191,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4167,6 +4234,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4633,6 +4701,50 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@@ -5311,6 +5423,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"left": -17, "left": -17,
"top": -7, "top": -7,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -5353,6 +5466,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -5760,6 +5874,50 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@@ -6438,6 +6596,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"left": -17, "left": -17,
"top": -7, "top": -7,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -6480,6 +6639,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7373,6 +7533,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"left": -19, "left": -19,
"top": -9, "top": -9,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -7415,6 +7576,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7607,6 +7769,50 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@@ -8285,6 +8491,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"left": -17, "left": -17,
"top": -7, "top": -7,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8327,6 +8534,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8501,6 +8709,50 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@@ -9179,6 +9431,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"left": 80, "left": 80,
"top": 90, "top": 90,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9221,6 +9474,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
File diff suppressed because one or more lines are too long
@@ -11,6 +11,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -53,6 +54,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -613,6 +615,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -655,6 +658,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1119,6 +1123,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1161,6 +1166,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1487,6 +1493,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1529,6 +1536,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1856,6 +1864,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1898,6 +1907,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2123,6 +2133,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2165,6 +2176,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2563,6 +2575,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2605,6 +2618,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2862,6 +2876,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2904,6 +2919,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3146,6 +3162,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3188,6 +3205,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3440,6 +3458,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3482,6 +3501,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3726,6 +3746,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3768,6 +3789,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3961,6 +3983,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4003,6 +4026,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4220,6 +4244,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4262,6 +4287,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4493,6 +4519,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4535,6 +4562,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4724,6 +4752,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4766,6 +4795,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4955,6 +4985,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4997,6 +5028,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -5184,6 +5216,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -5226,6 +5259,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -5413,6 +5447,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -5455,6 +5490,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -5672,6 +5708,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -5714,6 +5751,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -6003,6 +6041,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -6045,6 +6084,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -6428,6 +6468,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -6470,6 +6511,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -6806,6 +6848,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -6848,6 +6891,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7125,6 +7169,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -7167,6 +7212,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7423,6 +7469,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -7465,6 +7512,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7652,6 +7700,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -7694,6 +7743,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8007,6 +8057,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8049,6 +8100,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8362,6 +8414,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8404,6 +8457,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8766,6 +8820,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8808,6 +8863,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9053,6 +9109,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9095,6 +9152,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9318,6 +9376,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9360,6 +9419,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9582,6 +9642,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9624,6 +9685,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9813,6 +9875,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9855,6 +9918,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -10114,6 +10178,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -10156,6 +10221,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -10454,6 +10520,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -10496,6 +10563,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -10689,6 +10757,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -10731,6 +10800,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11142,6 +11212,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -11184,6 +11255,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11396,6 +11468,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -11438,6 +11511,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11635,6 +11709,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -11677,6 +11752,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11876,6 +11952,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -11918,6 +11995,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -12277,6 +12355,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -12319,6 +12398,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -12524,6 +12604,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -12566,6 +12647,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -12765,6 +12847,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -12807,6 +12890,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -13006,6 +13090,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -13048,6 +13133,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -13253,6 +13339,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -13295,6 +13382,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -13585,6 +13673,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -13627,6 +13716,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -13757,6 +13847,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -13799,6 +13890,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -14045,6 +14137,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -14087,6 +14180,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -14312,6 +14406,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -14354,6 +14449,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -14587,6 +14683,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -14629,6 +14726,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -14748,6 +14846,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -14790,6 +14889,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -15444,6 +15544,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -15486,6 +15587,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -16064,6 +16166,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -16106,6 +16209,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -16684,6 +16788,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -16726,6 +16831,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -17396,6 +17502,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -17438,6 +17545,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -18146,6 +18254,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -18188,6 +18297,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -18620,6 +18730,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -18662,6 +18773,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -19142,6 +19254,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -19184,6 +19297,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -19598,6 +19712,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -19640,6 +19755,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11,6 +11,7 @@ exports[`given element A and group of elements B and given both are selected whe
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -53,6 +54,7 @@ exports[`given element A and group of elements B and given both are selected whe
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -423,6 +425,7 @@ exports[`given element A and group of elements B and given both are selected whe
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -465,6 +468,7 @@ exports[`given element A and group of elements B and given both are selected whe
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -826,6 +830,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -868,6 +873,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": false, "isBindingEnabled": false,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1368,6 +1374,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1410,6 +1417,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1569,6 +1577,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1611,6 +1620,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -1941,6 +1951,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -1983,6 +1994,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2178,6 +2190,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2220,6 +2233,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2355,6 +2369,7 @@ exports[`regression tests > can drag element that covers another element, while
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2397,6 +2412,7 @@ exports[`regression tests > can drag element that covers another element, while
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2672,6 +2688,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2714,6 +2731,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -2915,6 +2933,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -2957,6 +2976,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3155,6 +3175,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3197,6 +3218,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3382,6 +3404,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3424,6 +3447,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3635,6 +3659,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3677,6 +3702,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -3943,6 +3969,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -3985,6 +4012,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4354,6 +4382,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4396,6 +4425,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4634,6 +4664,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4676,6 +4707,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -4884,6 +4916,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -4926,6 +4959,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -5091,6 +5125,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -5133,6 +5168,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -5287,6 +5323,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -5329,6 +5366,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -5666,6 +5704,7 @@ exports[`regression tests > drags selected elements from point inside common bou
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -5708,6 +5747,7 @@ exports[`regression tests > drags selected elements from point inside common bou
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -5953,6 +5993,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -5995,6 +6036,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -6758,6 +6800,7 @@ exports[`regression tests > given a group of selected elements with an element t
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -6800,6 +6843,7 @@ exports[`regression tests > given a group of selected elements with an element t
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7085,6 +7129,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -7127,6 +7172,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7358,6 +7404,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -7400,6 +7447,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7589,6 +7637,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -7631,6 +7680,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -7823,6 +7873,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -7865,6 +7916,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8000,6 +8052,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8042,6 +8095,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8177,6 +8231,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8219,6 +8274,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8354,6 +8410,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8396,6 +8453,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8574,6 +8632,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8616,6 +8675,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8793,6 +8853,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -8835,6 +8896,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -8984,6 +9046,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9026,6 +9089,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9204,6 +9268,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9246,6 +9311,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9381,6 +9447,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9423,6 +9490,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9600,6 +9668,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9642,6 +9711,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9777,6 +9847,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -9819,6 +9890,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -9968,6 +10040,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -10010,6 +10083,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -10145,6 +10219,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -10187,6 +10262,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -10656,6 +10732,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -10698,6 +10775,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -10930,6 +11008,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -10972,6 +11051,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11053,6 +11133,7 @@ exports[`regression tests > shift click on selected element should deselect it o
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -11095,6 +11176,7 @@ exports[`regression tests > shift click on selected element should deselect it o
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11249,6 +11331,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -11291,6 +11374,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11557,6 +11641,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -11599,6 +11684,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -11966,6 +12052,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -12008,6 +12095,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -12576,6 +12664,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -12618,6 +12707,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -12702,6 +12792,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -12744,6 +12835,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -13283,6 +13375,7 @@ exports[`regression tests > switches from group of selected elements to another
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -13325,6 +13418,7 @@ exports[`regression tests > switches from group of selected elements to another
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -13618,6 +13712,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -13660,6 +13755,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -13880,6 +13976,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -13922,6 +14019,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -14003,6 +14101,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -14045,6 +14144,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -14379,6 +14479,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -14421,6 +14522,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@@ -14502,6 +14604,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -14544,6 +14647,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
+9 -4
View File
@@ -7,7 +7,7 @@ import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { point } from "../../math"; import { pointFrom } from "../../math";
const { h } = window; const { h } = window;
@@ -32,7 +32,12 @@ describe("element binding", () => {
y: 0, y: 0,
width: 100, width: 100,
height: 1, height: 1,
points: [point(0, 0), point(0, 0), point(100, 0), point(100, 0)], points: [
pointFrom(0, 0),
pointFrom(0, 0),
pointFrom(100, 0),
pointFrom(100, 0),
],
}); });
API.setElements([rect, arrow]); API.setElements([rect, arrow]);
expect(arrow.startBinding).toBe(null); expect(arrow.startBinding).toBe(null);
@@ -310,7 +315,7 @@ describe("element binding", () => {
const arrow1 = API.createElement({ const arrow1 = API.createElement({
type: "arrow", type: "arrow",
id: "arrow1", id: "arrow1",
points: [point(0, 0), point(0, -87.45777932247563)], points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
startBinding: { startBinding: {
elementId: "rectangle1", elementId: "rectangle1",
focus: 0.2, focus: 0.2,
@@ -328,7 +333,7 @@ describe("element binding", () => {
const arrow2 = API.createElement({ const arrow2 = API.createElement({
type: "arrow", type: "arrow",
id: "arrow2", id: "arrow2",
points: [point(0, 0), point(0, -87.45777932247563)], points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
startBinding: { startBinding: {
elementId: "text1", elementId: "text1",
focus: 0.2, focus: 0.2,
@@ -0,0 +1,342 @@
import React from "react";
import ReactDOM from "react-dom";
import { vi } from "vitest";
import { Keyboard, Pointer, UI } from "./helpers/ui";
import type { ExcalidrawImageElement, ImageCrop } from "../element/types";
import { act, GlobalTestState, render } from "./test-utils";
import { Excalidraw, exportToCanvas, exportToSvg } from "..";
import { API } from "./helpers/api";
import type { NormalizedZoomValue } from "../types";
import { KEYS } from "../keys";
import { duplicateElement } from "../element";
import { cloneJSON } from "../utils";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
const { h } = window;
const mouse = new Pointer("mouse");
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
mouse.reset();
localStorage.clear();
sessionStorage.clear();
vi.clearAllMocks();
Object.assign(document, {
elementFromPoint: () => GlobalTestState.canvas,
});
await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />);
API.setAppState({
zoom: {
value: 1 as NormalizedZoomValue,
},
});
const image = API.createElement({ type: "image", width: 200, height: 100 });
API.setElements([image]);
API.setAppState({
selectedElementIds: {
[image.id]: true,
},
});
});
const generateRandomNaturalWidthAndHeight = (image: ExcalidrawImageElement) => {
const initialWidth = image.width;
const initialHeight = image.height;
const scale = 1 + Math.random() * 5;
return {
naturalWidth: initialWidth * scale,
naturalHeight: initialHeight * scale,
};
};
const compareCrops = (cropA: ImageCrop, cropB: ImageCrop) => {
(Object.keys(cropA) as [keyof ImageCrop]).forEach((key) => {
const propA = cropA[key];
const propB = cropB[key];
expect(propA as number).toBeCloseTo(propB as number);
});
};
describe("Enter and leave the crop editor", () => {
it("enter the editor by double clicking", () => {
const image = h.elements[0];
expect(h.state.croppingElementId).toBe(null);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
expect(h.state.croppingElementId).toBe(image.id);
});
it("enter the editor by pressing enter", () => {
const image = h.elements[0];
expect(h.state.croppingElementId).toBe(null);
Keyboard.keyDown(KEYS.ENTER);
expect(h.state.croppingElementId).not.toBe(null);
expect(h.state.croppingElementId).toBe(image.id);
});
it("leave the editor by clicking outside", () => {
const image = h.elements[0];
Keyboard.keyDown(KEYS.ENTER);
expect(h.state.croppingElementId).not.toBe(null);
mouse.click(image.x - 20, image.y - 20);
expect(h.state.croppingElementId).toBe(null);
});
it("leave the editor by pressing escape", () => {
const image = h.elements[0];
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
Keyboard.keyDown(KEYS.ESCAPE);
expect(h.state.croppingElementId).toBe(null);
});
});
describe("Crop an image", () => {
it("Cropping changes the dimension", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth / 2, 0]);
expect(image.width).toBeLessThan(initialWidth);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, initialHeight / 2]);
expect(image.height).toBeLessThan(initialHeight);
});
it("Cropping has minimal sizes", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth, 0]);
expect(image.width).toBeLessThan(initialWidth);
expect(image.width).toBeGreaterThan(0);
UI.crop(image, "w", naturalWidth, naturalHeight, [-initialWidth, 0]);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, initialHeight]);
expect(image.height).toBeLessThan(initialHeight);
expect(image.height).toBeGreaterThan(0);
});
it("Preserve aspect ratio", async () => {
let image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth / 3, 0]);
let resizedWidth = image.width;
let resizedHeight = image.height;
// max height, cropping should not change anything
UI.crop(
image,
"w",
naturalWidth,
naturalHeight,
[-initialWidth / 3, 0],
true,
);
expect(image.width).toBe(resizedWidth);
expect(image.height).toBe(resizedHeight);
// re-crop to initial state
UI.crop(image, "w", naturalWidth, naturalHeight, [-initialWidth / 3, 0]);
// change crop height and width
UI.crop(image, "s", naturalWidth, naturalHeight, [0, -initialHeight / 2]);
UI.crop(image, "e", naturalWidth, naturalHeight, [-initialWidth / 3, 0]);
resizedWidth = image.width;
resizedHeight = image.height;
// test corner handle aspect ratio preserving
UI.crop(image, "se", naturalWidth, naturalHeight, [initialWidth, 0], true);
expect(image.width / image.height).toBe(resizedWidth / resizedHeight);
expect(image.width).toBeLessThanOrEqual(initialWidth);
expect(image.height).toBeLessThanOrEqual(initialHeight);
// reset
image = API.createElement({ type: "image", width: 200, height: 100 });
API.setElements([image]);
API.setAppState({
selectedElementIds: {
[image.id]: true,
},
});
// 50 x 50 square
UI.crop(image, "nw", naturalWidth, naturalHeight, [150, 50]);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, -100], true);
expect(image.width).toBeCloseTo(image.height);
// image is at the corner, not space to its right to expand, should not be able to resize
expect(image.height).toBeCloseTo(50);
UI.crop(image, "nw", naturalWidth, naturalHeight, [-150, -100], true);
expect(image.width).toBeCloseTo(image.height);
// max height should be reached
expect(image.height).toBeCloseTo(initialHeight);
expect(image.width).toBe(initialHeight);
});
});
describe("Cropping and other features", async () => {
it("Cropping works independently of duplication", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const duplicatedImage = duplicateElement(null, new Map(), image, {});
act(() => {
h.app.scene.insertElement(duplicatedImage);
});
expect(duplicatedImage.width).toBe(image.width);
expect(duplicatedImage.height).toBe(image.height);
UI.crop(duplicatedImage, "nw", naturalWidth, naturalHeight, [
-initialWidth / 2,
-initialHeight / 2,
]);
expect(duplicatedImage.width).toBe(initialWidth);
expect(duplicatedImage.height).toBe(initialHeight);
const resizedWidth = image.width;
const resizedHeight = image.height;
expect(image.width).not.toBe(duplicatedImage.width);
expect(image.height).not.toBe(duplicatedImage.height);
UI.crop(duplicatedImage, "se", naturalWidth, naturalHeight, [
-initialWidth / 1.5,
-initialHeight / 1.5,
]);
expect(duplicatedImage.width).not.toBe(initialWidth);
expect(image.width).toBe(resizedWidth);
expect(duplicatedImage.height).not.toBe(initialHeight);
expect(image.height).toBe(resizedHeight);
});
it("Resizing should not affect crop", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
const cropBeforeResizing = image.crop;
const cropBeforeResizingCloned = cloneJSON(image.crop) as ImageCrop;
expect(cropBeforeResizing).not.toBe(null);
UI.crop(image, "e", naturalWidth, naturalHeight, [200, 0]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
UI.resize(image, "s", [0, -100]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
UI.resize(image, "ne", [-50, -50]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
});
it("Flipping does not change crop", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const cropBeforeResizing = image.crop;
const cropBeforeResizingCloned = cloneJSON(image.crop) as ImageCrop;
API.executeAction(actionFlipHorizontal);
expect(image.crop).toBe(cropBeforeResizing);
compareCrops(cropBeforeResizingCloned, image.crop!);
API.executeAction(actionFlipVertical);
expect(image.crop).toBe(cropBeforeResizing);
compareCrops(cropBeforeResizingCloned, image.crop!);
});
it("Exports should preserve crops", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 4,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const widthToHeightRatio = image.width / image.height;
const canvas = await exportToCanvas({
elements: [image],
appState: h.state,
files: h.app.files,
exportPadding: 0,
});
const exportedCanvasRatio = canvas.width / canvas.height;
expect(widthToHeightRatio).toBeCloseTo(exportedCanvasRatio);
const svg = await exportToSvg({
elements: [image],
appState: h.state,
files: h.app.files,
exportPadding: 0,
});
const svgWidth = svg.getAttribute("width");
const svgHeight = svg.getAttribute("height");
expect(svgWidth).toBeDefined();
expect(svgHeight).toBeDefined();
const exportedSvgRatio = Number(svgWidth) / Number(svgHeight);
expect(widthToHeightRatio).toBeCloseTo(exportedSvgRatio);
});
});
+4 -4
View File
@@ -28,7 +28,7 @@ import { getBoundTextElementPosition } from "../element/textElement";
import { createPasteEvent } from "../clipboard"; import { createPasteEvent } from "../clipboard";
import { arrayToMap, cloneJSON } from "../utils"; import { arrayToMap, cloneJSON } from "../utils";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { point, type Radians } from "../../math"; import { pointFrom, type Radians } from "../../math";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@@ -146,9 +146,9 @@ const createLinearElementWithCurveInsideMinMaxPoints = (
link: null, link: null,
locked: false, locked: false,
points: [ points: [
point<LocalPoint>(0, 0), pointFrom<LocalPoint>(0, 0),
point<LocalPoint>(-922.4761962890625, 300.3277587890625), pointFrom<LocalPoint>(-922.4761962890625, 300.3277587890625),
point<LocalPoint>(828.0126953125, 410.51605224609375), pointFrom<LocalPoint>(828.0126953125, 410.51605224609375),
], ],
}); });
}; };
+5 -5
View File
@@ -38,7 +38,7 @@ import type App from "../../components/App";
import { createTestHook } from "../../components/App"; import { createTestHook } from "../../components/App";
import type { Action } from "../../actions/types"; import type { Action } from "../../actions/types";
import { mutateElement } from "../../element/mutateElement"; import { mutateElement } from "../../element/mutateElement";
import { point, type LocalPoint, type Radians } from "../../../math"; import { pointFrom, type LocalPoint, type Radians } from "../../../math";
const readFile = util.promisify(fs.readFile); const readFile = util.promisify(fs.readFile);
// so that window.h is available when App.tsx is not imported as well. // so that window.h is available when App.tsx is not imported as well.
@@ -307,8 +307,8 @@ export class API {
height, height,
type, type,
points: rest.points ?? [ points: rest.points ?? [
point<LocalPoint>(0, 0), pointFrom<LocalPoint>(0, 0),
point<LocalPoint>(100, 100), pointFrom<LocalPoint>(100, 100),
], ],
elbowed: rest.elbowed ?? false, elbowed: rest.elbowed ?? false,
}); });
@@ -320,8 +320,8 @@ export class API {
height, height,
type, type,
points: rest.points ?? [ points: rest.points ?? [
point<LocalPoint>(0, 0), pointFrom<LocalPoint>(0, 0),
point<LocalPoint>(100, 100), pointFrom<LocalPoint>(100, 100),
], ],
}); });
break; break;
+44 -7
View File
@@ -1,4 +1,3 @@
import type { ToolType } from "../../types";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
@@ -9,6 +8,7 @@ import type {
ExcalidrawDiamondElement, ExcalidrawDiamondElement,
ExcalidrawTextContainer, ExcalidrawTextContainer,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
} from "../../element/types"; } from "../../element/types";
import type { TransformHandleType } from "../../element/transformHandles"; import type { TransformHandleType } from "../../element/transformHandles";
import { import {
@@ -34,7 +34,9 @@ import { getTextEditor } from "../queries/dom";
import { arrayToMap } from "../../utils"; import { arrayToMap } from "../../utils";
import { createTestHook } from "../../components/App"; import { createTestHook } from "../../components/App";
import type { GlobalPoint, LocalPoint, Radians } from "../../../math"; import type { GlobalPoint, LocalPoint, Radians } from "../../../math";
import { point, pointRotateRads } from "../../../math"; import { pointFrom, pointRotateRads } from "../../../math";
import { cropElement } from "../../element/cropElement";
import type { ToolType } from "../../types";
// so that window.h is available when App.tsx is not imported as well. // so that window.h is available when App.tsx is not imported as well.
createTestHook(); createTestHook();
@@ -142,7 +144,7 @@ const getElementPointForSelection = (
element: ExcalidrawElement, element: ExcalidrawElement,
): GlobalPoint => { ): GlobalPoint => {
const { x, y, width, height, angle } = element; const { x, y, width, height, angle } = element;
const target = point<GlobalPoint>( const target = pointFrom<GlobalPoint>(
x + x +
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2), (isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
y, y,
@@ -151,9 +153,12 @@ const getElementPointForSelection = (
if (isLinearElement(element)) { if (isLinearElement(element)) {
const bounds = getElementPointsCoords(element, element.points); const bounds = getElementPointsCoords(element, element.points);
center = point((bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2); center = pointFrom(
(bounds[0] + bounds[2]) / 2,
(bounds[1] + bounds[3]) / 2,
);
} else { } else {
center = point(x + width / 2, y + height / 2); center = pointFrom(x + width / 2, y + height / 2);
} }
if (isTextElement(element)) { if (isTextElement(element)) {
@@ -469,8 +474,8 @@ export class UI {
const width = initialWidth ?? initialHeight ?? size; const width = initialWidth ?? initialHeight ?? size;
const height = initialHeight ?? size; const height = initialHeight ?? size;
const points: LocalPoint[] = initialPoints ?? [ const points: LocalPoint[] = initialPoints ?? [
point(0, 0), pointFrom(0, 0),
point(width, height), pointFrom(width, height),
]; ];
UI.clickTool(type); UI.clickTool(type);
@@ -558,6 +563,38 @@ export class UI {
return transform(element, handle, mouseMove, keyboardModifiers); return transform(element, handle, mouseMove, keyboardModifiers);
} }
static crop(
element: ExcalidrawImageElement,
handle: TransformHandleDirection,
naturalWidth: number,
naturalHeight: number,
mouseMove: [deltaX: number, deltaY: number],
keepAspectRatio = false,
) {
const handleCoords = getTransformHandles(
element,
h.state.zoom,
arrayToMap(h.elements),
"mouse",
{},
)[handle]!;
const clientX = handleCoords[0] + handleCoords[2] / 2;
const clientY = handleCoords[1] + handleCoords[3] / 2;
const mutations = cropElement(
element,
handle,
naturalWidth,
naturalHeight,
clientX + mouseMove[0],
clientY + mouseMove[1],
keepAspectRatio ? element.width / element.height : undefined,
);
API.updateElement(element, mutations);
}
static rotate( static rotate(
element: ExcalidrawElement | ExcalidrawElement[], element: ExcalidrawElement | ExcalidrawElement[],
mouseMove: [deltaX: number, deltaY: number], mouseMove: [deltaX: number, deltaY: number],
+9 -9
View File
@@ -46,7 +46,7 @@ import { HistoryEntry } from "../history";
import { AppStateChange, ElementsChange } from "../change"; import { AppStateChange, ElementsChange } from "../change";
import { Snapshot, StoreAction } from "../store"; import { Snapshot, StoreAction } from "../store";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { point } from "../../math"; import { pointFrom } from "../../math";
const { h } = window; const { h } = window;
@@ -2041,9 +2041,9 @@ describe("history", () => {
width: 178.9000000000001, width: 178.9000000000001,
height: 236.10000000000002, height: 236.10000000000002,
points: [ points: [
point(0, 0), pointFrom(0, 0),
point(178.9000000000001, 0), pointFrom(178.9000000000001, 0),
point(178.9000000000001, 236.10000000000002), pointFrom(178.9000000000001, 236.10000000000002),
], ],
startBinding: { startBinding: {
elementId: "KPrBI4g_v9qUB1XxYLgSz", elementId: "KPrBI4g_v9qUB1XxYLgSz",
@@ -2159,11 +2159,11 @@ describe("history", () => {
elements: [ elements: [
newElementWith(h.elements[0] as ExcalidrawLinearElement, { newElementWith(h.elements[0] as ExcalidrawLinearElement, {
points: [ points: [
point(0, 0), pointFrom(0, 0),
point(5, 5), pointFrom(5, 5),
point(10, 10), pointFrom(10, 10),
point(15, 15), pointFrom(15, 15),
point(20, 20), pointFrom(20, 20),
] as LocalPoint[], ] as LocalPoint[],
}), }),
], ],
@@ -28,7 +28,7 @@ import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
import { vi } from "vitest"; import { vi } from "vitest";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import type { GlobalPoint } from "../../math"; import type { GlobalPoint } from "../../math";
import { pointCenter, point } from "../../math"; import { pointCenter, pointFrom } from "../../math";
const renderInteractiveScene = vi.spyOn( const renderInteractiveScene = vi.spyOn(
InteractiveCanvas, InteractiveCanvas,
@@ -57,8 +57,8 @@ describe("Test Linear Elements", () => {
interactiveCanvas = container.querySelector("canvas.interactive")!; interactiveCanvas = container.querySelector("canvas.interactive")!;
}); });
const p1 = point<GlobalPoint>(20, 20); const p1 = pointFrom<GlobalPoint>(20, 20);
const p2 = point<GlobalPoint>(60, 20); const p2 = pointFrom<GlobalPoint>(60, 20);
const midpoint = pointCenter<GlobalPoint>(p1, p2); const midpoint = pointCenter<GlobalPoint>(p1, p2);
const delta = 50; const delta = 50;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@@ -75,7 +75,7 @@ describe("Test Linear Elements", () => {
height: 0, height: 0,
type, type,
roughness, roughness,
points: [point(0, 0), point(p2[0] - p1[0], p2[1] - p1[1])], points: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])],
roundness, roundness,
}); });
API.setElements([line]); API.setElements([line]);
@@ -99,9 +99,9 @@ describe("Test Linear Elements", () => {
type, type,
roughness, roughness,
points: [ points: [
point(0, 0), pointFrom(0, 0),
point(p3[0], p3[1]), pointFrom(p3[0], p3[1]),
point(p2[0] - p1[0], p2[1] - p1[1]), pointFrom(p2[0] - p1[0], p2[1] - p1[1]),
], ],
roundness, roundness,
}); });
@@ -161,7 +161,7 @@ describe("Test Linear Elements", () => {
expect(line.points.length).toEqual(2); expect(line.points.length).toEqual(2);
mouse.clickAt(midpoint[0], midpoint[1]); mouse.clickAt(midpoint[0], midpoint[1]);
drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1)); drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1));
expect(line.points.length).toEqual(2); expect(line.points.length).toEqual(2);
@@ -169,7 +169,7 @@ describe("Test Linear Elements", () => {
expect(line.y).toBe(originalY); expect(line.y).toBe(originalY);
expect(line.points.length).toEqual(2); expect(line.points.length).toEqual(2);
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta)); drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
expect(line.x).toBe(originalX); expect(line.x).toBe(originalX);
expect(line.y).toBe(originalY); expect(line.y).toBe(originalY);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
@@ -184,7 +184,7 @@ describe("Test Linear Elements", () => {
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2); expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
// drag line from midpoint // drag line from midpoint
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta)); drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
@@ -248,7 +248,7 @@ describe("Test Linear Elements", () => {
mouse.clickAt(midpoint[0], midpoint[1]); mouse.clickAt(midpoint[0], midpoint[1]);
expect(line.points.length).toEqual(2); expect(line.points.length).toEqual(2);
drag(midpoint, point(midpoint[0] + 1, midpoint[1] + 1)); drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1));
expect(line.x).toBe(originalX); expect(line.x).toBe(originalX);
expect(line.y).toBe(originalY); expect(line.y).toBe(originalY);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
@@ -261,7 +261,7 @@ describe("Test Linear Elements", () => {
enterLineEditingMode(line); enterLineEditingMode(line);
// drag line from midpoint // drag line from midpoint
drag(midpoint, point(midpoint[0] + delta, midpoint[1] + delta)); drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `12`,
); );
@@ -356,7 +356,7 @@ describe("Test Linear Elements", () => {
const startPoint = pointCenter(points[0], midPoints[0]!); const startPoint = pointCenter(points[0], midPoints[0]!);
const deltaX = 50; const deltaX = 50;
const deltaY = 20; const deltaY = 20;
const endPoint = point<GlobalPoint>( const endPoint = pointFrom<GlobalPoint>(
startPoint[0] + deltaX, startPoint[0] + deltaX,
startPoint[1] + deltaY, startPoint[1] + deltaY,
); );
@@ -399,8 +399,8 @@ describe("Test Linear Elements", () => {
// This is the expected midpoint for line with round edge // This is the expected midpoint for line with round edge
// hence hardcoding it so if later some bug is introduced // hence hardcoding it so if later some bug is introduced
// this will fail and we can fix it // this will fail and we can fix it
const firstSegmentMidpoint = point<GlobalPoint>(55, 45); const firstSegmentMidpoint = pointFrom<GlobalPoint>(55, 45);
const lastSegmentMidpoint = point<GlobalPoint>(75, 40); const lastSegmentMidpoint = pointFrom<GlobalPoint>(75, 40);
let line: ExcalidrawLinearElement; let line: ExcalidrawLinearElement;
@@ -416,7 +416,7 @@ describe("Test Linear Elements", () => {
// drag line via first segment midpoint // drag line via first segment midpoint
drag( drag(
firstSegmentMidpoint, firstSegmentMidpoint,
point( pointFrom(
firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[0] + delta,
firstSegmentMidpoint[1] + delta, firstSegmentMidpoint[1] + delta,
), ),
@@ -426,7 +426,10 @@ describe("Test Linear Elements", () => {
// drag line from last segment midpoint // drag line from last segment midpoint
drag( drag(
lastSegmentMidpoint, lastSegmentMidpoint,
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta), pointFrom(
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
),
); );
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
@@ -475,10 +478,10 @@ describe("Test Linear Elements", () => {
h.state, h.state,
); );
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]); const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
// Drag from first point // Drag from first point
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta)); drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `12`,
@@ -516,10 +519,10 @@ describe("Test Linear Elements", () => {
h.state, h.state,
); );
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]); const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
// Drag from first point // Drag from first point
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta)); drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `12`,
@@ -556,7 +559,7 @@ describe("Test Linear Elements", () => {
// dragging line from last segment midpoint // dragging line from last segment midpoint
drag( drag(
lastSegmentMidpoint, lastSegmentMidpoint,
point(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50), pointFrom(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
); );
expect(line.points.length).toEqual(4); expect(line.points.length).toEqual(4);
@@ -589,11 +592,11 @@ describe("Test Linear Elements", () => {
// This is the expected midpoint for line with round edge // This is the expected midpoint for line with round edge
// hence hardcoding it so if later some bug is introduced // hence hardcoding it so if later some bug is introduced
// this will fail and we can fix it // this will fail and we can fix it
const firstSegmentMidpoint = point<GlobalPoint>( const firstSegmentMidpoint = pointFrom<GlobalPoint>(
55.9697848965255, 55.9697848965255,
47.442326230998205, 47.442326230998205,
); );
const lastSegmentMidpoint = point<GlobalPoint>( const lastSegmentMidpoint = pointFrom<GlobalPoint>(
76.08587175006699, 76.08587175006699,
43.294165939653226, 43.294165939653226,
); );
@@ -612,7 +615,7 @@ describe("Test Linear Elements", () => {
// drag line from first segment midpoint // drag line from first segment midpoint
drag( drag(
firstSegmentMidpoint, firstSegmentMidpoint,
point( pointFrom(
firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[0] + delta,
firstSegmentMidpoint[1] + delta, firstSegmentMidpoint[1] + delta,
), ),
@@ -622,7 +625,10 @@ describe("Test Linear Elements", () => {
// drag line from last segment midpoint // drag line from last segment midpoint
drag( drag(
lastSegmentMidpoint, lastSegmentMidpoint,
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta), pointFrom(
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
),
); );
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`16`, `16`,
@@ -669,10 +675,10 @@ describe("Test Linear Elements", () => {
h.state, h.state,
); );
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]); const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
// Drag from first point // Drag from first point
drag(hitCoords, point(hitCoords[0] - delta, hitCoords[1] - delta)); drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
const newPoints = LinearElementEditor.getPointsGlobalCoordinates( const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line, line,
@@ -717,10 +723,10 @@ describe("Test Linear Elements", () => {
h.state, h.state,
); );
const hitCoords = point<GlobalPoint>(points[0][0], points[0][1]); const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
// Drag from first point // Drag from first point
drag(hitCoords, point(hitCoords[0] + delta, hitCoords[1] + delta)); drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `12`,
@@ -751,7 +757,10 @@ describe("Test Linear Elements", () => {
drag( drag(
lastSegmentMidpoint, lastSegmentMidpoint,
point(lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta), pointFrom(
lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta,
),
); );
expect(line.points.length).toEqual(4); expect(line.points.length).toEqual(4);
@@ -811,8 +820,8 @@ describe("Test Linear Elements", () => {
API.setSelectedElements([line]); API.setSelectedElements([line]);
enterLineEditingMode(line, true); enterLineEditingMode(line, true);
drag( drag(
point(line.points[0][0] + line.x, line.points[0][1] + line.y), pointFrom(line.points[0][0] + line.x, line.points[0][1] + line.y),
point( pointFrom(
dragEndPositionOffset[0] + line.x, dragEndPositionOffset[0] + line.x,
dragEndPositionOffset[1] + line.y, dragEndPositionOffset[1] + line.y,
), ),
@@ -927,14 +936,14 @@ describe("Test Linear Elements", () => {
// This is the expected midpoint for line with round edge // This is the expected midpoint for line with round edge
// hence hardcoding it so if later some bug is introduced // hence hardcoding it so if later some bug is introduced
// this will fail and we can fix it // this will fail and we can fix it
const firstSegmentMidpoint = point<GlobalPoint>( const firstSegmentMidpoint = pointFrom<GlobalPoint>(
55.9697848965255, 55.9697848965255,
47.442326230998205, 47.442326230998205,
); );
// drag line from first segment midpoint // drag line from first segment midpoint
drag( drag(
firstSegmentMidpoint, firstSegmentMidpoint,
point( pointFrom(
firstSegmentMidpoint[0] + delta, firstSegmentMidpoint[0] + delta,
firstSegmentMidpoint[1] + delta, firstSegmentMidpoint[1] + delta,
), ),
@@ -1151,7 +1160,7 @@ describe("Test Linear Elements", () => {
); );
// Drag from last point // Drag from last point
drag(points[1], point(points[1][0] + 300, points[1][1])); drag(points[1], pointFrom(points[1][0] + 300, points[1][1]));
expect({ width: container.width, height: container.height }) expect({ width: container.width, height: container.height })
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
@@ -1350,11 +1359,11 @@ describe("Test Linear Elements", () => {
[ [
{ {
index: 0, index: 0,
point: point(line.points[0][0] + 10, line.points[0][1] + 10), point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
}, },
{ {
index: line.points.length - 1, index: line.points.length - 1,
point: point( point: pointFrom(
line.points[line.points.length - 1][0] - 10, line.points[line.points.length - 1][0] - 10,
line.points[line.points.length - 1][1] - 10, line.points[line.points.length - 1][1] - 10,
), ),
+35 -30
View File
@@ -17,7 +17,7 @@ import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import type { LocalPoint } from "../../math"; import type { LocalPoint } from "../../math";
import { point } from "../../math"; import { pointFrom } from "../../math";
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -220,12 +220,17 @@ describe("generic element", () => {
describe.each(["line", "freedraw"] as const)("%s element", (type) => { describe.each(["line", "freedraw"] as const)("%s element", (type) => {
const points: Record<typeof type, LocalPoint[]> = { const points: Record<typeof type, LocalPoint[]> = {
line: [point(0, 0), point(60, -20), point(20, 40), point(-40, 0)], line: [
pointFrom(0, 0),
pointFrom(60, -20),
pointFrom(20, 40),
pointFrom(-40, 0),
],
freedraw: [ freedraw: [
point(0, 0), pointFrom(0, 0),
point(-2.474600807561444, 41.021700699972), pointFrom(-2.474600807561444, 41.021700699972),
point(3.6627956000014024, 47.84174560617245), pointFrom(3.6627956000014024, 47.84174560617245),
point(40.495224145598115, 47.15909710753482), pointFrom(40.495224145598115, 47.15909710753482),
], ],
}; };
@@ -293,11 +298,11 @@ describe("arrow element", () => {
it("resizes with a label", async () => { it("resizes with a label", async () => {
const arrow = UI.createElement("arrow", { const arrow = UI.createElement("arrow", {
points: [ points: [
point(0, 0), pointFrom(0, 0),
point(40, 140), pointFrom(40, 140),
point(80, 60), // label's anchor pointFrom(80, 60), // label's anchor
point(180, 20), pointFrom(180, 20),
point(200, 120), pointFrom(200, 120),
], ],
}); });
const label = await UI.editText(arrow, "Hello"); const label = await UI.editText(arrow, "Hello");
@@ -747,24 +752,24 @@ describe("multiple selection", () => {
x: 60, x: 60,
y: 40, y: 40,
points: [ points: [
point(0, 0), pointFrom(0, 0),
point(-40, 40), pointFrom(-40, 40),
point(-60, 0), pointFrom(-60, 0),
point(0, -40), pointFrom(0, -40),
point(40, 20), pointFrom(40, 20),
point(0, 40), pointFrom(0, 40),
], ],
}); });
const freedraw = UI.createElement("freedraw", { const freedraw = UI.createElement("freedraw", {
x: 63.56072661326618, x: 63.56072661326618,
y: 100, y: 100,
points: [ points: [
point(0, 0), pointFrom(0, 0),
point(-43.56072661326618, 18.15048126846341), pointFrom(-43.56072661326618, 18.15048126846341),
point(-43.56072661326618, 29.041198460587566), pointFrom(-43.56072661326618, 29.041198460587566),
point(-38.115368017204105, 42.652452795512204), pointFrom(-38.115368017204105, 42.652452795512204),
point(-19.964886748740696, 66.24829266003775), pointFrom(-19.964886748740696, 66.24829266003775),
point(19.056612930986716, 77.1390098521619), pointFrom(19.056612930986716, 77.1390098521619),
], ],
}); });
@@ -1101,13 +1106,13 @@ describe("multiple selection", () => {
x: 60, x: 60,
y: 0, y: 0,
points: [ points: [
point(0, 0), pointFrom(0, 0),
point(-40, 40), pointFrom(-40, 40),
point(-20, 60), pointFrom(-20, 60),
point(20, 20), pointFrom(20, 20),
point(40, 40), pointFrom(40, 40),
point(-20, 100), pointFrom(-20, 100),
point(-60, 60), pointFrom(-60, 60),
], ],
}); });
File diff suppressed because one or more lines are too long
+11
View File
@@ -176,6 +176,8 @@ export type StaticCanvasAppState = Readonly<
gridStep: AppState["gridStep"]; gridStep: AppState["gridStep"];
frameRendering: AppState["frameRendering"]; frameRendering: AppState["frameRendering"];
currentHoveredFontFamily: AppState["currentHoveredFontFamily"]; currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
// Cropping
croppingElementId: AppState["croppingElementId"];
} }
>; >;
@@ -198,6 +200,9 @@ export type InteractiveCanvasAppState = Readonly<
snapLines: AppState["snapLines"]; snapLines: AppState["snapLines"];
zenModeEnabled: AppState["zenModeEnabled"]; zenModeEnabled: AppState["zenModeEnabled"];
editingTextElement: AppState["editingTextElement"]; editingTextElement: AppState["editingTextElement"];
// Cropping
isCropping: AppState["isCropping"];
croppingElementId: AppState["croppingElementId"];
// Search matches // Search matches
searchMatches: AppState["searchMatches"]; searchMatches: AppState["searchMatches"];
} }
@@ -219,6 +224,7 @@ export type ObservedElementsAppState = {
editingLinearElementId: LinearElementEditor["elementId"] | null; editingLinearElementId: LinearElementEditor["elementId"] | null;
// Right now it's coupled to `editingLinearElement`, ideally it should not be really needed as we already have selectedElementIds & editingLinearElementId // Right now it's coupled to `editingLinearElement`, ideally it should not be really needed as we already have selectedElementIds & editingLinearElementId
selectedLinearElementId: LinearElementEditor["elementId"] | null; selectedLinearElementId: LinearElementEditor["elementId"] | null;
croppingElementId: AppState["croppingElementId"];
}; };
export interface AppState { export interface AppState {
@@ -386,6 +392,11 @@ export interface AppState {
userToFollow: UserToFollow | null; userToFollow: UserToFollow | null;
/** the socket ids of the users following the current user */ /** the socket ids of the users following the current user */
followedBy: Set<SocketId>; followedBy: Set<SocketId>;
/** image cropping */
isCropping: boolean;
croppingElementId: ExcalidrawElement["id"] | null;
searchMatches: readonly SearchMatch[]; searchMatches: readonly SearchMatch[];
} }
+26 -21
View File
@@ -1,4 +1,9 @@
import { isLineSegment, lineSegment, point, type GlobalPoint } from "../math"; import {
isLineSegment,
lineSegment,
pointFrom,
type GlobalPoint,
} from "../math";
import type { LineSegment } from "../utils"; import type { LineSegment } from "../utils";
import type { BoundingBox, Bounds } from "./element/bounds"; import type { BoundingBox, Bounds } from "./element/bounds";
import { isBounds } from "./element/typeChecks"; import { isBounds } from "./element/typeChecks";
@@ -52,8 +57,8 @@ export const debugDrawPoint = (
debugDrawLine( debugDrawLine(
lineSegment( lineSegment(
point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10), pointFrom<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset - 10),
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10), pointFrom<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset + 10),
), ),
{ {
color: opts?.color ?? "cyan", color: opts?.color ?? "cyan",
@@ -62,8 +67,8 @@ export const debugDrawPoint = (
); );
debugDrawLine( debugDrawLine(
lineSegment( lineSegment(
point<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10), pointFrom<GlobalPoint>(p[0] + xOffset - 10, p[1] + yOffset + 10),
point<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10), pointFrom<GlobalPoint>(p[0] + xOffset + 10, p[1] + yOffset - 10),
), ),
{ {
color: opts?.color ?? "cyan", color: opts?.color ?? "cyan",
@@ -83,20 +88,20 @@ export const debugDrawBoundingBox = (
debugDrawLine( debugDrawLine(
[ [
lineSegment( lineSegment(
point<GlobalPoint>(bbox.minX, bbox.minY), pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
point<GlobalPoint>(bbox.maxX, bbox.minY), pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
), ),
lineSegment( lineSegment(
point<GlobalPoint>(bbox.maxX, bbox.minY), pointFrom<GlobalPoint>(bbox.maxX, bbox.minY),
point<GlobalPoint>(bbox.maxX, bbox.maxY), pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
), ),
lineSegment( lineSegment(
point<GlobalPoint>(bbox.maxX, bbox.maxY), pointFrom<GlobalPoint>(bbox.maxX, bbox.maxY),
point<GlobalPoint>(bbox.minX, bbox.maxY), pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
), ),
lineSegment( lineSegment(
point<GlobalPoint>(bbox.minX, bbox.maxY), pointFrom<GlobalPoint>(bbox.minX, bbox.maxY),
point<GlobalPoint>(bbox.minX, bbox.minY), pointFrom<GlobalPoint>(bbox.minX, bbox.minY),
), ),
], ],
{ {
@@ -118,20 +123,20 @@ export const debugDrawBounds = (
debugDrawLine( debugDrawLine(
[ [
lineSegment( lineSegment(
point<GlobalPoint>(bbox[0], bbox[1]), pointFrom<GlobalPoint>(bbox[0], bbox[1]),
point<GlobalPoint>(bbox[2], bbox[1]), pointFrom<GlobalPoint>(bbox[2], bbox[1]),
), ),
lineSegment( lineSegment(
point<GlobalPoint>(bbox[2], bbox[1]), pointFrom<GlobalPoint>(bbox[2], bbox[1]),
point<GlobalPoint>(bbox[2], bbox[3]), pointFrom<GlobalPoint>(bbox[2], bbox[3]),
), ),
lineSegment( lineSegment(
point<GlobalPoint>(bbox[2], bbox[3]), pointFrom<GlobalPoint>(bbox[2], bbox[3]),
point<GlobalPoint>(bbox[0], bbox[3]), pointFrom<GlobalPoint>(bbox[0], bbox[3]),
), ),
lineSegment( lineSegment(
point<GlobalPoint>(bbox[0], bbox[3]), pointFrom<GlobalPoint>(bbox[0], bbox[3]),
point<GlobalPoint>(bbox[0], bbox[1]), pointFrom<GlobalPoint>(bbox[0], bbox[1]),
), ),
], ],
{ {
+4 -4
View File
@@ -1,5 +1,5 @@
import { isPointOnSymmetricArc } from "./arc"; import { isPointOnSymmetricArc } from "./arc";
import { point } from "./point"; import { pointFrom } from "./point";
describe("point on arc", () => { describe("point on arc", () => {
it("should detect point on simple arc", () => { it("should detect point on simple arc", () => {
@@ -10,7 +10,7 @@ describe("point on arc", () => {
startAngle: -Math.PI / 4, startAngle: -Math.PI / 4,
endAngle: Math.PI / 4, endAngle: Math.PI / 4,
}, },
point(0.92291667, 0.385), pointFrom(0.92291667, 0.385),
), ),
).toBe(true); ).toBe(true);
}); });
@@ -22,7 +22,7 @@ describe("point on arc", () => {
startAngle: -Math.PI / 4, startAngle: -Math.PI / 4,
endAngle: Math.PI / 4, endAngle: Math.PI / 4,
}, },
point(-0.92291667, 0.385), pointFrom(-0.92291667, 0.385),
), ),
).toBe(false); ).toBe(false);
}); });
@@ -34,7 +34,7 @@ describe("point on arc", () => {
startAngle: -Math.PI / 4, startAngle: -Math.PI / 4,
endAngle: Math.PI / 4, endAngle: Math.PI / 4,
}, },
point(-0.5, 0.5), pointFrom(-0.5, 0.5),
), ),
).toBe(false); ).toBe(false);
}); });
+11 -11
View File
@@ -1,4 +1,4 @@
import { point, pointRotateRads } from "./point"; import { pointFrom, pointRotateRads } from "./point";
import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types"; import type { Curve, GlobalPoint, LocalPoint, Radians } from "./types";
/** /**
@@ -43,10 +43,10 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
const out: Point[] = []; const out: Point[] = [];
if (len === 3) { if (len === 3) {
out.push( out.push(
point(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned pointFrom(pointsIn[0][0], pointsIn[0][1]), // Points need to be cloned
point(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned pointFrom(pointsIn[1][0], pointsIn[1][1]), // Points need to be cloned
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
point(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned pointFrom(pointsIn[2][0], pointsIn[2][1]), // Points need to be cloned
); );
} else { } else {
const points: Point[] = []; const points: Point[] = [];
@@ -59,19 +59,19 @@ export function curveToBezier<Point extends LocalPoint | GlobalPoint>(
} }
const b: Point[] = []; const b: Point[] = [];
const s = 1 - curveTightness; const s = 1 - curveTightness;
out.push(point(points[0][0], points[0][1])); out.push(pointFrom(points[0][0], points[0][1]));
for (let i = 1; i + 2 < points.length; i++) { for (let i = 1; i + 2 < points.length; i++) {
const cachedVertArray = points[i]; const cachedVertArray = points[i];
b[0] = point(cachedVertArray[0], cachedVertArray[1]); b[0] = pointFrom(cachedVertArray[0], cachedVertArray[1]);
b[1] = point( b[1] = pointFrom(
cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6, cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6, cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
); );
b[2] = point( b[2] = pointFrom(
points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6, points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6, points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
); );
b[3] = point(points[i + 1][0], points[i + 1][1]); b[3] = pointFrom(points[i + 1][0], points[i + 1][1]);
out.push(b[1], b[2], b[3]); out.push(b[1], b[2], b[3]);
} }
} }
@@ -102,7 +102,7 @@ export const cubicBezierPoint = <Point extends LocalPoint | GlobalPoint>(
3 * (1 - t) * Math.pow(t, 2) * p2[1] + 3 * (1 - t) * Math.pow(t, 2) * p2[1] +
Math.pow(t, 3) * p3[1]; Math.pow(t, 3) * p3[1];
return point(x, y); return pointFrom(x, y);
}; };
/** /**
+5 -5
View File
@@ -1,4 +1,4 @@
import { point, pointRotateRads } from "./point"; import { pointFrom, pointRotateRads } from "./point";
import type { Radians } from "./types"; import type { Radians } from "./types";
describe("rotate", () => { describe("rotate", () => {
@@ -9,14 +9,14 @@ describe("rotate", () => {
const y2 = 30; const y2 = 30;
const angle = (Math.PI / 2) as Radians; const angle = (Math.PI / 2) as Radians;
const [rotatedX, rotatedY] = pointRotateRads( const [rotatedX, rotatedY] = pointRotateRads(
point(x1, y1), pointFrom(x1, y1),
point(x2, y2), pointFrom(x2, y2),
angle, angle,
); );
expect([rotatedX, rotatedY]).toEqual([30, 20]); expect([rotatedX, rotatedY]).toEqual([30, 20]);
const res2 = pointRotateRads( const res2 = pointRotateRads(
point(rotatedX, rotatedY), pointFrom(rotatedX, rotatedY),
point(x2, y2), pointFrom(x2, y2),
-angle as Radians, -angle as Radians,
); );
expect(res2).toEqual([x1, x2]); expect(res2).toEqual([x1, x2]);
+7 -7
View File
@@ -16,7 +16,7 @@ import { vectorFromPoint, vectorScale } from "./vector";
* @param y The Y coordinate * @param y The Y coordinate
* @returns The branded and created point * @returns The branded and created point
*/ */
export function point<Point extends GlobalPoint | LocalPoint>( export function pointFrom<Point extends GlobalPoint | LocalPoint>(
x: number, x: number,
y: number, y: number,
): Point { ): Point {
@@ -33,7 +33,7 @@ export function pointFromArray<Point extends GlobalPoint | LocalPoint>(
numberArray: number[], numberArray: number[],
): Point | undefined { ): Point | undefined {
return numberArray.length === 2 return numberArray.length === 2
? point<Point>(numberArray[0], numberArray[1]) ? pointFrom<Point>(numberArray[0], numberArray[1])
: undefined; : undefined;
} }
@@ -107,7 +107,7 @@ export function pointRotateRads<Point extends GlobalPoint | LocalPoint>(
[cx, cy]: Point, [cx, cy]: Point,
angle: Radians, angle: Radians,
): Point { ): Point {
return point( return pointFrom(
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx, (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy, (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy,
); );
@@ -146,7 +146,7 @@ export function pointTranslate<
From extends GlobalPoint | LocalPoint, From extends GlobalPoint | LocalPoint,
To extends GlobalPoint | LocalPoint, To extends GlobalPoint | LocalPoint,
>(p: From, v: Vector = [0, 0] as Vector): To { >(p: From, v: Vector = [0, 0] as Vector): To {
return point(p[0] + v[0], p[1] + v[1]); return pointFrom(p[0] + v[0], p[1] + v[1]);
} }
/** /**
@@ -157,7 +157,7 @@ export function pointTranslate<
* @returns The middle point * @returns The middle point
*/ */
export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P { export function pointCenter<P extends LocalPoint | GlobalPoint>(a: P, b: P): P {
return point((a[0] + b[0]) / 2, (a[1] + b[1]) / 2); return pointFrom((a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
} }
/** /**
@@ -172,7 +172,7 @@ export function pointAdd<Point extends LocalPoint | GlobalPoint>(
a: Point, a: Point,
b: Point, b: Point,
): Point { ): Point {
return point(a[0] + b[0], a[1] + b[1]); return pointFrom(a[0] + b[0], a[1] + b[1]);
} }
/** /**
@@ -187,7 +187,7 @@ export function pointSubtract<Point extends LocalPoint | GlobalPoint>(
a: Point, a: Point,
b: Point, b: Point,
): Point { ): Point {
return point(a[0] - b[0], a[1] - b[1]); return pointFrom(a[0] - b[0], a[1] - b[1]);
} }
/** /**
+3
View File
@@ -28,3 +28,6 @@ export const average = (a: number, b: number) => (a + b) / 2;
export const isFiniteNumber = (value: any): value is number => { export const isFiniteNumber = (value: any): value is number => {
return typeof value === "number" && Number.isFinite(value); return typeof value === "number" && Number.isFinite(value);
}; };
export const isCloseTo = (a: number, b: number, precision = PRECISION) =>
Math.abs(a - b) < precision;
+7
View File
@@ -139,3 +139,10 @@ export const vectorNormalize = (v: Vector): Vector => {
return vector(v[0] / m, v[1] / m); return vector(v[0] / m, v[1] / m);
}; };
/**
* Project the first vector onto the second vector
*/
export const vectorProjection = (a: Vector, b: Vector) => {
return vectorScale(b, vectorDot(a, b) / vectorDot(b, b));
};
@@ -11,6 +11,7 @@ exports[`exportToSvg > with default arguments 1`] = `
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@@ -53,6 +54,7 @@ exports[`exportToSvg > with default arguments 1`] = `
"gridSize": 20, "gridSize": 20,
"gridStep": 5, "gridStep": 5,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
+33 -33
View File
@@ -4,7 +4,7 @@ import {
degreesToRadians, degreesToRadians,
lineSegment, lineSegment,
lineSegmentRotate, lineSegmentRotate,
point, pointFrom,
pointRotateDegs, pointRotateDegs,
} from "../math"; } from "../math";
import { pointOnCurve, pointOnPolyline } from "./collision"; import { pointOnCurve, pointOnPolyline } from "./collision";
@@ -12,21 +12,21 @@ import type { Polyline } from "./geometry/shape";
describe("point and curve", () => { describe("point and curve", () => {
const c: Curve<GlobalPoint> = curve( const c: Curve<GlobalPoint> = curve(
point(1.4, 1.65), pointFrom(1.4, 1.65),
point(1.9, 7.9), pointFrom(1.9, 7.9),
point(5.9, 1.65), pointFrom(5.9, 1.65),
point(6.44, 4.84), pointFrom(6.44, 4.84),
); );
it("point on curve", () => { it("point on curve", () => {
expect(pointOnCurve(c[0], c, 10e-5)).toBe(true); expect(pointOnCurve(c[0], c, 10e-5)).toBe(true);
expect(pointOnCurve(c[3], c, 10e-5)).toBe(true); expect(pointOnCurve(c[3], c, 10e-5)).toBe(true);
expect(pointOnCurve(point(2, 4), c, 0.1)).toBe(true); expect(pointOnCurve(pointFrom(2, 4), c, 0.1)).toBe(true);
expect(pointOnCurve(point(4, 4.4), c, 0.1)).toBe(true); expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true);
expect(pointOnCurve(point(5.6, 3.85), c, 0.1)).toBe(true); expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true);
expect(pointOnCurve(point(5.6, 4), c, 0.1)).toBe(false); expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false);
expect(pointOnCurve(c[1], c, 0.1)).toBe(false); expect(pointOnCurve(c[1], c, 0.1)).toBe(false);
expect(pointOnCurve(c[2], c, 0.1)).toBe(false); expect(pointOnCurve(c[2], c, 0.1)).toBe(false);
}); });
@@ -34,52 +34,52 @@ describe("point and curve", () => {
describe("point and polylines", () => { describe("point and polylines", () => {
const polyline: Polyline<GlobalPoint> = [ const polyline: Polyline<GlobalPoint> = [
lineSegment(point(1, 0), point(1, 2)), lineSegment(pointFrom(1, 0), pointFrom(1, 2)),
lineSegment(point(1, 2), point(2, 2)), lineSegment(pointFrom(1, 2), pointFrom(2, 2)),
lineSegment(point(2, 2), point(2, 1)), lineSegment(pointFrom(2, 2), pointFrom(2, 1)),
lineSegment(point(2, 1), point(3, 1)), lineSegment(pointFrom(2, 1), pointFrom(3, 1)),
]; ];
it("point on the line", () => { it("point on the line", () => {
expect(pointOnPolyline(point(1, 0), polyline)).toBe(true); expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true);
expect(pointOnPolyline(point(1, 2), polyline)).toBe(true); expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true);
expect(pointOnPolyline(point(2, 2), polyline)).toBe(true); expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true);
expect(pointOnPolyline(point(2, 1), polyline)).toBe(true); expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true);
expect(pointOnPolyline(point(3, 1), polyline)).toBe(true); expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true);
expect(pointOnPolyline(point(1, 1), polyline)).toBe(true); expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true);
expect(pointOnPolyline(point(2, 1.5), polyline)).toBe(true); expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true);
expect(pointOnPolyline(point(2.5, 1), polyline)).toBe(true); expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true);
expect(pointOnPolyline(point(0, 1), polyline)).toBe(false); expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false);
expect(pointOnPolyline(point(2.1, 1.5), polyline)).toBe(false); expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false);
}); });
it("point on the line with rotation", () => { it("point on the line with rotation", () => {
const truePoints = [ const truePoints = [
point(1, 0), pointFrom(1, 0),
point(1, 2), pointFrom(1, 2),
point(2, 2), pointFrom(2, 2),
point(2, 1), pointFrom(2, 1),
point(3, 1), pointFrom(3, 1),
]; ];
truePoints.forEach((p) => { truePoints.forEach((p) => {
const rotation = (Math.random() * 360) as Degrees; const rotation = (Math.random() * 360) as Degrees;
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation); const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
const rotatedPolyline = polyline.map((line) => const rotatedPolyline = polyline.map((line) =>
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)), lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
); );
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true); expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
}); });
const falsePoints = [point(0, 1), point(2.1, 1.5)]; const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)];
falsePoints.forEach((p) => { falsePoints.forEach((p) => {
const rotation = (Math.random() * 360) as Degrees; const rotation = (Math.random() * 360) as Degrees;
const rotatedPoint = pointRotateDegs(p, point(0, 0), rotation); const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation);
const rotatedPolyline = polyline.map((line) => const rotatedPolyline = polyline.map((line) =>
lineSegmentRotate(line, degreesToRadians(rotation), point(0, 0)), lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)),
); );
expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false); expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
}); });
+2 -2
View File
@@ -7,7 +7,7 @@ import {
import type { Curve } from "../math"; import type { Curve } from "../math";
import { import {
lineSegment, lineSegment,
point, pointFrom,
polygonIncludesPoint, polygonIncludesPoint,
pointOnLineSegment, pointOnLineSegment,
pointOnPolygon, pointOnPolygon,
@@ -110,7 +110,7 @@ const polyLineFromCurve = <Point extends LocalPoint | GlobalPoint>(
for (let i = 0; i < segments; i++) { for (let i = 0; i < segments; i++) {
t += increment; t += increment;
if (t <= 1) { if (t <= 1) {
const nextPoint: Point = point(equation(t, 0), equation(t, 1)); const nextPoint: Point = pointFrom(equation(t, 0), equation(t, 1));
lineSegments.push(lineSegment(startingPoint, nextPoint)); lineSegments.push(lineSegment(startingPoint, nextPoint));
startingPoint = nextPoint; startingPoint = nextPoint;
} }
+79 -45
View File
@@ -1,6 +1,6 @@
import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math"; import type { GlobalPoint, LineSegment, Polygon, Radians } from "../../math";
import { import {
point, pointFrom,
lineSegment, lineSegment,
polygon, polygon,
pointOnLineSegment, pointOnLineSegment,
@@ -23,93 +23,127 @@ describe("point and line", () => {
// expect(pointRightofLine(point(2, 1), l)).toBe(true); // expect(pointRightofLine(point(2, 1), l)).toBe(true);
// }); // });
const s: LineSegment<GlobalPoint> = lineSegment(point(1, 0), point(1, 2)); const s: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 0),
pointFrom(1, 2),
);
it("point on the line", () => { it("point on the line", () => {
expect(pointOnLineSegment(point(0, 1), s)).toBe(false); expect(pointOnLineSegment(pointFrom(0, 1), s)).toBe(false);
expect(pointOnLineSegment(point(1, 1), s, 0)).toBe(true); expect(pointOnLineSegment(pointFrom(1, 1), s, 0)).toBe(true);
expect(pointOnLineSegment(point(2, 1), s)).toBe(false); expect(pointOnLineSegment(pointFrom(2, 1), s)).toBe(false);
}); });
}); });
describe("point and polygon", () => { describe("point and polygon", () => {
const poly: Polygon<GlobalPoint> = polygon( const poly: Polygon<GlobalPoint> = polygon(
point(10, 10), pointFrom(10, 10),
point(50, 10), pointFrom(50, 10),
point(50, 50), pointFrom(50, 50),
point(10, 50), pointFrom(10, 50),
); );
it("point on polygon", () => { it("point on polygon", () => {
expect(pointOnPolygon(point(30, 10), poly)).toBe(true); expect(pointOnPolygon(pointFrom(30, 10), poly)).toBe(true);
expect(pointOnPolygon(point(50, 30), poly)).toBe(true); expect(pointOnPolygon(pointFrom(50, 30), poly)).toBe(true);
expect(pointOnPolygon(point(30, 50), poly)).toBe(true); expect(pointOnPolygon(pointFrom(30, 50), poly)).toBe(true);
expect(pointOnPolygon(point(10, 30), poly)).toBe(true); expect(pointOnPolygon(pointFrom(10, 30), poly)).toBe(true);
expect(pointOnPolygon(point(30, 30), poly)).toBe(false); expect(pointOnPolygon(pointFrom(30, 30), poly)).toBe(false);
expect(pointOnPolygon(point(30, 70), poly)).toBe(false); expect(pointOnPolygon(pointFrom(30, 70), poly)).toBe(false);
}); });
it("point in polygon", () => { it("point in polygon", () => {
const poly: Polygon<GlobalPoint> = polygon( const poly: Polygon<GlobalPoint> = polygon(
point(0, 0), pointFrom(0, 0),
point(2, 0), pointFrom(2, 0),
point(2, 2), pointFrom(2, 2),
point(0, 2), pointFrom(0, 2),
); );
expect(polygonIncludesPoint(point(1, 1), poly)).toBe(true); expect(polygonIncludesPoint(pointFrom(1, 1), poly)).toBe(true);
expect(polygonIncludesPoint(point(3, 3), poly)).toBe(false); expect(polygonIncludesPoint(pointFrom(3, 3), poly)).toBe(false);
}); });
}); });
describe("point and ellipse", () => { describe("point and ellipse", () => {
const ellipse: Ellipse<GlobalPoint> = { const ellipse: Ellipse<GlobalPoint> = {
center: point(0, 0), center: pointFrom(0, 0),
angle: 0 as Radians, angle: 0 as Radians,
halfWidth: 2, halfWidth: 2,
halfHeight: 1, halfHeight: 1,
}; };
it("point on ellipse", () => { it("point on ellipse", () => {
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => { [
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(pointOnEllipse(p, ellipse)).toBe(true); expect(pointOnEllipse(p, ellipse)).toBe(true);
}); });
expect(pointOnEllipse(point(-1.4, 0.7), ellipse, 0.1)).toBe(true); expect(pointOnEllipse(pointFrom(-1.4, 0.7), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(point(-1.4, 0.71), ellipse, 0.01)).toBe(true); expect(pointOnEllipse(pointFrom(-1.4, 0.71), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(point(1.4, 0.7), ellipse, 0.1)).toBe(true); expect(pointOnEllipse(pointFrom(1.4, 0.7), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(point(1.4, 0.71), ellipse, 0.01)).toBe(true); expect(pointOnEllipse(pointFrom(1.4, 0.71), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.1)).toBe(true); expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(point(1, -0.86), ellipse, 0.01)).toBe(true); expect(pointOnEllipse(pointFrom(1, -0.86), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.1)).toBe(true); expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.1)).toBe(true);
expect(pointOnEllipse(point(-1, -0.86), ellipse, 0.01)).toBe(true); expect(pointOnEllipse(pointFrom(-1, -0.86), ellipse, 0.01)).toBe(true);
expect(pointOnEllipse(point(-1, 0.8), ellipse)).toBe(false); expect(pointOnEllipse(pointFrom(-1, 0.8), ellipse)).toBe(false);
expect(pointOnEllipse(point(1, -0.8), ellipse)).toBe(false); expect(pointOnEllipse(pointFrom(1, -0.8), ellipse)).toBe(false);
}); });
it("point in ellipse", () => { it("point in ellipse", () => {
[point(0, 1), point(0, -1), point(2, 0), point(-2, 0)].forEach((p) => { [
pointFrom(0, 1),
pointFrom(0, -1),
pointFrom(2, 0),
pointFrom(-2, 0),
].forEach((p) => {
expect(pointInEllipse(p, ellipse)).toBe(true); expect(pointInEllipse(p, ellipse)).toBe(true);
}); });
expect(pointInEllipse(point(-1, 0.8), ellipse)).toBe(true); expect(pointInEllipse(pointFrom(-1, 0.8), ellipse)).toBe(true);
expect(pointInEllipse(point(1, -0.8), ellipse)).toBe(true); expect(pointInEllipse(pointFrom(1, -0.8), ellipse)).toBe(true);
expect(pointInEllipse(point(-1, 1), ellipse)).toBe(false); expect(pointInEllipse(pointFrom(-1, 1), ellipse)).toBe(false);
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false); expect(pointInEllipse(pointFrom(-1.4, 0.8), ellipse)).toBe(false);
}); });
}); });
describe("line and line", () => { describe("line and line", () => {
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4)); const lineA: LineSegment<GlobalPoint> = lineSegment(
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7)); pointFrom(1, 4),
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8)); pointFrom(3, 4),
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8)); );
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9)); const lineB: LineSegment<GlobalPoint> = lineSegment(
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4)); pointFrom(2, 1),
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3)); pointFrom(2, 7),
);
const lineC: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 8),
pointFrom(3, 8),
);
const lineD: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 8),
pointFrom(3, 8),
);
const lineE: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 9),
pointFrom(3, 9),
);
const lineF: LineSegment<GlobalPoint> = lineSegment(
pointFrom(1, 2),
pointFrom(3, 4),
);
const lineG: LineSegment<GlobalPoint> = lineSegment(
pointFrom(0, 1),
pointFrom(2, 3),
);
it("intersection", () => { it("intersection", () => {
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]); expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
+44 -41
View File
@@ -16,7 +16,7 @@ import type { Curve, LineSegment, Polygon, Radians } from "../../math";
import { import {
curve, curve,
lineSegment, lineSegment,
point, pointFrom,
pointDistance, pointDistance,
pointFromArray, pointFromArray,
pointFromVector, pointFromVector,
@@ -118,23 +118,23 @@ export const getPolygonShape = <Point extends GlobalPoint | LocalPoint>(
const cx = x + width / 2; const cx = x + width / 2;
const cy = y + height / 2; const cy = y + height / 2;
const center: Point = point(cx, cy); const center: Point = pointFrom(cx, cy);
let data: Polygon<Point>; let data: Polygon<Point>;
if (element.type === "diamond") { if (element.type === "diamond") {
data = polygon( data = polygon(
pointRotateRads(point(cx, y), center, angle), pointRotateRads(pointFrom(cx, y), center, angle),
pointRotateRads(point(x + width, cy), center, angle), pointRotateRads(pointFrom(x + width, cy), center, angle),
pointRotateRads(point(cx, y + height), center, angle), pointRotateRads(pointFrom(cx, y + height), center, angle),
pointRotateRads(point(x, cy), center, angle), pointRotateRads(pointFrom(x, cy), center, angle),
); );
} else { } else {
data = polygon( data = polygon(
pointRotateRads(point(x, y), center, angle), pointRotateRads(pointFrom(x, y), center, angle),
pointRotateRads(point(x + width, y), center, angle), pointRotateRads(pointFrom(x + width, y), center, angle),
pointRotateRads(point(x + width, y + height), center, angle), pointRotateRads(pointFrom(x + width, y + height), center, angle),
pointRotateRads(point(x, y + height), center, angle), pointRotateRads(pointFrom(x, y + height), center, angle),
); );
} }
@@ -162,11 +162,11 @@ export const getSelectionBoxShape = <Point extends GlobalPoint | LocalPoint>(
y2 += padding; y2 += padding;
//const angleInDegrees = angleToDegrees(element.angle); //const angleInDegrees = angleToDegrees(element.angle);
const center = point(cx, cy); const center = pointFrom(cx, cy);
const topLeft = pointRotateRads(point(x1, y1), center, element.angle); const topLeft = pointRotateRads(pointFrom(x1, y1), center, element.angle);
const topRight = pointRotateRads(point(x2, y1), center, element.angle); const topRight = pointRotateRads(pointFrom(x2, y1), center, element.angle);
const bottomLeft = pointRotateRads(point(x1, y2), center, element.angle); const bottomLeft = pointRotateRads(pointFrom(x1, y2), center, element.angle);
const bottomRight = pointRotateRads(point(x2, y2), center, element.angle); const bottomRight = pointRotateRads(pointFrom(x2, y2), center, element.angle);
return { return {
type: "polygon", type: "polygon",
@@ -183,7 +183,7 @@ export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
return { return {
type: "ellipse", type: "ellipse",
data: { data: {
center: point(x + width / 2, y + height / 2), center: pointFrom(x + width / 2, y + height / 2),
angle, angle,
halfWidth: width / 2, halfWidth: width / 2,
halfHeight: height / 2, halfHeight: height / 2,
@@ -203,20 +203,20 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
// linear // linear
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>( export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
roughShape: Drawable, roughShape: Drawable,
startingPoint: Point = point(0, 0), startingPoint: Point = pointFrom(0, 0),
angleInRadian: Radians, angleInRadian: Radians,
center: Point, center: Point,
): GeometricShape<Point> => { ): GeometricShape<Point> => {
const transform = (p: Point): Point => const transform = (p: Point): Point =>
pointRotateRads( pointRotateRads(
point(p[0] + startingPoint[0], p[1] + startingPoint[1]), pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
center, center,
angleInRadian, angleInRadian,
); );
const ops = getCurvePathOps(roughShape); const ops = getCurvePathOps(roughShape);
const polycurve: Polycurve<Point> = []; const polycurve: Polycurve<Point> = [];
let p0 = point<Point>(0, 0); let p0 = pointFrom<Point>(0, 0);
for (const op of ops) { for (const op of ops) {
if (op.op === "move") { if (op.op === "move") {
@@ -225,9 +225,9 @@ export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
p0 = transform(p); p0 = transform(p);
} }
if (op.op === "bcurveTo") { if (op.op === "bcurveTo") {
const p1 = transform(point<Point>(op.data[0], op.data[1])); const p1 = transform(pointFrom<Point>(op.data[0], op.data[1]));
const p2 = transform(point<Point>(op.data[2], op.data[3])); const p2 = transform(pointFrom<Point>(op.data[2], op.data[3]));
const p3 = transform(point<Point>(op.data[4], op.data[5])); const p3 = transform(pointFrom<Point>(op.data[4], op.data[5]));
polycurve.push(curve<Point>(p0, p1, p2, p3)); polycurve.push(curve<Point>(p0, p1, p2, p3));
p0 = p3; p0 = p3;
} }
@@ -288,13 +288,13 @@ export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>( export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
roughShape: Drawable, roughShape: Drawable,
startingPoint: Point = point<Point>(0, 0), startingPoint: Point = pointFrom<Point>(0, 0),
angleInRadian: Radians, angleInRadian: Radians,
center: Point, center: Point,
): GeometricShape<Point> => { ): GeometricShape<Point> => {
const transform = (p: Point) => const transform = (p: Point) =>
pointRotateRads( pointRotateRads(
point(p[0] + startingPoint[0], p[1] + startingPoint[1]), pointFrom(p[0] + startingPoint[0], p[1] + startingPoint[1]),
center, center,
angleInRadian, angleInRadian,
); );
@@ -316,17 +316,17 @@ export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
if (operation.op === "move") { if (operation.op === "move") {
odd = !odd; odd = !odd;
if (odd) { if (odd) {
points.push(point(operation.data[0], operation.data[1])); points.push(pointFrom(operation.data[0], operation.data[1]));
} }
} else if (operation.op === "bcurveTo") { } else if (operation.op === "bcurveTo") {
if (odd) { if (odd) {
points.push(point(operation.data[0], operation.data[1])); points.push(pointFrom(operation.data[0], operation.data[1]));
points.push(point(operation.data[2], operation.data[3])); points.push(pointFrom(operation.data[2], operation.data[3]));
points.push(point(operation.data[4], operation.data[5])); points.push(pointFrom(operation.data[4], operation.data[5]));
} }
} else if (operation.op === "lineTo") { } else if (operation.op === "lineTo") {
if (odd) { if (odd) {
points.push(point(operation.data[0], operation.data[1])); points.push(pointFrom(operation.data[0], operation.data[1]));
} }
} }
} }
@@ -364,27 +364,27 @@ export const segmentIntersectRectangleElement = <
element.x + element.width + gap, element.x + element.width + gap,
element.y + element.height + gap, element.y + element.height + gap,
]; ];
const center = point( const center = pointFrom(
(bounds[0] + bounds[2]) / 2, (bounds[0] + bounds[2]) / 2,
(bounds[1] + bounds[3]) / 2, (bounds[1] + bounds[3]) / 2,
); );
return [ return [
lineSegment( lineSegment(
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle), pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle), pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
), ),
lineSegment( lineSegment(
pointRotateRads(point(bounds[2], bounds[1]), center, element.angle), pointRotateRads(pointFrom(bounds[2], bounds[1]), center, element.angle),
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle), pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
), ),
lineSegment( lineSegment(
pointRotateRads(point(bounds[2], bounds[3]), center, element.angle), pointRotateRads(pointFrom(bounds[2], bounds[3]), center, element.angle),
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle), pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
), ),
lineSegment( lineSegment(
pointRotateRads(point(bounds[0], bounds[3]), center, element.angle), pointRotateRads(pointFrom(bounds[0], bounds[3]), center, element.angle),
pointRotateRads(point(bounds[0], bounds[1]), center, element.angle), pointRotateRads(pointFrom(bounds[0], bounds[1]), center, element.angle),
), ),
] ]
.map((s) => segmentsIntersectAt(segment, s)) .map((s) => segmentsIntersectAt(segment, s))
@@ -404,7 +404,7 @@ const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
); );
const [rotatedPointX, rotatedPointY] = pointRotateRads( const [rotatedPointX, rotatedPointY] = pointRotateRads(
pointFromVector(translatedPoint), pointFromVector(translatedPoint),
point(0, 0), pointFrom(0, 0),
-angle as Radians, -angle as Radians,
); );
@@ -442,7 +442,10 @@ const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
b * ty * Math.sign(rotatedPointY), b * ty * Math.sign(rotatedPointY),
]; ];
return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY)); return pointDistance(
pointFrom(rotatedPointX, rotatedPointY),
pointFrom(minX, minY),
);
}; };
export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>( export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
@@ -464,7 +467,7 @@ export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
); );
const [rotatedPointX, rotatedPointY] = pointRotateRads( const [rotatedPointX, rotatedPointY] = pointRotateRads(
pointFromVector(translatedPoint), pointFromVector(translatedPoint),
point(0, 0), pointFrom(0, 0),
-angle as Radians, -angle as Radians,
); );
-1
View File
@@ -68,7 +68,6 @@
"css-loader": "6.7.1", "css-loader": "6.7.1",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"fonteditor-core": "2.4.0", "fonteditor-core": "2.4.0",
"node-fetch": "3.3.2",
"sass-loader": "13.0.2", "sass-loader": "13.0.2",
"ts-loader": "9.3.1", "ts-loader": "9.3.1",
"typescript": "4.9.4", "typescript": "4.9.4",
+10 -10
View File
@@ -17,7 +17,7 @@ import { arrayToMap } from "../excalidraw/utils";
import type { LocalPoint } from "../math"; import type { LocalPoint } from "../math";
import { import {
rangeIncludesValue, rangeIncludesValue,
point, pointFrom,
pointRotateRads, pointRotateRads,
rangeInclusive, rangeInclusive,
} from "../math"; } from "../math";
@@ -41,17 +41,17 @@ const getNonLinearElementRelativePoints = (
] => { ] => {
if (element.type === "diamond") { if (element.type === "diamond") {
return [ return [
point(element.width / 2, 0), pointFrom(element.width / 2, 0),
point(element.width, element.height / 2), pointFrom(element.width, element.height / 2),
point(element.width / 2, element.height), pointFrom(element.width / 2, element.height),
point(0, element.height / 2), pointFrom(0, element.height / 2),
]; ];
} }
return [ return [
point(0, 0), pointFrom(0, 0),
point(0 + element.width, 0), pointFrom(0 + element.width, 0),
point(0 + element.width, element.height), pointFrom(0 + element.width, element.height),
point(0, element.height), pointFrom(0, element.height),
]; ];
}; };
@@ -94,7 +94,7 @@ const getRotatedBBox = (element: Element): Bounds => {
const points = getElementRelativePoints(element); const points = getElementRelativePoints(element);
const { cx, cy } = getMinMaxPoints(points); const { cx, cy } = getMinMaxPoints(points);
const centerPoint = point<LocalPoint>(cx, cy); const centerPoint = pointFrom<LocalPoint>(cx, cy);
const rotatedPoints = points.map((p) => const rotatedPoints = points.map((p) =>
pointRotateRads(p, centerPoint, element.angle), pointRotateRads(p, centerPoint, element.angle),
+5 -3
View File
@@ -1,7 +1,6 @@
const { build } = require("esbuild"); const { build } = require("esbuild");
const { sassPlugin } = require("esbuild-sass-plugin"); const { sassPlugin } = require("esbuild-sass-plugin");
const { externalGlobalPlugin } = require("esbuild-plugin-external-global"); const { externalGlobalPlugin } = require("esbuild-plugin-external-global");
const { woff2BrowserPlugin } = require("./woff2/woff2-esbuild-plugins");
// Will be used later for treeshaking // Will be used later for treeshaking
//const fs = require("fs"); //const fs = require("fs");
@@ -45,13 +44,15 @@ const browserConfig = {
format: "esm", format: "esm",
plugins: [ plugins: [
sassPlugin(), sassPlugin(),
woff2BrowserPlugin(),
externalGlobalPlugin({ externalGlobalPlugin({
react: "React", react: "React",
"react-dom": "ReactDOM", "react-dom": "ReactDOM",
}), }),
], ],
splitting: true, splitting: true,
loader: {
".woff2": "file",
},
}; };
const createESMBrowserBuild = async () => { const createESMBrowserBuild = async () => {
// Development unminified build with source maps // Development unminified build with source maps
@@ -100,9 +101,10 @@ const rawConfig = {
entryPoints: ["index.tsx"], entryPoints: ["index.tsx"],
bundle: true, bundle: true,
format: "esm", format: "esm",
plugins: [sassPlugin(), woff2BrowserPlugin()], plugins: [sassPlugin()],
loader: { loader: {
".json": "copy", ".json": "copy",
".woff2": "file",
}, },
packages: "external", packages: "external",
}; };
+5 -5
View File
@@ -1,17 +1,17 @@
const fs = require("fs"); const fs = require("fs");
const { build } = require("esbuild"); const { build } = require("esbuild");
const { sassPlugin } = require("esbuild-sass-plugin"); const { sassPlugin } = require("esbuild-sass-plugin");
const { const { woff2ServerPlugin } = require("./woff2/woff2-esbuild-plugins");
woff2BrowserPlugin,
woff2ServerPlugin,
} = require("./woff2/woff2-esbuild-plugins");
const browserConfig = { const browserConfig = {
entryPoints: ["index.ts"], entryPoints: ["index.ts"],
bundle: true, bundle: true,
format: "esm", format: "esm",
plugins: [sassPlugin(), woff2BrowserPlugin()], plugins: [sassPlugin()],
assetNames: "assets/[name]", assetNames: "assets/[name]",
loader: {
".woff2": "file",
},
}; };
// Will be used later for treeshaking // Will be used later for treeshaking
+2 -63
View File
@@ -2,45 +2,9 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const { execSync } = require("child_process"); const { execSync } = require("child_process");
const which = require("which"); const which = require("which");
const fetch = require("node-fetch");
const wawoff = require("wawoff2"); const wawoff = require("wawoff2");
const { Font } = require("fonteditor-core"); const { Font } = require("fonteditor-core");
/**
* Custom esbuild plugin to convert url woff2 imports into a text.
* Other woff2 imports are handled by a "file" loader.
*
* @returns {import("esbuild").Plugin}
*/
module.exports.woff2BrowserPlugin = () => {
return {
name: "woff2BrowserPlugin",
setup(build) {
build.initialOptions.loader = {
".woff2": "file",
...build.initialOptions.loader,
};
build.onResolve({ filter: /^https:\/\/.+?\.woff2$/ }, (args) => {
return {
path: args.path,
namespace: "woff2BrowserPlugin",
};
});
build.onLoad(
{ filter: /.*/, namespace: "woff2BrowserPlugin" },
async (args) => {
return {
contents: args.path,
loader: "text",
};
},
);
},
};
};
/** /**
* Custom esbuild plugin to: * Custom esbuild plugin to:
* 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs) * 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs)
@@ -53,27 +17,6 @@ module.exports.woff2BrowserPlugin = () => {
* @returns {import("esbuild").Plugin} * @returns {import("esbuild").Plugin}
*/ */
module.exports.woff2ServerPlugin = (options = {}) => { module.exports.woff2ServerPlugin = (options = {}) => {
// google CDN fails time to time, so let's retry
async function fetchRetry(url, options = {}, retries = 0, delay = 1000) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Status: ${response.status}, ${await response.json()}`);
}
return response;
} catch (e) {
if (retries > 0) {
await new Promise((resolve) => setTimeout(resolve, delay));
return fetchRetry(url, options, retries - 1, delay * 2);
}
console.error(`Couldn't fetch: ${url}, error: ${e.message}`);
throw e;
}
}
return { return {
name: "woff2ServerPlugin", name: "woff2ServerPlugin",
setup(build) { setup(build) {
@@ -82,9 +25,7 @@ module.exports.woff2ServerPlugin = (options = {}) => {
const fonts = new Map(); const fonts = new Map();
build.onResolve({ filter: /\.woff2$/ }, (args) => { build.onResolve({ filter: /\.woff2$/ }, (args) => {
const resolvedPath = args.path.startsWith("http") const resolvedPath = path.resolve(args.resolveDir, args.path);
? args.path // url
: path.resolve(args.resolveDir, args.path); // absolute path
return { return {
path: resolvedPath, path: resolvedPath,
@@ -101,9 +42,7 @@ module.exports.woff2ServerPlugin = (options = {}) => {
// read local woff2 as a buffer (WARN: `readFileSync` does not work!) // read local woff2 as a buffer (WARN: `readFileSync` does not work!)
woff2Buffer = await fs.promises.readFile(args.path); woff2Buffer = await fs.promises.readFile(args.path);
} else { } else {
// fetch remote woff2 as a buffer (i.e. from a cdn) throw new Error(`Font path has to be absolute! "${args.path}"`);
const response = await fetchRetry(args.path, {}, 3);
woff2Buffer = await response.buffer();
} }
// google's brotli decompression into snft // google's brotli decompression into snft
+13 -33
View File
@@ -1,15 +1,13 @@
// `EXCALIDRAW_ASSET_PATH` as a SSOT
const OSS_FONTS_CDN = const OSS_FONTS_CDN =
"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/"; "https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
/** /**
* Custom vite plugin to convert url woff2 imports into a text. * Custom vite plugin for auto-prefixing `EXCALIDRAW_ASSET_PATH` woff2 fonts in `excalidraw-app`.
* Other woff2 imports are automatically served and resolved as a file uri.
* *
* @returns {import("vite").PluginOption} * @returns {import("vite").PluginOption}
*/ */
module.exports.woff2BrowserPlugin = () => { module.exports.woff2BrowserPlugin = () => {
// for now limited to woff2 only, might be extended to any assets in the future
const regex = /^https:\/\/.+?\.woff2$/;
let isDev; let isDev;
return { return {
@@ -18,34 +16,9 @@ module.exports.woff2BrowserPlugin = () => {
config(_, { command }) { config(_, { command }) {
isDev = command === "serve"; isDev = command === "serve";
}, },
resolveId(source) {
if (!regex.test(source)) {
return null;
}
// getting the url to the dependency tree
return source;
},
load(id) {
if (!regex.test(id)) {
return null;
}
// loading the url as string
return `export default "${id}"`;
},
// necessary for dev as vite / rollup does skips https imports in serve (~dev) mode
// aka dev mode equivalent of "export default x" above (resolveId + load)
transform(code, id) { transform(code, id) {
// treat https woff2 imports as a text // using copy / replace as fonts defined in the `.css` don't have to be manually copied over (vite/rollup does this automatically),
if (isDev && id.endsWith("/excalidraw/fonts/index.ts")) { // but at the same time can't be easily prefixed with the `EXCALIDRAW_ASSET_PATH` only for the `excalidraw-app`
return code.replaceAll(
/import\s+(\w+)\s+from\s+(["']https:\/\/.+?\.woff2["'])/g,
`const $1 = $2`,
);
}
// use CDN for Assistant
if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) { if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) {
return `/* WARN: The following content is generated during excalidraw-app build */ return `/* WARN: The following content is generated during excalidraw-app build */
@@ -90,7 +63,6 @@ module.exports.woff2BrowserPlugin = () => {
}`; }`;
} }
// using EXCALIDRAW_ASSET_PATH as a SSOT
if (!isDev && id.endsWith("excalidraw-app/index.html")) { if (!isDev && id.endsWith("excalidraw-app/index.html")) {
return code.replace( return code.replace(
"<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->", "<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->",
@@ -110,9 +82,10 @@ module.exports.woff2BrowserPlugin = () => {
type="font/woff2" type="font/woff2"
crossorigin="anonymous" crossorigin="anonymous"
/> />
<!-- For Nunito only preload the latin range, which should be good enough for now -->
<link <link
rel="preload" rel="preload"
href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2" href="${OSS_FONTS_CDN}Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg-DqUjjPte.woff2"
as="font" as="font"
type="font/woff2" type="font/woff2"
crossorigin="anonymous" crossorigin="anonymous"
@@ -124,6 +97,13 @@ module.exports.woff2BrowserPlugin = () => {
type="font/woff2" type="font/woff2"
crossorigin="anonymous" crossorigin="anonymous"
/> />
<link
rel="preload"
href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
`, `,
); );
} }

Some files were not shown because too many files have changed in this diff Show More