feat: element alignments - snapping (#6256)
Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
4765f5536e
commit
4c35eba72d
@ -15,6 +15,7 @@ export const actionToggleGridMode = register({
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
||||||
|
objectsSnapModeEnabled: false,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
28
src/actions/actionToggleObjectsSnapMode.tsx
Normal file
28
src/actions/actionToggleObjectsSnapMode.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { CODES, KEYS } from "../keys";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionToggleObjectsSnapMode = register({
|
||||||
|
name: "objectsSnapMode",
|
||||||
|
viewMode: true,
|
||||||
|
trackEvent: {
|
||||||
|
category: "canvas",
|
||||||
|
predicate: (appState) => !appState.objectsSnapModeEnabled,
|
||||||
|
},
|
||||||
|
perform(elements, appState) {
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
objectsSnapModeEnabled: !this.checked!(appState),
|
||||||
|
gridSize: null,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
checked: (appState) => appState.objectsSnapModeEnabled,
|
||||||
|
predicate: (elements, appState, appProps) => {
|
||||||
|
return typeof appProps.objectsSnapModeEnabled === "undefined";
|
||||||
|
},
|
||||||
|
contextItemLabel: "buttons.objectsSnapMode",
|
||||||
|
keyTest: (event) =>
|
||||||
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.S,
|
||||||
|
});
|
@ -80,6 +80,7 @@ export {
|
|||||||
|
|
||||||
export { actionToggleGridMode } from "./actionToggleGridMode";
|
export { actionToggleGridMode } from "./actionToggleGridMode";
|
||||||
export { actionToggleZenMode } from "./actionToggleZenMode";
|
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||||
|
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
|
||||||
|
|
||||||
export { actionToggleStats } from "./actionToggleStats";
|
export { actionToggleStats } from "./actionToggleStats";
|
||||||
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
||||||
|
@ -28,6 +28,7 @@ export type ShortcutName =
|
|||||||
| "ungroup"
|
| "ungroup"
|
||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
|
| "objectsSnapMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "addToLibrary"
|
| "addToLibrary"
|
||||||
| "viewMode"
|
| "viewMode"
|
||||||
@ -74,6 +75,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
||||||
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
||||||
zenMode: [getShortcutKey("Alt+Z")],
|
zenMode: [getShortcutKey("Alt+Z")],
|
||||||
|
objectsSnapMode: [getShortcutKey("Alt+S")],
|
||||||
stats: [getShortcutKey("Alt+/")],
|
stats: [getShortcutKey("Alt+/")],
|
||||||
addToLibrary: [],
|
addToLibrary: [],
|
||||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||||
|
@ -51,6 +51,7 @@ export type ActionName =
|
|||||||
| "pasteStyles"
|
| "pasteStyles"
|
||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
|
| "objectsSnapMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "changeStrokeColor"
|
| "changeStrokeColor"
|
||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
|
@ -99,6 +99,12 @@ export const getDefaultAppState = (): Omit<
|
|||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
selectedLinearElement: null,
|
selectedLinearElement: null,
|
||||||
|
snapLines: [],
|
||||||
|
originSnapOffset: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
objectsSnapModeEnabled: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -206,6 +212,9 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
pendingImageElementId: { browser: false, export: false, server: false },
|
pendingImageElementId: { browser: false, export: false, server: false },
|
||||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||||
selectedLinearElement: { browser: true, export: false, server: false },
|
selectedLinearElement: { browser: true, export: false, server: false },
|
||||||
|
snapLines: { browser: false, export: false, server: false },
|
||||||
|
originSnapOffset: { browser: false, export: false, server: false },
|
||||||
|
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const _clearAppStateForStorage = <
|
const _clearAppStateForStorage = <
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
actionLink,
|
actionLink,
|
||||||
actionToggleElementLock,
|
actionToggleElementLock,
|
||||||
actionToggleLinearEditor,
|
actionToggleLinearEditor,
|
||||||
|
actionToggleObjectsSnapMode,
|
||||||
} from "../actions";
|
} from "../actions";
|
||||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
@ -228,6 +229,7 @@ import {
|
|||||||
FrameNameBoundsCache,
|
FrameNameBoundsCache,
|
||||||
SidebarName,
|
SidebarName,
|
||||||
SidebarTabName,
|
SidebarTabName,
|
||||||
|
KeyboardModifiersObject,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
@ -342,6 +344,17 @@ import {
|
|||||||
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
||||||
import { jotaiStore } from "../jotai";
|
import { jotaiStore } from "../jotai";
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
|
import {
|
||||||
|
getSnapLinesAtPointer,
|
||||||
|
snapDraggedElements,
|
||||||
|
isActiveToolNonLinearSnappable,
|
||||||
|
snapNewElement,
|
||||||
|
snapResizingElements,
|
||||||
|
isSnappingEnabled,
|
||||||
|
getVisibleGaps,
|
||||||
|
getReferenceSnapPoints,
|
||||||
|
SnapCache,
|
||||||
|
} from "../snapping";
|
||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||||
@ -490,6 +503,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
viewModeEnabled = false,
|
viewModeEnabled = false,
|
||||||
zenModeEnabled = false,
|
zenModeEnabled = false,
|
||||||
gridModeEnabled = false,
|
gridModeEnabled = false,
|
||||||
|
objectsSnapModeEnabled = false,
|
||||||
theme = defaultAppState.theme,
|
theme = defaultAppState.theme,
|
||||||
name = defaultAppState.name,
|
name = defaultAppState.name,
|
||||||
} = props;
|
} = props;
|
||||||
@ -500,6 +514,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
...this.getCanvasOffsets(),
|
...this.getCanvasOffsets(),
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
|
objectsSnapModeEnabled,
|
||||||
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
||||||
name,
|
name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
@ -1722,6 +1737,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.destroy();
|
this.scene.destroy();
|
||||||
this.library.destroy();
|
this.library.destroy();
|
||||||
ShapeCache.destroy();
|
ShapeCache.destroy();
|
||||||
|
SnapCache.destroy();
|
||||||
clearTimeout(touchTimeout);
|
clearTimeout(touchTimeout);
|
||||||
isSomeElementSelected.clearCache();
|
isSomeElementSelected.clearCache();
|
||||||
selectGroupsForSelectedElements.clearCache();
|
selectGroupsForSelectedElements.clearCache();
|
||||||
@ -3120,15 +3136,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.onImageAction();
|
this.onImageAction();
|
||||||
}
|
}
|
||||||
if (nextActiveTool.type !== "selection") {
|
if (nextActiveTool.type !== "selection") {
|
||||||
this.setState({
|
this.setState((prevState) => ({
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
|
snapLines: [],
|
||||||
|
originSnapOffset: null,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
snapLines: [],
|
||||||
|
originSnapOffset: null,
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
this.setState({ activeTool: nextActiveTool, activeEmbeddable: null });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -3865,6 +3887,30 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
||||||
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.state.draggingElement &&
|
||||||
|
isActiveToolNonLinearSnappable(this.state.activeTool.type)
|
||||||
|
) {
|
||||||
|
const { originOffset, snapLines } = getSnapLinesAtPointer(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.state,
|
||||||
|
{
|
||||||
|
x: scenePointerX,
|
||||||
|
y: scenePointerY,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
snapLines,
|
||||||
|
originSnapOffset: originOffset,
|
||||||
|
});
|
||||||
|
} else if (!this.state.draggingElement) {
|
||||||
|
this.setState({
|
||||||
|
snapLines: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.editingLinearElement &&
|
this.state.editingLinearElement &&
|
||||||
!this.state.editingLinearElement.isDragging
|
!this.state.editingLinearElement.isDragging
|
||||||
@ -4335,6 +4381,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ contextMenu: null });
|
this.setState({ contextMenu: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.snapLines) {
|
||||||
|
this.setAppState({ snapLines: [] });
|
||||||
|
}
|
||||||
|
|
||||||
this.updateGestureOnPointerDown(event);
|
this.updateGestureOnPointerDown(event);
|
||||||
|
|
||||||
// if dragging element is freedraw and another pointerdown event occurs
|
// if dragging element is freedraw and another pointerdown event occurs
|
||||||
@ -5616,6 +5666,52 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private maybeCacheReferenceSnapPoints(
|
||||||
|
event: KeyboardModifiersObject,
|
||||||
|
selectedElements: ExcalidrawElement[],
|
||||||
|
recomputeAnyways: boolean = false,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
isSnappingEnabled({
|
||||||
|
event,
|
||||||
|
appState: this.state,
|
||||||
|
selectedElements,
|
||||||
|
}) &&
|
||||||
|
(recomputeAnyways || !SnapCache.getReferenceSnapPoints())
|
||||||
|
) {
|
||||||
|
SnapCache.setReferenceSnapPoints(
|
||||||
|
getReferenceSnapPoints(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
selectedElements,
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private maybeCacheVisibleGaps(
|
||||||
|
event: KeyboardModifiersObject,
|
||||||
|
selectedElements: ExcalidrawElement[],
|
||||||
|
recomputeAnyways: boolean = false,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
isSnappingEnabled({
|
||||||
|
event,
|
||||||
|
appState: this.state,
|
||||||
|
selectedElements,
|
||||||
|
}) &&
|
||||||
|
(recomputeAnyways || !SnapCache.getVisibleGaps())
|
||||||
|
) {
|
||||||
|
SnapCache.setVisibleGaps(
|
||||||
|
getVisibleGaps(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
selectedElements,
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onKeyDownFromPointerDownHandler(
|
private onKeyDownFromPointerDownHandler(
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): (event: KeyboardEvent) => void {
|
): (event: KeyboardEvent) => void {
|
||||||
@ -5845,33 +5941,62 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!this.state.editingElement &&
|
!this.state.editingElement &&
|
||||||
this.state.activeEmbeddable?.state !== "active"
|
this.state.activeEmbeddable?.state !== "active"
|
||||||
) {
|
) {
|
||||||
const [dragX, dragY] = getGridPoint(
|
const dragOffset = {
|
||||||
pointerCoords.x - pointerDownState.drag.offset.x,
|
x: pointerCoords.x - pointerDownState.origin.x,
|
||||||
pointerCoords.y - pointerDownState.drag.offset.y,
|
y: pointerCoords.y - pointerDownState.origin.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const [dragDistanceX, dragDistanceY] = [
|
const originalElements = [
|
||||||
Math.abs(pointerCoords.x - pointerDownState.origin.x),
|
...pointerDownState.originalElements.values(),
|
||||||
Math.abs(pointerCoords.y - pointerDownState.origin.y),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// We only drag in one direction if shift is pressed
|
// We only drag in one direction if shift is pressed
|
||||||
const lockDirection = event.shiftKey;
|
const lockDirection = event.shiftKey;
|
||||||
|
|
||||||
|
if (lockDirection) {
|
||||||
|
const distanceX = Math.abs(dragOffset.x);
|
||||||
|
const distanceY = Math.abs(dragOffset.y);
|
||||||
|
|
||||||
|
const lockX = lockDirection && distanceX < distanceY;
|
||||||
|
const lockY = lockDirection && distanceX > distanceY;
|
||||||
|
|
||||||
|
if (lockX) {
|
||||||
|
dragOffset.x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lockY) {
|
||||||
|
dragOffset.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap cache *must* be synchronously popuplated before initial drag,
|
||||||
|
// otherwise the first drag even will not snap, causing a jump before
|
||||||
|
// it snaps to its position if previously snapped already.
|
||||||
|
this.maybeCacheVisibleGaps(event, selectedElements);
|
||||||
|
this.maybeCacheReferenceSnapPoints(event, selectedElements);
|
||||||
|
|
||||||
|
const { snapOffset, snapLines } = snapDraggedElements(
|
||||||
|
getSelectedElements(originalElements, this.state),
|
||||||
|
dragOffset,
|
||||||
|
this.state,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setState({ snapLines });
|
||||||
|
|
||||||
// when we're editing the name of a frame, we want the user to be
|
// when we're editing the name of a frame, we want the user to be
|
||||||
// able to select and interact with the text input
|
// able to select and interact with the text input
|
||||||
!this.state.editingFrame &&
|
!this.state.editingFrame &&
|
||||||
dragSelectedElements(
|
dragSelectedElements(
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
dragX,
|
dragOffset,
|
||||||
dragY,
|
|
||||||
lockDirection,
|
|
||||||
dragDistanceX,
|
|
||||||
dragDistanceY,
|
|
||||||
this.state,
|
this.state,
|
||||||
this.scene,
|
this.scene,
|
||||||
|
snapOffset,
|
||||||
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.maybeSuggestBindingForAll(selectedElements);
|
this.maybeSuggestBindingForAll(selectedElements);
|
||||||
|
|
||||||
// We duplicate the selected element if alt is pressed on pointer move
|
// We duplicate the selected element if alt is pressed on pointer move
|
||||||
@ -5912,15 +6037,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
groupIdMap,
|
groupIdMap,
|
||||||
element,
|
element,
|
||||||
);
|
);
|
||||||
const [originDragX, originDragY] = getGridPoint(
|
const origElement = pointerDownState.originalElements.get(
|
||||||
pointerDownState.origin.x - pointerDownState.drag.offset.x,
|
element.id,
|
||||||
pointerDownState.origin.y - pointerDownState.drag.offset.y,
|
)!;
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
|
||||||
);
|
|
||||||
mutateElement(duplicatedElement, {
|
mutateElement(duplicatedElement, {
|
||||||
x: duplicatedElement.x + (originDragX - dragX),
|
x: origElement.x,
|
||||||
y: duplicatedElement.y + (originDragY - dragY),
|
y: origElement.y,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// put duplicated element to pointerDownState.originalElements
|
||||||
|
// so that we can snap to the duplicated element without releasing
|
||||||
|
pointerDownState.originalElements.set(
|
||||||
|
duplicatedElement.id,
|
||||||
|
duplicatedElement,
|
||||||
|
);
|
||||||
|
|
||||||
nextElements.push(duplicatedElement);
|
nextElements.push(duplicatedElement);
|
||||||
elementsToAppend.push(element);
|
elementsToAppend.push(element);
|
||||||
oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
|
oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
|
||||||
@ -5946,6 +6077,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
oldIdToDuplicatedId,
|
oldIdToDuplicatedId,
|
||||||
);
|
);
|
||||||
this.scene.replaceAllElements(nextSceneElements);
|
this.scene.replaceAllElements(nextSceneElements);
|
||||||
|
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||||
|
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -6162,6 +6295,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isResizing,
|
isResizing,
|
||||||
isRotating,
|
isRotating,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isRotating: false,
|
isRotating: false,
|
||||||
@ -6176,8 +6310,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
multiElement || isTextElement(this.state.editingElement)
|
multiElement || isTextElement(this.state.editingElement)
|
||||||
? this.state.editingElement
|
? this.state.editingElement
|
||||||
: null,
|
: null,
|
||||||
|
snapLines: [],
|
||||||
|
|
||||||
|
originSnapOffset: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SnapCache.setReferenceSnapPoints(null);
|
||||||
|
SnapCache.setVisibleGaps(null);
|
||||||
|
|
||||||
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -7705,7 +7845,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [gridX, gridY] = getGridPoint(
|
let [gridX, gridY] = getGridPoint(
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||||
@ -7719,6 +7859,33 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
? image.width / image.height
|
? image.width / image.height
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
this.maybeCacheReferenceSnapPoints(event, [draggingElement]);
|
||||||
|
|
||||||
|
const { snapOffset, snapLines } = snapNewElement(
|
||||||
|
draggingElement,
|
||||||
|
this.state,
|
||||||
|
event,
|
||||||
|
{
|
||||||
|
x:
|
||||||
|
pointerDownState.originInGrid.x +
|
||||||
|
(this.state.originSnapOffset?.x ?? 0),
|
||||||
|
y:
|
||||||
|
pointerDownState.originInGrid.y +
|
||||||
|
(this.state.originSnapOffset?.y ?? 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: gridX - pointerDownState.originInGrid.x,
|
||||||
|
y: gridY - pointerDownState.originInGrid.y,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
gridX += snapOffset.x;
|
||||||
|
gridY += snapOffset.y;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
snapLines,
|
||||||
|
});
|
||||||
|
|
||||||
dragNewElement(
|
dragNewElement(
|
||||||
draggingElement,
|
draggingElement,
|
||||||
this.state.activeTool.type,
|
this.state.activeTool.type,
|
||||||
@ -7733,6 +7900,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
: shouldMaintainAspectRatio(event),
|
: shouldMaintainAspectRatio(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
|
this.state.originSnapOffset,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.maybeSuggestBindingForAll([draggingElement]);
|
this.maybeSuggestBindingForAll([draggingElement]);
|
||||||
@ -7774,7 +7942,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
});
|
});
|
||||||
const pointerCoords = pointerDownState.lastCoords;
|
const pointerCoords = pointerDownState.lastCoords;
|
||||||
const [resizeX, resizeY] = getGridPoint(
|
let [resizeX, resizeY] = getGridPoint(
|
||||||
pointerCoords.x - pointerDownState.resize.offset.x,
|
pointerCoords.x - pointerDownState.resize.offset.x,
|
||||||
pointerCoords.y - pointerDownState.resize.offset.y,
|
pointerCoords.y - pointerDownState.resize.offset.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||||
@ -7802,6 +7970,41 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// check needed for avoiding flickering when a key gets pressed
|
||||||
|
// during dragging
|
||||||
|
if (!this.state.selectedElementsAreBeingDragged) {
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
pointerCoords.x,
|
||||||
|
pointerCoords.y,
|
||||||
|
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dragOffset = {
|
||||||
|
x: gridX - pointerDownState.originInGrid.x,
|
||||||
|
y: gridY - pointerDownState.originInGrid.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalElements = [...pointerDownState.originalElements.values()];
|
||||||
|
|
||||||
|
this.maybeCacheReferenceSnapPoints(event, selectedElements);
|
||||||
|
|
||||||
|
const { snapOffset, snapLines } = snapResizingElements(
|
||||||
|
selectedElements,
|
||||||
|
getSelectedElements(originalElements, this.state),
|
||||||
|
this.state,
|
||||||
|
event,
|
||||||
|
dragOffset,
|
||||||
|
transformHandleType,
|
||||||
|
);
|
||||||
|
|
||||||
|
resizeX += snapOffset.x;
|
||||||
|
resizeY += snapOffset.y;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
snapLines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
transformElements(
|
transformElements(
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
@ -7817,6 +8020,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
resizeY,
|
resizeY,
|
||||||
pointerDownState.resize.center.x,
|
pointerDownState.resize.center.x,
|
||||||
pointerDownState.resize.center.y,
|
pointerDownState.resize.center.y,
|
||||||
|
this.state,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.maybeSuggestBindingForAll(selectedElements);
|
this.maybeSuggestBindingForAll(selectedElements);
|
||||||
@ -7904,6 +8108,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionUnlockAllElements,
|
actionUnlockAllElements,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionToggleGridMode,
|
actionToggleGridMode,
|
||||||
|
actionToggleObjectsSnapMode,
|
||||||
actionToggleZenMode,
|
actionToggleZenMode,
|
||||||
actionToggleViewMode,
|
actionToggleViewMode,
|
||||||
actionToggleStats,
|
actionToggleStats,
|
||||||
|
@ -258,6 +258,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("buttons.zenMode")}
|
label={t("buttons.zenMode")}
|
||||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("buttons.objectsSnapMode")}
|
||||||
|
shortcuts={[getShortcutKey("Alt+S")]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.showGrid")}
|
label={t("labels.showGrid")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
|
||||||
|
@ -193,6 +193,8 @@ const getRelevantAppStateProps = (
|
|||||||
showHyperlinkPopup: appState.showHyperlinkPopup,
|
showHyperlinkPopup: appState.showHyperlinkPopup,
|
||||||
collaborators: appState.collaborators, // Necessary for collab. sessions
|
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||||
activeEmbeddable: appState.activeEmbeddable,
|
activeEmbeddable: appState.activeEmbeddable,
|
||||||
|
snapLines: appState.snapLines,
|
||||||
|
zenModeEnabled: appState.zenModeEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
@ -158,7 +158,7 @@ export const getElementAbsoluteCoords = (
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* for a given element, `getElementLineSegments` returns line segments
|
* for a given element, `getElementLineSegments` returns line segments
|
||||||
* that can be used for visual collision detection (useful for frames)
|
* that can be used for visual collision detection (useful for frames)
|
||||||
* as opposed to bounding box collision detection
|
* as opposed to bounding box collision detection
|
||||||
@ -674,6 +674,19 @@ export const getCommonBounds = (
|
|||||||
return [minX, minY, maxX, maxY];
|
return [minX, minY, maxX, maxY];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDraggedElementsBounds = (
|
||||||
|
elements: ExcalidrawElement[],
|
||||||
|
dragOffset: { x: number; y: number },
|
||||||
|
) => {
|
||||||
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
return [
|
||||||
|
minX + dragOffset.x,
|
||||||
|
minY + dragOffset.y,
|
||||||
|
maxX + dragOffset.x,
|
||||||
|
maxY + dragOffset.y,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
export const getResizedElementAbsoluteCoords = (
|
export const getResizedElementAbsoluteCoords = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
|
@ -6,23 +6,22 @@ import { NonDeletedExcalidrawElement } from "./types";
|
|||||||
import { AppState, PointerDownState } from "../types";
|
import { AppState, PointerDownState } from "../types";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { isSelectedViaGroup } from "../groups";
|
import { isSelectedViaGroup } from "../groups";
|
||||||
|
import { getGridPoint } from "../math";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isFrameElement } from "./typeChecks";
|
import { isFrameElement } from "./typeChecks";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
pointerX: number,
|
offset: { x: number; y: number },
|
||||||
pointerY: number,
|
|
||||||
lockDirection: boolean = false,
|
|
||||||
distanceX: number = 0,
|
|
||||||
distanceY: number = 0,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
snapOffset: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
},
|
||||||
|
gridSize: AppState["gridSize"],
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1] = getCommonBounds(selectedElements);
|
|
||||||
const offset = { x: pointerX - x1, y: pointerY - y1 };
|
|
||||||
|
|
||||||
// we do not want a frame and its elements to be selected at the same time
|
// we do not want a frame and its elements to be selected at the same time
|
||||||
// but when it happens (due to some bug), we want to avoid updating element
|
// but when it happens (due to some bug), we want to avoid updating element
|
||||||
// in the frame twice, hence the use of set
|
// in the frame twice, hence the use of set
|
||||||
@ -44,12 +43,11 @@ export const dragSelectedElements = (
|
|||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(
|
updateElementCoords(
|
||||||
lockDirection,
|
|
||||||
distanceX,
|
|
||||||
distanceY,
|
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
element,
|
element,
|
||||||
offset,
|
offset,
|
||||||
|
snapOffset,
|
||||||
|
gridSize,
|
||||||
);
|
);
|
||||||
// update coords of bound text only if we're dragging the container directly
|
// update coords of bound text only if we're dragging the container directly
|
||||||
// (we don't drag the group that it's part of)
|
// (we don't drag the group that it's part of)
|
||||||
@ -69,12 +67,11 @@ export const dragSelectedElements = (
|
|||||||
(!textElement.frameId || !frames.includes(textElement.frameId))
|
(!textElement.frameId || !frames.includes(textElement.frameId))
|
||||||
) {
|
) {
|
||||||
updateElementCoords(
|
updateElementCoords(
|
||||||
lockDirection,
|
|
||||||
distanceX,
|
|
||||||
distanceY,
|
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
textElement,
|
textElement,
|
||||||
offset,
|
offset,
|
||||||
|
snapOffset,
|
||||||
|
gridSize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,31 +82,40 @@ export const dragSelectedElements = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateElementCoords = (
|
const updateElementCoords = (
|
||||||
lockDirection: boolean,
|
|
||||||
distanceX: number,
|
|
||||||
distanceY: number,
|
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
offset: { x: number; y: number },
|
dragOffset: { x: number; y: number },
|
||||||
|
snapOffset: { x: number; y: number },
|
||||||
|
gridSize: AppState["gridSize"],
|
||||||
) => {
|
) => {
|
||||||
let x: number;
|
const originalElement =
|
||||||
let y: number;
|
pointerDownState.originalElements.get(element.id) ?? element;
|
||||||
if (lockDirection) {
|
|
||||||
const lockX = lockDirection && distanceX < distanceY;
|
let nextX = originalElement.x + dragOffset.x + snapOffset.x;
|
||||||
const lockY = lockDirection && distanceX > distanceY;
|
let nextY = originalElement.y + dragOffset.y + snapOffset.y;
|
||||||
const original = pointerDownState.originalElements.get(element.id);
|
|
||||||
x = lockX && original ? original.x : element.x + offset.x;
|
if (snapOffset.x === 0 || snapOffset.y === 0) {
|
||||||
y = lockY && original ? original.y : element.y + offset.y;
|
const [nextGridX, nextGridY] = getGridPoint(
|
||||||
} else {
|
originalElement.x + dragOffset.x,
|
||||||
x = element.x + offset.x;
|
originalElement.y + dragOffset.y,
|
||||||
y = element.y + offset.y;
|
gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (snapOffset.x === 0) {
|
||||||
|
nextX = nextGridX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapOffset.y === 0) {
|
||||||
|
nextY = nextGridY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
x,
|
x: nextX,
|
||||||
y,
|
y: nextY,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDragOffsetXY = (
|
export const getDragOffsetXY = (
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
x: number,
|
x: number,
|
||||||
@ -133,6 +139,10 @@ export const dragNewElement = (
|
|||||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||||
true */
|
true */
|
||||||
widthAspectRatio?: number | null,
|
widthAspectRatio?: number | null,
|
||||||
|
originOffset: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null = null,
|
||||||
) => {
|
) => {
|
||||||
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
||||||
if (widthAspectRatio) {
|
if (widthAspectRatio) {
|
||||||
@ -173,8 +183,8 @@ export const dragNewElement = (
|
|||||||
|
|
||||||
if (width !== 0 && height !== 0) {
|
if (width !== 0 && height !== 0) {
|
||||||
mutateElement(draggingElement, {
|
mutateElement(draggingElement, {
|
||||||
x: newX,
|
x: newX + (originOffset?.x ?? 0),
|
||||||
y: newY,
|
y: newY + (originOffset?.y ?? 0),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
@ -41,7 +41,7 @@ import {
|
|||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
TransformHandleDirection,
|
TransformHandleDirection,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { Point, PointerDownState } from "../types";
|
import { AppState, Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
@ -79,6 +79,7 @@ export const transformElements = (
|
|||||||
pointerY: number,
|
pointerY: number,
|
||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
@ -466,8 +467,8 @@ export const resizeSingleElement = (
|
|||||||
boundTextElement.fontSize,
|
boundTextElement.fontSize,
|
||||||
boundTextElement.lineHeight,
|
boundTextElement.lineHeight,
|
||||||
);
|
);
|
||||||
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
eleNewWidth = Math.max(eleNewWidth, minWidth);
|
||||||
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
eleNewHeight = Math.max(eleNewHeight, minHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,8 +509,11 @@ export const resizeSingleElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flipX = eleNewWidth < 0;
|
||||||
|
const flipY = eleNewHeight < 0;
|
||||||
|
|
||||||
// Flip horizontally
|
// Flip horizontally
|
||||||
if (eleNewWidth < 0) {
|
if (flipX) {
|
||||||
if (transformHandleDirection.includes("e")) {
|
if (transformHandleDirection.includes("e")) {
|
||||||
newTopLeft[0] -= Math.abs(newBoundsWidth);
|
newTopLeft[0] -= Math.abs(newBoundsWidth);
|
||||||
}
|
}
|
||||||
@ -517,8 +521,9 @@ export const resizeSingleElement = (
|
|||||||
newTopLeft[0] += Math.abs(newBoundsWidth);
|
newTopLeft[0] += Math.abs(newBoundsWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flip vertically
|
// Flip vertically
|
||||||
if (eleNewHeight < 0) {
|
if (flipY) {
|
||||||
if (transformHandleDirection.includes("s")) {
|
if (transformHandleDirection.includes("s")) {
|
||||||
newTopLeft[1] -= Math.abs(newBoundsHeight);
|
newTopLeft[1] -= Math.abs(newBoundsHeight);
|
||||||
}
|
}
|
||||||
@ -542,10 +547,20 @@ export const resizeSingleElement = (
|
|||||||
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
||||||
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
||||||
|
|
||||||
|
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
||||||
|
// So we need to readjust (x,y) to be where the first point should be
|
||||||
|
const newOrigin = [...newTopLeft];
|
||||||
|
const linearElementXOffset = stateAtResizeStart.x - newBoundsX1;
|
||||||
|
const linearElementYOffset = stateAtResizeStart.y - newBoundsY1;
|
||||||
|
newOrigin[0] += linearElementXOffset;
|
||||||
|
newOrigin[1] += linearElementYOffset;
|
||||||
|
|
||||||
|
const nextX = newOrigin[0];
|
||||||
|
const nextY = newOrigin[1];
|
||||||
|
|
||||||
// Readjust points for linear elements
|
// Readjust points for linear elements
|
||||||
let rescaledElementPointsY;
|
let rescaledElementPointsY;
|
||||||
let rescaledPoints;
|
let rescaledPoints;
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
rescaledElementPointsY = rescalePoints(
|
rescaledElementPointsY = rescalePoints(
|
||||||
1,
|
1,
|
||||||
@ -562,16 +577,11 @@ export const resizeSingleElement = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
|
|
||||||
// So we need to readjust (x,y) to be where the first point should be
|
|
||||||
const newOrigin = [...newTopLeft];
|
|
||||||
newOrigin[0] += stateAtResizeStart.x - newBoundsX1;
|
|
||||||
newOrigin[1] += stateAtResizeStart.y - newBoundsY1;
|
|
||||||
const resizedElement = {
|
const resizedElement = {
|
||||||
width: Math.abs(eleNewWidth),
|
width: Math.abs(eleNewWidth),
|
||||||
height: Math.abs(eleNewHeight),
|
height: Math.abs(eleNewHeight),
|
||||||
x: newOrigin[0],
|
x: nextX,
|
||||||
y: newOrigin[1],
|
y: nextY,
|
||||||
points: rescaledPoints,
|
points: rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -680,6 +690,10 @@ export const resizeMultipleElements = (
|
|||||||
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
|
||||||
targetElements.map(({ orig }) => orig).concat(boundTextElements),
|
targetElements.map(({ orig }) => orig).concat(boundTextElements),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// const originalHeight = maxY - minY;
|
||||||
|
// const originalWidth = maxX - minX;
|
||||||
|
|
||||||
const direction = transformHandleType;
|
const direction = transformHandleType;
|
||||||
|
|
||||||
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
|
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
|
||||||
|
@ -957,7 +957,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
85,
|
85,
|
||||||
4.5,
|
4.999999999999986,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -1002,8 +1002,8 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
375,
|
374.99999999999994,
|
||||||
-539,
|
-535.0000000000001,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -1190,7 +1190,7 @@ describe("textWysiwyg", () => {
|
|||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect(rectangle.height).toBe(156);
|
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
@ -1200,9 +1200,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(156);
|
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||||
// cache updated again
|
// cache updated again
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo(
|
||||||
|
155,
|
||||||
|
8,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reset the container height cache when font properties updated", async () => {
|
it("should reset the container height cache when font properties updated", async () => {
|
||||||
|
@ -21,6 +21,7 @@ export const CODES = {
|
|||||||
V: "KeyV",
|
V: "KeyV",
|
||||||
Z: "KeyZ",
|
Z: "KeyZ",
|
||||||
R: "KeyR",
|
R: "KeyR",
|
||||||
|
S: "KeyS",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const KEYS = {
|
export const KEYS = {
|
||||||
|
@ -164,6 +164,7 @@
|
|||||||
"darkMode": "Dark mode",
|
"darkMode": "Dark mode",
|
||||||
"lightMode": "Light mode",
|
"lightMode": "Light mode",
|
||||||
"zenMode": "Zen mode",
|
"zenMode": "Zen mode",
|
||||||
|
"objectsSnapMode": "Snap to objects",
|
||||||
"exitZenMode": "Exit zen mode",
|
"exitZenMode": "Exit zen mode",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { rotate } from "./math";
|
import { rangeIntersection, rangesOverlap, rotate } from "./math";
|
||||||
|
|
||||||
describe("rotate", () => {
|
describe("rotate", () => {
|
||||||
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
||||||
@ -13,3 +13,43 @@ describe("rotate", () => {
|
|||||||
expect(res2).toEqual([x1, x2]);
|
expect(res2).toEqual([x1, x2]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("range overlap", () => {
|
||||||
|
it("should overlap when range a contains range b", () => {
|
||||||
|
expect(rangesOverlap([1, 4], [2, 3])).toBe(true);
|
||||||
|
expect(rangesOverlap([1, 4], [1, 4])).toBe(true);
|
||||||
|
expect(rangesOverlap([1, 4], [1, 3])).toBe(true);
|
||||||
|
expect(rangesOverlap([1, 4], [2, 4])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should overlap when range b contains range a", () => {
|
||||||
|
expect(rangesOverlap([2, 3], [1, 4])).toBe(true);
|
||||||
|
expect(rangesOverlap([1, 3], [1, 4])).toBe(true);
|
||||||
|
expect(rangesOverlap([2, 4], [1, 4])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should overlap when range a and b intersect", () => {
|
||||||
|
expect(rangesOverlap([1, 4], [2, 5])).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("range intersection", () => {
|
||||||
|
it("should intersect completely with itself", () => {
|
||||||
|
expect(rangeIntersection([1, 4], [1, 4])).toEqual([1, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should intersect irrespective of order", () => {
|
||||||
|
expect(rangeIntersection([1, 4], [2, 3])).toEqual([2, 3]);
|
||||||
|
expect(rangeIntersection([2, 3], [1, 4])).toEqual([2, 3]);
|
||||||
|
expect(rangeIntersection([1, 4], [3, 5])).toEqual([3, 4]);
|
||||||
|
expect(rangeIntersection([3, 5], [1, 4])).toEqual([3, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should intersect at the edge", () => {
|
||||||
|
expect(rangeIntersection([1, 4], [4, 5])).toEqual([4, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not intersect", () => {
|
||||||
|
expect(rangeIntersection([1, 4], [5, 7])).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
33
src/math.ts
33
src/math.ts
@ -472,3 +472,36 @@ export const isRightAngle = (angle: number) => {
|
|||||||
// angle, which we can check with modulo after rounding.
|
// angle, which we can check with modulo after rounding.
|
||||||
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
|
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Given two ranges, return if the two ranges overlap with each other
|
||||||
|
// e.g. [1, 3] overlaps with [2, 4] while [1, 3] does not overlap with [4, 5]
|
||||||
|
export const rangesOverlap = (
|
||||||
|
[a0, a1]: [number, number],
|
||||||
|
[b0, b1]: [number, number],
|
||||||
|
) => {
|
||||||
|
if (a0 <= b0) {
|
||||||
|
return a1 >= b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a0 >= b0) {
|
||||||
|
return b1 >= a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Given two ranges,return ther intersection of the two ranges if any
|
||||||
|
// e.g. the intersection of [1, 3] and [2, 4] is [2, 3]
|
||||||
|
export const rangeIntersection = (
|
||||||
|
rangeA: [number, number],
|
||||||
|
rangeB: [number, number],
|
||||||
|
): [number, number] | null => {
|
||||||
|
const rangeStart = Math.max(rangeA[0], rangeB[0]);
|
||||||
|
const rangeEnd = Math.min(rangeA[1], rangeB[1]);
|
||||||
|
|
||||||
|
if (rangeStart <= rangeEnd) {
|
||||||
|
return [rangeStart, rangeEnd];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
@ -67,6 +67,7 @@ import {
|
|||||||
EXTERNAL_LINK_IMG,
|
EXTERNAL_LINK_IMG,
|
||||||
getLinkHandleFromCoords,
|
getLinkHandleFromCoords,
|
||||||
} from "../element/Hyperlink";
|
} from "../element/Hyperlink";
|
||||||
|
import { renderSnaps } from "./renderSnaps";
|
||||||
import {
|
import {
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
isFrameElement,
|
isFrameElement,
|
||||||
@ -720,6 +721,8 @@ const _renderInteractiveScene = ({
|
|||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSnaps(context, appState);
|
||||||
|
|
||||||
// Reset zoom
|
// Reset zoom
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
|
189
src/renderer/renderSnaps.ts
Normal file
189
src/renderer/renderSnaps.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { PointSnapLine, PointerSnapLine } from "../snapping";
|
||||||
|
import { InteractiveCanvasAppState, Point } from "../types";
|
||||||
|
|
||||||
|
const SNAP_COLOR_LIGHT = "#ff6b6b";
|
||||||
|
const SNAP_COLOR_DARK = "#ff0000";
|
||||||
|
const SNAP_WIDTH = 1;
|
||||||
|
const SNAP_CROSS_SIZE = 2;
|
||||||
|
|
||||||
|
export const renderSnaps = (
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
) => {
|
||||||
|
if (!appState.snapLines.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in dark mode, we need to adjust the color to account for color inversion.
|
||||||
|
// Don't change if zen mode, because we draw only crosses, we want the
|
||||||
|
// colors to be more visible
|
||||||
|
const snapColor =
|
||||||
|
appState.theme === "light" || appState.zenModeEnabled
|
||||||
|
? SNAP_COLOR_LIGHT
|
||||||
|
: SNAP_COLOR_DARK;
|
||||||
|
// in zen mode make the cross more visible since we don't draw the lines
|
||||||
|
const snapWidth =
|
||||||
|
(appState.zenModeEnabled ? SNAP_WIDTH * 1.5 : SNAP_WIDTH) /
|
||||||
|
appState.zoom.value;
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.translate(appState.scrollX, appState.scrollY);
|
||||||
|
|
||||||
|
for (const snapLine of appState.snapLines) {
|
||||||
|
if (snapLine.type === "pointer") {
|
||||||
|
context.lineWidth = snapWidth;
|
||||||
|
context.strokeStyle = snapColor;
|
||||||
|
|
||||||
|
drawPointerSnapLine(snapLine, context, appState);
|
||||||
|
} else if (snapLine.type === "gap") {
|
||||||
|
context.lineWidth = snapWidth;
|
||||||
|
context.strokeStyle = snapColor;
|
||||||
|
|
||||||
|
drawGapLine(
|
||||||
|
snapLine.points[0],
|
||||||
|
snapLine.points[1],
|
||||||
|
snapLine.direction,
|
||||||
|
appState,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
} else if (snapLine.type === "points") {
|
||||||
|
context.lineWidth = snapWidth;
|
||||||
|
context.strokeStyle = snapColor;
|
||||||
|
drawPointsSnapLine(snapLine, context, appState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawPointsSnapLine = (
|
||||||
|
pointSnapLine: PointSnapLine,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
) => {
|
||||||
|
if (!appState.zenModeEnabled) {
|
||||||
|
const firstPoint = pointSnapLine.points[0];
|
||||||
|
const lastPoint = pointSnapLine.points[pointSnapLine.points.length - 1];
|
||||||
|
|
||||||
|
drawLine(firstPoint, lastPoint, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const point of pointSnapLine.points) {
|
||||||
|
drawCross(point, appState, context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawPointerSnapLine = (
|
||||||
|
pointerSnapLine: PointerSnapLine,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
) => {
|
||||||
|
drawCross(pointerSnapLine.points[0], appState, context);
|
||||||
|
if (!appState.zenModeEnabled) {
|
||||||
|
drawLine(pointerSnapLine.points[0], pointerSnapLine.points[1], context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawCross = (
|
||||||
|
[x, y]: Point,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
const size =
|
||||||
|
(appState.zenModeEnabled ? SNAP_CROSS_SIZE * 1.5 : SNAP_CROSS_SIZE) /
|
||||||
|
appState.zoom.value;
|
||||||
|
context.beginPath();
|
||||||
|
|
||||||
|
context.moveTo(x - size, y - size);
|
||||||
|
context.lineTo(x + size, y + size);
|
||||||
|
|
||||||
|
context.moveTo(x + size, y - size);
|
||||||
|
context.lineTo(x - size, y + size);
|
||||||
|
|
||||||
|
context.stroke();
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawLine = (
|
||||||
|
from: Point,
|
||||||
|
to: Point,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
) => {
|
||||||
|
context.beginPath();
|
||||||
|
context.lineTo(...from);
|
||||||
|
context.lineTo(...to);
|
||||||
|
context.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawGapLine = (
|
||||||
|
from: Point,
|
||||||
|
to: Point,
|
||||||
|
direction: "horizontal" | "vertical",
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
) => {
|
||||||
|
// a horizontal gap snap line
|
||||||
|
// |–––––––||–––––––|
|
||||||
|
// ^ ^ ^ ^
|
||||||
|
// \ \ \ \
|
||||||
|
// (1) (2) (3) (4)
|
||||||
|
|
||||||
|
const FULL = 8 / appState.zoom.value;
|
||||||
|
const HALF = FULL / 2;
|
||||||
|
const QUARTER = FULL / 4;
|
||||||
|
|
||||||
|
if (direction === "horizontal") {
|
||||||
|
const halfPoint = [(from[0] + to[0]) / 2, from[1]];
|
||||||
|
// (1)
|
||||||
|
if (!appState.zenModeEnabled) {
|
||||||
|
drawLine([from[0], from[1] - FULL], [from[0], from[1] + FULL], context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3)
|
||||||
|
drawLine(
|
||||||
|
[halfPoint[0] - QUARTER, halfPoint[1] - HALF],
|
||||||
|
[halfPoint[0] - QUARTER, halfPoint[1] + HALF],
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
drawLine(
|
||||||
|
[halfPoint[0] + QUARTER, halfPoint[1] - HALF],
|
||||||
|
[halfPoint[0] + QUARTER, halfPoint[1] + HALF],
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!appState.zenModeEnabled) {
|
||||||
|
// (4)
|
||||||
|
drawLine([to[0], to[1] - FULL], [to[0], to[1] + FULL], context);
|
||||||
|
|
||||||
|
// (2)
|
||||||
|
drawLine(from, to, context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const halfPoint = [from[0], (from[1] + to[1]) / 2];
|
||||||
|
// (1)
|
||||||
|
if (!appState.zenModeEnabled) {
|
||||||
|
drawLine([from[0] - FULL, from[1]], [from[0] + FULL, from[1]], context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (3)
|
||||||
|
drawLine(
|
||||||
|
[halfPoint[0] - HALF, halfPoint[1] - QUARTER],
|
||||||
|
[halfPoint[0] + HALF, halfPoint[1] - QUARTER],
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
drawLine(
|
||||||
|
[halfPoint[0] - HALF, halfPoint[1] + QUARTER],
|
||||||
|
[halfPoint[0] + HALF, halfPoint[1] + QUARTER],
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!appState.zenModeEnabled) {
|
||||||
|
// (4)
|
||||||
|
drawLine([to[0] - FULL, to[1]], [to[0] + FULL, to[1]], context);
|
||||||
|
|
||||||
|
// (2)
|
||||||
|
drawLine(from, to, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -11,6 +11,7 @@ import {
|
|||||||
getFrameElements,
|
getFrameElements,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import { isShallowEqual } from "../utils";
|
import { isShallowEqual } from "../utils";
|
||||||
|
import { isElementInViewport } from "../element/sizeHelpers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frames and their containing elements are not to be selected at the same time.
|
* Frames and their containing elements are not to be selected at the same time.
|
||||||
@ -89,6 +90,26 @@ export const getElementsWithinSelection = (
|
|||||||
return elementsInSelection;
|
return elementsInSelection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getVisibleAndNonSelectedElements = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const selectedElementsSet = new Set(
|
||||||
|
selectedElements.map((element) => element.id),
|
||||||
|
);
|
||||||
|
return elements.filter((element) => {
|
||||||
|
const isVisible = isElementInViewport(
|
||||||
|
element,
|
||||||
|
appState.width,
|
||||||
|
appState.height,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
return !selectedElementsSet.has(element.id) && isVisible;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// FIXME move this into the editor instance to keep utility methods stateless
|
// FIXME move this into the editor instance to keep utility methods stateless
|
||||||
export const isSomeElementSelected = (function () {
|
export const isSomeElementSelected = (function () {
|
||||||
let lastElements: readonly NonDeletedExcalidrawElement[] | null = null;
|
let lastElements: readonly NonDeletedExcalidrawElement[] | null = null;
|
||||||
|
1361
src/snapping.ts
Normal file
1361
src/snapping.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -331,12 +331,17 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -363,6 +368,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -524,12 +530,14 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -553,6 +561,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -723,12 +732,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -752,6 +763,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -1096,12 +1108,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -1125,6 +1139,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -1469,12 +1484,14 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -1498,6 +1515,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -1668,12 +1686,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -1695,6 +1715,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -1904,12 +1925,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -1933,6 +1956,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -2205,12 +2229,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -2239,6 +2265,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -2594,12 +2621,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -2623,6 +2652,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -3473,12 +3503,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -3502,6 +3534,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -3846,12 +3879,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -3875,6 +3910,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -4219,12 +4255,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -4251,6 +4289,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -4951,12 +4990,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -4983,6 +5024,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -5531,12 +5573,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -5565,6 +5609,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -5950,6 +5995,19 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
},
|
},
|
||||||
"viewMode": true,
|
"viewMode": true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checked": [Function],
|
||||||
|
"contextItemLabel": "buttons.objectsSnapMode",
|
||||||
|
"keyTest": [Function],
|
||||||
|
"name": "objectsSnapMode",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "canvas",
|
||||||
|
"predicate": [Function],
|
||||||
|
},
|
||||||
|
"viewMode": true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checked": [Function],
|
"checked": [Function],
|
||||||
"contextItemLabel": "buttons.zenMode",
|
"contextItemLabel": "buttons.zenMode",
|
||||||
@ -6035,12 +6093,17 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -6062,6 +6125,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -6431,12 +6495,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": null,
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -6460,6 +6526,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
@ -6805,12 +6872,17 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -6834,6 +6906,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -87,6 +87,7 @@ describe("contextMenu element", () => {
|
|||||||
"gridMode",
|
"gridMode",
|
||||||
"zenMode",
|
"zenMode",
|
||||||
"viewMode",
|
"viewMode",
|
||||||
|
"objectsSnapMode",
|
||||||
"stats",
|
"stats",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1048,14 +1048,14 @@ describe("Test Linear Elements", () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"height": 130,
|
"height": 130,
|
||||||
"width": 367,
|
"width": 366.11716195150507,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(getBoundTextElementPosition(container, textElement))
|
expect(getBoundTextElementPosition(container, textElement))
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"x": 272,
|
"x": 271.11716195150507,
|
||||||
"y": 45,
|
"y": 45,
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -1069,9 +1069,9 @@ describe("Test Linear Elements", () => {
|
|||||||
[
|
[
|
||||||
20,
|
20,
|
||||||
35,
|
35,
|
||||||
502,
|
501.11716195150507,
|
||||||
95,
|
95,
|
||||||
205.9061448421403,
|
205.4589377083102,
|
||||||
52.5,
|
52.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
@ -84,7 +84,7 @@ describe("move element", () => {
|
|||||||
// select the second rectangles
|
// select the second rectangles
|
||||||
new Pointer("mouse").clickOn(rectB);
|
new Pointer("mouse").clickOn(rectB);
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(24);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(20);
|
expect(renderStaticScene).toHaveBeenCalledTimes(20);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(3);
|
expect(h.elements.length).toEqual(3);
|
||||||
|
@ -110,7 +110,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(11);
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
@ -153,8 +153,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
fireEvent.keyDown(document, {
|
fireEvent.keyDown(document, {
|
||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(11);
|
||||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
|
||||||
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
|
@ -55,10 +55,15 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
"objectsSnapModeEnabled": false,
|
||||||
"openDialog": null,
|
"openDialog": null,
|
||||||
"openMenu": null,
|
"openMenu": null,
|
||||||
"openPopup": null,
|
"openPopup": null,
|
||||||
"openSidebar": null,
|
"openSidebar": null,
|
||||||
|
"originSnapOffset": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
"pasteDialog": {
|
"pasteDialog": {
|
||||||
"data": null,
|
"data": null,
|
||||||
"shown": false,
|
"shown": false,
|
||||||
@ -80,6 +85,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showStats": false,
|
"showStats": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": null,
|
||||||
"suggestedBindings": [],
|
"suggestedBindings": [],
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
|
19
src/types.ts
19
src/types.ts
@ -34,6 +34,7 @@ import Library from "./data/library";
|
|||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { ContextMenuItems } from "./components/ContextMenu";
|
import { ContextMenuItems } from "./components/ContextMenu";
|
||||||
|
import { SnapLine } from "./snapping";
|
||||||
import { Merge, ForwardRef, ValueOf } from "./utility-types";
|
import { Merge, ForwardRef, ValueOf } from "./utility-types";
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
@ -150,6 +151,9 @@ export type InteractiveCanvasAppState = Readonly<
|
|||||||
showHyperlinkPopup: AppState["showHyperlinkPopup"];
|
showHyperlinkPopup: AppState["showHyperlinkPopup"];
|
||||||
// Collaborators
|
// Collaborators
|
||||||
collaborators: AppState["collaborators"];
|
collaborators: AppState["collaborators"];
|
||||||
|
// SnapLines
|
||||||
|
snapLines: AppState["snapLines"];
|
||||||
|
zenModeEnabled: AppState["zenModeEnabled"];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -287,6 +291,13 @@ export type AppState = {
|
|||||||
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||||
showHyperlinkPopup: false | "info" | "editor";
|
showHyperlinkPopup: false | "info" | "editor";
|
||||||
selectedLinearElement: LinearElementEditor | null;
|
selectedLinearElement: LinearElementEditor | null;
|
||||||
|
|
||||||
|
snapLines: SnapLine[];
|
||||||
|
originSnapOffset: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null;
|
||||||
|
objectsSnapModeEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<
|
||||||
@ -400,6 +411,7 @@ export interface ExcalidrawProps {
|
|||||||
viewModeEnabled?: boolean;
|
viewModeEnabled?: boolean;
|
||||||
zenModeEnabled?: boolean;
|
zenModeEnabled?: boolean;
|
||||||
gridModeEnabled?: boolean;
|
gridModeEnabled?: boolean;
|
||||||
|
objectsSnapModeEnabled?: boolean;
|
||||||
libraryReturnUrl?: string;
|
libraryReturnUrl?: string;
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -653,3 +665,10 @@ export type FrameNameBoundsCache = {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type KeyboardModifiersObject = {
|
||||||
|
ctrlKey: boolean;
|
||||||
|
shiftKey: boolean;
|
||||||
|
altKey: boolean;
|
||||||
|
metaKey: boolean;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user