mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
Merge 37412cd68a
into dff69e9191
This commit is contained in:
commit
602addb3c0
16 changed files with 744 additions and 5 deletions
|
@ -121,6 +121,8 @@ import {
|
||||||
ArrowheadCrowfootIcon,
|
ArrowheadCrowfootIcon,
|
||||||
ArrowheadCrowfootOneIcon,
|
ArrowheadCrowfootOneIcon,
|
||||||
ArrowheadCrowfootOneOrManyIcon,
|
ArrowheadCrowfootOneOrManyIcon,
|
||||||
|
snapShapeEnabledIcon,
|
||||||
|
snapShapeDisabledIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
|
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
|
@ -1818,3 +1820,41 @@ export const actionChangeArrowType = register({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionToggleShapeSnap = register({
|
||||||
|
name: "toggleShapeSnap",
|
||||||
|
label: "Toggle Snap to Shape",
|
||||||
|
trackEvent: false,
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
isShapeSnapEnabled: !appState.isShapeSnapEnabled,
|
||||||
|
},
|
||||||
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t("labels.shapeSnap")}</legend>
|
||||||
|
<ButtonIconSelect
|
||||||
|
group="button"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
text: t("labels.shapeSnapDisable"),
|
||||||
|
icon: snapShapeDisabledIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
text: t("labels.shapeSnapEnable"),
|
||||||
|
icon: snapShapeEnabledIcon,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={appState.isShapeSnapEnabled}
|
||||||
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
|
@ -125,6 +125,7 @@ export type ActionName =
|
||||||
| "toggleLinearEditor"
|
| "toggleLinearEditor"
|
||||||
| "toggleEraserTool"
|
| "toggleEraserTool"
|
||||||
| "toggleHandTool"
|
| "toggleHandTool"
|
||||||
|
| "toggleShapeSnap"
|
||||||
| "selectAllElementsInFrame"
|
| "selectAllElementsInFrame"
|
||||||
| "removeAllElementsFromFrame"
|
| "removeAllElementsFromFrame"
|
||||||
| "updateFrameRendering"
|
| "updateFrameRendering"
|
||||||
|
|
|
@ -67,6 +67,7 @@ export const getDefaultAppState = (): Omit<
|
||||||
gridStep: DEFAULT_GRID_STEP,
|
gridStep: DEFAULT_GRID_STEP,
|
||||||
gridModeEnabled: false,
|
gridModeEnabled: false,
|
||||||
isBindingEnabled: true,
|
isBindingEnabled: true,
|
||||||
|
isShapeSnapEnabled: false,
|
||||||
defaultSidebarDockedPreference: false,
|
defaultSidebarDockedPreference: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
|
@ -188,6 +189,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||||
gridModeEnabled: { browser: true, export: true, server: true },
|
gridModeEnabled: { browser: true, export: true, server: true },
|
||||||
height: { browser: false, export: false, server: false },
|
height: { browser: false, export: false, server: false },
|
||||||
isBindingEnabled: { browser: false, export: false, server: false },
|
isBindingEnabled: { browser: false, export: false, server: false },
|
||||||
|
isShapeSnapEnabled: { browser: true, export: false, server: false },
|
||||||
defaultSidebarDockedPreference: {
|
defaultSidebarDockedPreference: {
|
||||||
browser: true,
|
browser: true,
|
||||||
export: false,
|
export: false,
|
||||||
|
|
|
@ -169,9 +169,12 @@ export const SelectedShapeActions = ({
|
||||||
renderAction("changeStrokeWidth")}
|
renderAction("changeStrokeWidth")}
|
||||||
|
|
||||||
{(appState.activeTool.type === "freedraw" ||
|
{(appState.activeTool.type === "freedraw" ||
|
||||||
targetElements.some((element) => element.type === "freedraw")) &&
|
targetElements.some((element) => element.type === "freedraw")) && (
|
||||||
renderAction("changeStrokeShape")}
|
<>
|
||||||
|
{renderAction("changeStrokeShape")}
|
||||||
|
{renderAction("toggleShapeSnap")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{(hasStrokeStyle(appState.activeTool.type) ||
|
{(hasStrokeStyle(appState.activeTool.type) ||
|
||||||
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||||
import { getSelectionBoxShape } from "@excalidraw/utils/shape";
|
import { getSelectionBoxShape } from "@excalidraw/utils/shape";
|
||||||
|
import { convertToShape } from "@excalidraw/utils/snapToShape";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
|
@ -9039,6 +9040,34 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.state.isShapeSnapEnabled) {
|
||||||
|
const detectedElement = convertToShape(newElement);
|
||||||
|
|
||||||
|
if (detectedElement !== newElement) {
|
||||||
|
if (detectedElement.type === "arrow") {
|
||||||
|
mutateElement(
|
||||||
|
detectedElement,
|
||||||
|
{
|
||||||
|
startArrowhead: this.state.currentItemStartArrowhead,
|
||||||
|
endArrowhead: this.state.currentItemEndArrowhead,
|
||||||
|
},
|
||||||
|
// TODO: Make arrows bind to nearby elements if possible
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scene.replaceAllElements([
|
||||||
|
...this.scene
|
||||||
|
.getElementsIncludingDeleted()
|
||||||
|
.filter((el) => el.id !== newElement.id),
|
||||||
|
detectedElement,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedElementIds: { [detectedElement.id]: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1902,6 +1902,23 @@ export const eyeClosedIcon = createIcon(
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const snapShapeEnabledIcon = createIcon(
|
||||||
|
<g stroke="currentColor" fill="none" strokeWidth="1.5">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||||
|
<circle cx="16" cy="8" r="1.5" fill="currentColor" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const snapShapeDisabledIcon = createIcon(
|
||||||
|
<g stroke="currentColor" fill="none" strokeWidth="1.5">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||||
|
<line x1="4" y1="4" x2="20" y2="20" />
|
||||||
|
<line x1="4" y1="20" x2="20" y2="4" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
export const brainIcon = createIcon(
|
export const brainIcon = createIcon(
|
||||||
<g stroke="currentColor" fill="none">
|
<g stroke="currentColor" fill="none">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
|
|
@ -103,6 +103,9 @@
|
||||||
"loadingScene": "Loading scene…",
|
"loadingScene": "Loading scene…",
|
||||||
"loadScene": "Load scene from file",
|
"loadScene": "Load scene from file",
|
||||||
"align": "Align",
|
"align": "Align",
|
||||||
|
"shapeSnap": "Snap to shapes",
|
||||||
|
"shapeSnapDisable": "Disable snap to shapes",
|
||||||
|
"shapeSnapEnable": "Enable snap to shapes",
|
||||||
"alignTop": "Align top",
|
"alignTop": "Align top",
|
||||||
"alignBottom": "Align bottom",
|
"alignBottom": "Align bottom",
|
||||||
"alignLeft": "Align left",
|
"alignLeft": "Align left",
|
||||||
|
|
|
@ -934,6 +934,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -1144,6 +1145,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -1364,6 +1366,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -1699,6 +1702,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2034,6 +2038,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2254,6 +2259,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2498,6 +2504,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2803,6 +2810,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -3176,6 +3184,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -3655,6 +3664,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -3982,6 +3992,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -4309,6 +4320,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -5590,6 +5602,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -6812,6 +6825,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -7747,6 +7761,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -8754,6 +8769,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -9743,6 +9759,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
|
|
@ -60,6 +60,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -660,6 +661,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -1168,6 +1170,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -1540,6 +1543,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -1913,6 +1917,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -2184,6 +2189,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -2624,6 +2630,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -2927,6 +2934,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -3215,6 +3223,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -3513,6 +3522,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -3803,6 +3813,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -4042,6 +4053,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -4305,6 +4317,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -4582,6 +4595,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -4817,6 +4831,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -5052,6 +5067,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -5285,6 +5301,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -5518,6 +5535,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -5781,6 +5799,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -6116,6 +6135,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -6545,6 +6565,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -6927,6 +6948,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -7250,6 +7272,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -7552,6 +7575,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -7785,6 +7809,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -8144,6 +8169,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -8503,6 +8529,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -8911,6 +8938,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -9202,6 +9230,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -9471,6 +9500,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -9739,6 +9769,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -9974,6 +10005,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -10279,6 +10311,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -10623,6 +10656,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -10862,6 +10896,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -11315,6 +11350,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -11573,6 +11609,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -11816,6 +11853,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -12061,6 +12099,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -12466,6 +12505,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -12717,6 +12757,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -12962,6 +13003,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -13207,6 +13249,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -13458,6 +13501,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -13794,6 +13838,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -13970,6 +14015,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -14262,6 +14308,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -14533,6 +14580,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -14812,6 +14860,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -14977,6 +15026,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -15675,6 +15725,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -16295,6 +16346,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -16915,6 +16967,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -17626,6 +17679,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -18374,6 +18428,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -18852,6 +18907,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -19378,6 +19434,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
@ -19838,6 +19895,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"newElement": null,
|
"newElement": null,
|
||||||
|
|
|
@ -60,6 +60,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -476,6 +477,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -883,6 +885,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -1429,6 +1432,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -1634,6 +1638,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2010,6 +2015,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2249,6 +2255,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2430,6 +2437,7 @@ exports[`regression tests > can drag element that covers another element, while
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2751,6 +2759,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -2998,6 +3007,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -3242,6 +3252,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -3473,6 +3484,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -3730,6 +3742,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -4042,6 +4055,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -4465,6 +4479,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -4749,6 +4764,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -5003,6 +5019,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -5214,6 +5231,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -5414,6 +5432,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -5797,6 +5816,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -6088,6 +6108,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -6897,6 +6918,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -7228,6 +7250,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -7505,6 +7528,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -7740,6 +7764,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -7978,6 +8003,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -8159,6 +8185,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -8340,6 +8367,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -8521,6 +8549,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -8745,6 +8774,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -8968,6 +8998,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -9163,6 +9194,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -9387,6 +9419,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -9568,6 +9601,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -9791,6 +9825,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -9972,6 +10007,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -10167,6 +10203,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -10348,6 +10385,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -10857,6 +10895,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -11135,6 +11174,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "touch",
|
"lastPointerDownWith": "touch",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -11262,6 +11302,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -11462,6 +11503,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -11774,6 +11816,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -12187,6 +12230,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -12801,6 +12845,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -12931,6 +12976,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -13516,6 +13562,7 @@ exports[`regression tests > switches from group of selected elements to another
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -13855,6 +13902,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -14121,6 +14169,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "touch",
|
"lastPointerDownWith": "touch",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -14248,6 +14297,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -14628,6 +14678,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
@ -14755,6 +14806,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
|
|
@ -53,6 +53,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
|
|
@ -333,6 +333,7 @@ export interface AppState {
|
||||||
currentHoveredFontFamily: FontFamilyValues | null;
|
currentHoveredFontFamily: FontFamilyValues | null;
|
||||||
currentItemRoundness: StrokeRoundness;
|
currentItemRoundness: StrokeRoundness;
|
||||||
currentItemArrowType: "sharp" | "round" | "elbow";
|
currentItemArrowType: "sharp" | "round" | "elbow";
|
||||||
|
isShapeSnapEnabled: boolean;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
scrollX: number;
|
scrollX: number;
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
|
@ -912,3 +913,17 @@ export type Offsets = Partial<{
|
||||||
bottom: number;
|
bottom: number;
|
||||||
left: number;
|
left: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ShapeDetectionType =
|
||||||
|
| "rectangle"
|
||||||
|
| "ellipse"
|
||||||
|
| "diamond"
|
||||||
|
| "arrow"
|
||||||
|
| "line"
|
||||||
|
| "freedraw";
|
||||||
|
|
||||||
|
export interface ShapeDetectionResult {
|
||||||
|
type: ShapeDetectionType;
|
||||||
|
points: readonly (readonly [number, number])[];
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { degreesToRadians } from "./angle";
|
import { degreesToRadians, radiansToDegrees } from "./angle";
|
||||||
import { PRECISION } from "./utils";
|
import { PRECISION } from "./utils";
|
||||||
import { vectorFromPoint, vectorScale } from "./vector";
|
import { vectorDot, vectorFromPoint, vectorScale } from "./vector";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
LocalPoint,
|
LocalPoint,
|
||||||
|
@ -230,3 +230,67 @@ export const isPointWithinBounds = <P extends GlobalPoint | LocalPoint>(
|
||||||
q[1] >= Math.min(p[1], r[1])
|
q[1] >= Math.min(p[1], r[1])
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the perpendicular distance from a point to a line segment defined by two endpoints.
|
||||||
|
*
|
||||||
|
* If the segment is of zero length, the function returns the distance from the point to the start.
|
||||||
|
*
|
||||||
|
* @typeParam P - The point type, restricted to LocalPoint or GlobalPoint.
|
||||||
|
* @param p - The point from which the perpendicular distance is measured.
|
||||||
|
* @param start - The starting point of the line segment.
|
||||||
|
* @param end - The ending point of the line segment.
|
||||||
|
* @returns The perpendicular distance from point p to the line segment defined by start and end.
|
||||||
|
*/
|
||||||
|
export const perpendicularDistance = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p: P,
|
||||||
|
start: P,
|
||||||
|
end: P,
|
||||||
|
): number => {
|
||||||
|
const dx = end[0] - start[0];
|
||||||
|
const dy = end[1] - start[1];
|
||||||
|
if (dx === 0 && dy === 0) {
|
||||||
|
return Math.hypot(p[0] - start[0], p[1] - start[1]);
|
||||||
|
}
|
||||||
|
// Equation of line distance
|
||||||
|
const numerator = Math.abs(
|
||||||
|
dy * p[0] - dx * p[1] + end[0] * start[1] - end[1] * start[0],
|
||||||
|
);
|
||||||
|
const denom = Math.hypot(dx, dy);
|
||||||
|
return numerator / denom;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Calculates the angle between three points in degrees.
|
||||||
|
* The angle is calculated at the first point (p0) using the second (p1) and third (p2) points.
|
||||||
|
* The angle is measured in degrees and is always positive.
|
||||||
|
* The function uses the dot product and the arccosine function to calculate the angle. * The result is clamped to the range [-1, 1] to avoid precision errors.
|
||||||
|
* @param p0 The first point used to form the angle.
|
||||||
|
* @param p1 The vertex point where the angle is calculated.
|
||||||
|
* @param p2 The second point used to form the angle.
|
||||||
|
* @returns The angle in degrees between the three points.
|
||||||
|
**/
|
||||||
|
export const angleBetween = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p0: P,
|
||||||
|
p1: P,
|
||||||
|
p2: P,
|
||||||
|
): Degrees => {
|
||||||
|
const v1 = vectorFromPoint(p0, p1);
|
||||||
|
const v2 = vectorFromPoint(p1, p2);
|
||||||
|
|
||||||
|
// dot and cross product
|
||||||
|
const magnitude1 = Math.hypot(v1[0], v1[1]);
|
||||||
|
const magnitude2 = Math.hypot(v2[0], v2[1]);
|
||||||
|
|
||||||
|
if (magnitude1 === 0 || magnitude2 === 0) {
|
||||||
|
return 0 as Degrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dot = vectorDot(v1, v2);
|
||||||
|
|
||||||
|
let cos = dot / (magnitude1 * magnitude2);
|
||||||
|
// Clamp cos to [-1,1] to avoid precision errors
|
||||||
|
cos = Math.max(-1, Math.min(1, cos));
|
||||||
|
const rad = Math.acos(cos) as Radians;
|
||||||
|
|
||||||
|
return radiansToDegrees(rad);
|
||||||
|
};
|
||||||
|
|
435
packages/utils/src/snapToShape.ts
Normal file
435
packages/utils/src/snapToShape.ts
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
import {
|
||||||
|
getCenterForBounds,
|
||||||
|
getCommonBoundingBox,
|
||||||
|
} from "@excalidraw/element/bounds";
|
||||||
|
import {
|
||||||
|
newArrowElement,
|
||||||
|
newElement,
|
||||||
|
newLinearElement,
|
||||||
|
} from "@excalidraw/element/newElement";
|
||||||
|
|
||||||
|
import {
|
||||||
|
angleBetween,
|
||||||
|
perpendicularDistance,
|
||||||
|
pointDistance,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
import { ROUNDNESS } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type { BoundingBox, Bounds } from "@excalidraw/element/bounds";
|
||||||
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawRectangleElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
type Shape =
|
||||||
|
| ExcalidrawRectangleElement["type"]
|
||||||
|
| ExcalidrawEllipseElement["type"]
|
||||||
|
| ExcalidrawDiamondElement["type"]
|
||||||
|
| ExcalidrawArrowElement["type"]
|
||||||
|
| ExcalidrawLinearElement["type"]
|
||||||
|
| ExcalidrawFreeDrawElement["type"];
|
||||||
|
|
||||||
|
interface ShapeRecognitionResult {
|
||||||
|
type: Shape;
|
||||||
|
simplified: readonly LocalPoint[];
|
||||||
|
boundingBox: BoundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUADRILATERAL_SIDES = 4;
|
||||||
|
const QUADRILATERAL_MIN_POINTS = 4; // RDP simplified vertices
|
||||||
|
const QUADRILATERAL_MAX_POINTS = 5; // RDP might include closing point
|
||||||
|
const ARROW_EXPECTED_POINTS = 5; // RDP simplified vertices for arrow shape
|
||||||
|
const LINE_EXPECTED_POINTS = 2; // RDP simplified vertices for line shape
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS = {
|
||||||
|
// Max distance between stroke start/end (as % of bbox diagonal) to consider closed
|
||||||
|
shapeIsClosedPercentThreshold: 20,
|
||||||
|
// Min distance (px) to consider shape closed (takes precedence if larger than %)
|
||||||
|
shapeIsClosedDistanceThreshold: 10,
|
||||||
|
// RDP simplification tolerance (% of bbox diagonal)
|
||||||
|
rdpTolerancePercent: 10,
|
||||||
|
// Arrow specific thresholds
|
||||||
|
arrowMinTipAngle: 30, // Min angle degrees for the tip
|
||||||
|
arrowMaxTipAngle: 150, // Max angle degrees for the tip
|
||||||
|
arrowHeadMaxShaftRatio: 0.8, // Max length ratio of arrowhead segment to shaft
|
||||||
|
// Quadrilateral specific thresholds
|
||||||
|
rectangleMinCornerAngle: 20, // Min deviation from 180 degrees for a valid corner
|
||||||
|
rectangleMaxCornerAngle: 160, // Max deviation from 0 degrees for a valid corner
|
||||||
|
// Angle difference (degrees) to nearest 0/90 orientation to classify as rectangle
|
||||||
|
rectangleOrientationAngleThreshold: 10,
|
||||||
|
// Max variance in radius (normalized) to consider a shape an ellipse
|
||||||
|
ellipseRadiusVarianceThreshold: 0.5,
|
||||||
|
} as const; // Use 'as const' for stricter typing of default values
|
||||||
|
|
||||||
|
// Options for shape recognition, allowing partial overrides
|
||||||
|
type ShapeRecognitionOptions = typeof DEFAULT_OPTIONS;
|
||||||
|
type PartialShapeRecognitionOptions = Partial<ShapeRecognitionOptions>;
|
||||||
|
|
||||||
|
interface Segment {
|
||||||
|
length: number;
|
||||||
|
angleDeg: number; // Angle in degrees [0, 180) representing the line's orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplify a polyline using Ramer-Douglas-Peucker algorithm.
|
||||||
|
*/
|
||||||
|
function simplifyRDP(
|
||||||
|
points: readonly LocalPoint[],
|
||||||
|
epsilon: number,
|
||||||
|
): readonly LocalPoint[] {
|
||||||
|
if (points.length < 3) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = points[0];
|
||||||
|
const last = points[points.length - 1];
|
||||||
|
let index = -1;
|
||||||
|
let maxDist = 0;
|
||||||
|
|
||||||
|
// Find the point with the maximum distance from the line segment between first and last
|
||||||
|
for (let i = 1; i < points.length - 1; i++) {
|
||||||
|
const dist = perpendicularDistance(points[i], first, last);
|
||||||
|
if (dist > maxDist) {
|
||||||
|
maxDist = dist;
|
||||||
|
index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If max distance is greater than epsilon, recursively simplify
|
||||||
|
if (maxDist > epsilon && index !== -1) {
|
||||||
|
const left = simplifyRDP(points.slice(0, index + 1), epsilon);
|
||||||
|
const right = simplifyRDP(points.slice(index), epsilon);
|
||||||
|
// Concatenate results (omit duplicate point at junction)
|
||||||
|
return left.slice(0, -1).concat(right);
|
||||||
|
}
|
||||||
|
// Not enough deviation, return straight line segment (keep only endpoints)
|
||||||
|
return [first, last];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the properties (length, angle) of segments in a polygon.
|
||||||
|
*/
|
||||||
|
function calculateSegments(vertices: readonly LocalPoint[]): Segment[] {
|
||||||
|
const segments: Segment[] = [];
|
||||||
|
const numVertices = vertices.length;
|
||||||
|
for (let i = 0; i < numVertices; i++) {
|
||||||
|
const p1 = vertices[i];
|
||||||
|
// Ensure wrapping for the last segment connecting back to the start
|
||||||
|
const p2 = vertices[(i + 1) % numVertices];
|
||||||
|
const dx = p2[0] - p1[0];
|
||||||
|
const dy = p2[1] - p1[1];
|
||||||
|
const length = Math.hypot(dx, dy);
|
||||||
|
|
||||||
|
// Calculate angle in degrees [0, 360)
|
||||||
|
let angleRad = Math.atan2(dy, dx);
|
||||||
|
if (angleRad < 0) {
|
||||||
|
angleRad += 2 * Math.PI;
|
||||||
|
}
|
||||||
|
let angleDeg = (angleRad * 180) / Math.PI;
|
||||||
|
|
||||||
|
// Normalize angle to [0, 180) for undirected line orientation
|
||||||
|
if (angleDeg >= 180) {
|
||||||
|
angleDeg -= 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push({ length, angleDeg });
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the shape is closed based on the distance between start and end points.
|
||||||
|
*/
|
||||||
|
function isShapeClosed(
|
||||||
|
points: readonly LocalPoint[],
|
||||||
|
boundingBoxDiagonal: number,
|
||||||
|
options: ShapeRecognitionOptions,
|
||||||
|
): boolean {
|
||||||
|
const start = points[0];
|
||||||
|
const end = points[points.length - 1];
|
||||||
|
const closedDist = pointDistance(start, end);
|
||||||
|
const closedThreshold = Math.max(
|
||||||
|
options.shapeIsClosedDistanceThreshold,
|
||||||
|
boundingBoxDiagonal * (options.shapeIsClosedPercentThreshold / 100),
|
||||||
|
);
|
||||||
|
return closedDist < closedThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a quadrilateral is likely axis-aligned based on its segment angles.
|
||||||
|
*/
|
||||||
|
function isAxisAligned(
|
||||||
|
segments: Segment[],
|
||||||
|
orientationThreshold: number,
|
||||||
|
): boolean {
|
||||||
|
return segments.some((seg) => {
|
||||||
|
const angle = seg.angleDeg;
|
||||||
|
// Distance to horizontal (0 or 180 degrees)
|
||||||
|
const distToHoriz = Math.min(angle, 180 - angle);
|
||||||
|
// Distance to vertical (90 degrees)
|
||||||
|
const distToVert = Math.abs(angle - 90);
|
||||||
|
return (
|
||||||
|
distToHoriz < orientationThreshold || distToVert < orientationThreshold
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the variance of the distance from points to a center point.
|
||||||
|
* Returns a normalized variance value (0 = perfectly round).
|
||||||
|
*/
|
||||||
|
function calculateRadiusVariance(
|
||||||
|
points: readonly LocalPoint[],
|
||||||
|
boundingBox: BoundingBox,
|
||||||
|
): number {
|
||||||
|
if (points.length === 0) {
|
||||||
|
return 0; // Or handle as an error/special case
|
||||||
|
}
|
||||||
|
|
||||||
|
const [cx, cy] = getCenterForBounds([
|
||||||
|
boundingBox.minX,
|
||||||
|
boundingBox.minY,
|
||||||
|
boundingBox.maxX,
|
||||||
|
boundingBox.maxY,
|
||||||
|
] as Bounds);
|
||||||
|
|
||||||
|
let totalDist = 0;
|
||||||
|
let maxDist = 0;
|
||||||
|
let minDist = Infinity;
|
||||||
|
|
||||||
|
for (const p of points) {
|
||||||
|
const d = Math.hypot(p[0] - cx, p[1] - cy);
|
||||||
|
totalDist += d;
|
||||||
|
maxDist = Math.max(maxDist, d);
|
||||||
|
minDist = Math.min(minDist, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avgDist = totalDist / points.length;
|
||||||
|
|
||||||
|
// Avoid division by zero if avgDist is 0 (e.g., all points are at the center)
|
||||||
|
if (avgDist === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const radiusVariance = (maxDist - minDist) / avgDist;
|
||||||
|
return radiusVariance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks if the points form a straight line segment. */
|
||||||
|
function checkLine(
|
||||||
|
points: readonly LocalPoint[],
|
||||||
|
isClosed: boolean,
|
||||||
|
): Shape | null {
|
||||||
|
if (!isClosed && points.length === LINE_EXPECTED_POINTS) {
|
||||||
|
return "line";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks if the points form an arrow shape. */
|
||||||
|
function checkArrow(
|
||||||
|
points: readonly LocalPoint[],
|
||||||
|
isClosed: boolean,
|
||||||
|
options: ShapeRecognitionOptions,
|
||||||
|
): Shape | null {
|
||||||
|
if (isClosed || points.length !== ARROW_EXPECTED_POINTS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shaftStart = points[0];
|
||||||
|
const shaftEnd = points[1]; // Assuming RDP simplifies shaft to 2 points
|
||||||
|
const arrowBase = points[2];
|
||||||
|
const arrowTip = points[3];
|
||||||
|
const arrowTailEnd = points[4];
|
||||||
|
|
||||||
|
const tipAngle = angleBetween(arrowTip, arrowBase, arrowTailEnd);
|
||||||
|
|
||||||
|
if (
|
||||||
|
tipAngle <= options.arrowMinTipAngle ||
|
||||||
|
tipAngle >= options.arrowMaxTipAngle
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headSegment1Len = pointDistance(arrowBase, arrowTip);
|
||||||
|
const headSegment2Len = pointDistance(arrowTip, arrowTailEnd);
|
||||||
|
const shaftLen = pointDistance(shaftStart, shaftEnd); // Approx shaft length
|
||||||
|
|
||||||
|
// Heuristic: Arrowhead segments should be significantly shorter than the shaft
|
||||||
|
const isHeadShortEnough =
|
||||||
|
headSegment1Len < shaftLen * options.arrowHeadMaxShaftRatio &&
|
||||||
|
headSegment2Len < shaftLen * options.arrowHeadMaxShaftRatio;
|
||||||
|
|
||||||
|
return isHeadShortEnough ? "arrow" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks if the points form a rectangle or diamond shape. */
|
||||||
|
function checkQuadrilateral(
|
||||||
|
points: readonly LocalPoint[],
|
||||||
|
isClosed: boolean,
|
||||||
|
options: ShapeRecognitionOptions,
|
||||||
|
): Shape | null {
|
||||||
|
if (
|
||||||
|
!isClosed ||
|
||||||
|
points.length < QUADRILATERAL_MIN_POINTS ||
|
||||||
|
points.length > QUADRILATERAL_MAX_POINTS
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the first 4 points as vertices (RDP might add 5th closing point)
|
||||||
|
const vertices = points.slice(0, QUADRILATERAL_SIDES);
|
||||||
|
// console.log("Vertices (Quad Check):", vertices);
|
||||||
|
|
||||||
|
// Calculate internal angles
|
||||||
|
const angles: number[] = [];
|
||||||
|
for (let i = 0; i < QUADRILATERAL_SIDES; i++) {
|
||||||
|
const p1 = vertices[i];
|
||||||
|
const p2 = vertices[(i + 1) % QUADRILATERAL_SIDES];
|
||||||
|
const p3 = vertices[(i + 2) % QUADRILATERAL_SIDES];
|
||||||
|
|
||||||
|
angles.push(angleBetween(p1, p2, p3));
|
||||||
|
}
|
||||||
|
|
||||||
|
const allCornersAreValid = angles.every(
|
||||||
|
(a) =>
|
||||||
|
a > options.rectangleMinCornerAngle &&
|
||||||
|
a < options.rectangleMaxCornerAngle,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allCornersAreValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = calculateSegments(vertices);
|
||||||
|
|
||||||
|
if (isAxisAligned(segments, options.rectangleOrientationAngleThreshold)) {
|
||||||
|
return "rectangle";
|
||||||
|
}
|
||||||
|
// Not axis-aligned, but quadrilateral => classify as diamond
|
||||||
|
return "diamond";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks if the points form an ellipse shape. */
|
||||||
|
function checkEllipse(
|
||||||
|
points: readonly LocalPoint[],
|
||||||
|
isClosed: boolean,
|
||||||
|
boundingBox: BoundingBox,
|
||||||
|
options: ShapeRecognitionOptions,
|
||||||
|
): Shape | null {
|
||||||
|
if (!isClosed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need a minimum number of points for it to be an ellipse
|
||||||
|
if (points.length < QUADRILATERAL_MAX_POINTS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const radiusVariance = calculateRadiusVariance(points, boundingBox);
|
||||||
|
|
||||||
|
return radiusVariance < options.ellipseRadiusVarianceThreshold
|
||||||
|
? "ellipse"
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recognizes common shapes from free-draw input points.
|
||||||
|
* @param element The freedraw element to analyze.
|
||||||
|
* @param opts Optional overrides for recognition thresholds.
|
||||||
|
* @returns Information about the recognized shape.
|
||||||
|
*/
|
||||||
|
export const recognizeShape = (
|
||||||
|
element: ExcalidrawFreeDrawElement,
|
||||||
|
opts: PartialShapeRecognitionOptions = {},
|
||||||
|
): ShapeRecognitionResult => {
|
||||||
|
const options = { ...DEFAULT_OPTIONS, ...opts };
|
||||||
|
const { points } = element;
|
||||||
|
const boundingBox = getCommonBoundingBox([element]);
|
||||||
|
|
||||||
|
// Need at least a few points to recognize a shape
|
||||||
|
if (!points || points.length < 3) {
|
||||||
|
return { type: "freedraw", simplified: points, boundingBox };
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundingBoxDiagonal = Math.hypot(boundingBox.width, boundingBox.height);
|
||||||
|
const rdpTolerance =
|
||||||
|
boundingBoxDiagonal * (options.rdpTolerancePercent / 100);
|
||||||
|
const simplifiedPoints = simplifyRDP(points, rdpTolerance);
|
||||||
|
|
||||||
|
const isClosed = isShapeClosed(
|
||||||
|
simplifiedPoints,
|
||||||
|
boundingBoxDiagonal,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Shape check order matters here ---
|
||||||
|
const recognizedType: Shape =
|
||||||
|
checkLine(simplifiedPoints, isClosed) ??
|
||||||
|
checkArrow(simplifiedPoints, isClosed, options) ??
|
||||||
|
checkQuadrilateral(simplifiedPoints, isClosed, options) ??
|
||||||
|
checkEllipse(simplifiedPoints, isClosed, boundingBox, options) ??
|
||||||
|
"freedraw"; // Default if no other shape matches
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: recognizedType,
|
||||||
|
simplified: simplifiedPoints,
|
||||||
|
boundingBox,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a freedraw element to the detected shape
|
||||||
|
*/
|
||||||
|
export const convertToShape = (
|
||||||
|
freeDrawElement: ExcalidrawFreeDrawElement,
|
||||||
|
): ExcalidrawElement => {
|
||||||
|
const recognizedShape = recognizeShape(freeDrawElement);
|
||||||
|
|
||||||
|
switch (recognizedShape.type) {
|
||||||
|
case "rectangle":
|
||||||
|
case "diamond":
|
||||||
|
case "ellipse": {
|
||||||
|
return newElement({
|
||||||
|
...freeDrawElement,
|
||||||
|
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
|
||||||
|
type: recognizedShape.type,
|
||||||
|
x: recognizedShape.boundingBox.minX,
|
||||||
|
y: recognizedShape.boundingBox.minY,
|
||||||
|
width: recognizedShape.boundingBox.width!,
|
||||||
|
height: recognizedShape.boundingBox.height!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "arrow": {
|
||||||
|
return newArrowElement({
|
||||||
|
...freeDrawElement,
|
||||||
|
type: recognizedShape.type,
|
||||||
|
points: [
|
||||||
|
recognizedShape.simplified[0],
|
||||||
|
recognizedShape.simplified[recognizedShape.simplified.length - 2],
|
||||||
|
],
|
||||||
|
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "line": {
|
||||||
|
return newLinearElement({
|
||||||
|
...freeDrawElement,
|
||||||
|
type: recognizedShape.type,
|
||||||
|
points: [
|
||||||
|
recognizedShape.simplified[0],
|
||||||
|
recognizedShape.simplified[recognizedShape.simplified.length - 1],
|
||||||
|
],
|
||||||
|
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return freeDrawElement;
|
||||||
|
}
|
||||||
|
};
|
|
@ -60,6 +60,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
|
|
@ -53,6 +53,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
||||||
"isLoading": false,
|
"isLoading": false,
|
||||||
"isResizing": false,
|
"isResizing": false,
|
||||||
"isRotating": false,
|
"isRotating": false,
|
||||||
|
"isShapeSnapEnabled": false,
|
||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
|
Loading…
Add table
Reference in a new issue