Implement line editing (#1616)

* implement line editing

* line editing with rotation

* ensure adding new points is disabled on point dragging

* fix hotkey replacement

* don't paint bounding box when creating new multipoint

* tweak points style, account for zoom and z-index

* don't persist editingLinearElement to localStorage

* don't mutate on noop points updates

* account for rotation when adding new point

* ensure clicking on points doesn't deselect element

* tweak history handling around editingline element

* update snapshots

* refactor pointerMove handling

* factor out point dragging

* factor out pointerDown

* improve positioning with rotation

* revert to use roughjs for calculating points bounds

* migrate from storing editingLinearElement.element to id

* make GlobalScene.getElement into O(1)

* use Alt for adding new points

* fix adding and deleting a point with rotation

* disable resize handlers & bounding box on line edit

Co-authored-by: daishi <daishi@axlight.com>
This commit is contained in:
David Luzar 2020-06-01 11:35:44 +02:00 committed by GitHub
parent db316f32e0
commit 14a66956d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1129 additions and 76 deletions

View file

@ -10,6 +10,7 @@ import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { newElementWith } from "../element/mutateElement";
import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor";
const deleteSelectedElements = (
elements: readonly ExcalidrawElement[],
@ -29,26 +30,80 @@ const deleteSelectedElements = (
};
};
function handleGroupEditingState(
appState: AppState,
elements: readonly ExcalidrawElement[],
): AppState {
if (appState.editingGroupId) {
const siblingElements = getElementsInGroup(
getNonDeletedElements(elements),
appState.editingGroupId!,
);
if (siblingElements.length) {
return {
...appState,
selectedElementIds: { [siblingElements[0].id]: true },
};
}
}
return appState;
}
export const actionDeleteSelected = register({
name: "deleteSelectedElements",
perform: (elements, appState) => {
if (
appState.editingLinearElement?.activePointIndex != null &&
appState.editingLinearElement?.activePointIndex > -1
) {
const { elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (element) {
// case: deleting last point
if (element.points.length < 2) {
const nextElements = elements.filter((el) => el.id !== element.id);
const nextAppState = handleGroupEditingState(appState, nextElements);
return {
elements: nextElements,
appState: {
...nextAppState,
editingLinearElement: null,
},
commitToHistory: false,
};
}
LinearElementEditor.movePoint(
element,
appState.editingLinearElement.activePointIndex,
"delete",
);
return {
elements: elements,
appState: {
...appState,
editingLinearElement: {
...appState.editingLinearElement,
activePointIndex:
appState.editingLinearElement.activePointIndex > 0
? appState.editingLinearElement.activePointIndex - 1
: 0,
},
},
commitToHistory: true,
};
}
}
let {
elements: nextElements,
appState: nextAppState,
} = deleteSelectedElements(elements, appState);
if (appState.editingGroupId) {
const siblingElements = getElementsInGroup(
getNonDeletedElements(nextElements),
appState.editingGroupId!,
);
if (siblingElements.length) {
nextAppState = {
...nextAppState,
selectedElementIds: { [siblingElements[0].id]: true },
};
}
}
nextAppState = handleGroupEditingState(nextAppState, nextElements);
return {
elements: nextElements,
appState: {

View file

@ -8,10 +8,30 @@ import { t } from "../i18n";
import { register } from "./register";
import { mutateElement } from "../element/mutateElement";
import { isPathALoop } from "../math";
import { LinearElementEditor } from "../element/linearElementEditor";
export const actionFinalize = register({
name: "finalize",
perform: (elements, appState) => {
if (appState.editingLinearElement) {
const { elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (element) {
return {
elements:
element.points.length < 2 || isInvisiblySmallElement(element)
? elements.filter((el) => el.id !== element.id)
: undefined,
appState: {
...appState,
editingLinearElement: null,
},
commitToHistory: true,
};
}
}
let newElements = elements;
if (window.document.activeElement instanceof HTMLElement) {
window.document.activeElement.blur();
@ -94,8 +114,8 @@ export const actionFinalize = register({
},
keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE &&
!appState.draggingElement &&
appState.multiElement === null) ||
(appState.editingLinearElement !== null ||
(!appState.draggingElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null),
PanelComponent: ({ appState, updateData }) => (

View file

@ -30,23 +30,26 @@ const writeData = (
const prevElementMap = getElementMap(prevElements);
const nextElements = data.elements;
const nextElementMap = getElementMap(nextElements);
return {
elements: nextElements
.map((nextElement) =>
newElementWith(
prevElementMap[nextElement.id] || nextElement,
nextElement,
),
)
.concat(
prevElements
.filter(
(prevElement) => !nextElementMap.hasOwnProperty(prevElement.id),
)
.map((prevElement) =>
newElementWith(prevElement, { isDeleted: true }),
),
const elements = nextElements
.map((nextElement) =>
newElementWith(
prevElementMap[nextElement.id] || nextElement,
nextElement,
),
)
.concat(
prevElements
.filter(
(prevElement) => !nextElementMap.hasOwnProperty(prevElement.id),
)
.map((prevElement) =>
newElementWith(prevElement, { isDeleted: true }),
),
);
return {
elements,
appState: { ...appState, ...data.appState },
commitToHistory,
syncHistory: true,