mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: redesign linear elements 🎉 (#5501)
* feat: redesign arrows and lines * set selectedLinearElement on pointerup * fix tests * fix lint * set selectionLinearElement to null when element is not selected * fix * don't set selectedElementIds to empty object when linear element selected * don't move arrows when clicked on bounding box * don't consider bounding box when linear element selected * better hitbox * show pointer when over the points in linear elements * highlight points when hovered * tweak design whene editing linear element points * tweak * fix test * fix multi point editing * cleanup * fix * fix * remove stroke when hovered * account for zoom when hover * review fix * set selectedLinearElement to null when selectedElementIds doesn't contain the linear element * remove hover affect when moved away from linear element * don't set selectedLinearAElement if already set * fix selection * render reduced in test :p * fix box selection for single linear element * set selectedLinearElement when deselecting selected elements and linear element is selected * don't show linear element handles when element locked * selected linear element when only linear present and selected with selectAll * don't set selectedLinearElement if already set * store selectedLinearElement in browser to persist * remove redundant checks * test fix * select linear element handles when user has finished multipoint editing * fix snap * add comments * show bounding box for locked linear elements * add stroke param to fillCircle and remove stroke when linear element point hovered * set selectedLinearElement when thats the only element left when deselcting others * skip tests instead of removing for rotation * (un)bind on pointerUp when moving linear element points outside editor * render bounding box for linear elements as a fallback on state mismatch * simplify and remove type assertion Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
fe7fbff7f6
commit
08ce7c7fc3
16 changed files with 600 additions and 210 deletions
|
@ -105,6 +105,7 @@ import {
|
|||
updateTextElement,
|
||||
} from "../element";
|
||||
import {
|
||||
bindOrUnbindLinearElement,
|
||||
bindOrUnbindSelectedElements,
|
||||
fixBindingsAfterDeletion,
|
||||
fixBindingsAfterDuplication,
|
||||
|
@ -1133,6 +1134,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.actionManager.executeAction(actionFinalize);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.selectedLinearElement &&
|
||||
!this.state.selectedElementIds[this.state.selectedLinearElement.elementId]
|
||||
) {
|
||||
// To make sure `selectedLinearElement` is in sync with `selectedElementIds`, however this shouldn't be needed once
|
||||
// we have a single API to update `selectedElementIds`
|
||||
this.setState({ selectedLinearElement: null });
|
||||
}
|
||||
|
||||
const { multiElement } = prevState;
|
||||
if (
|
||||
prevState.activeTool !== this.state.activeTool &&
|
||||
|
@ -2887,22 +2898,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||
} else if (isOverScrollBar) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
} else if (this.state.editingLinearElement) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
this.state.editingLinearElement.elementId,
|
||||
} else if (this.state.selectedLinearElement) {
|
||||
this.handleHoverSelectedLinearElement(
|
||||
this.state.selectedLinearElement,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
);
|
||||
|
||||
if (
|
||||
element &&
|
||||
isHittingElementNotConsideringBoundingBox(element, this.state, [
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
])
|
||||
) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||
} else {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
}
|
||||
} else if (
|
||||
// if using cmd/ctrl, we're not dragging
|
||||
!event[KEYS.CTRL_OR_CMD] &&
|
||||
|
@ -3014,6 +3015,53 @@ class App extends React.Component<AppProps, AppState> {
|
|||
invalidateContextMenu = true;
|
||||
};
|
||||
|
||||
handleHoverSelectedLinearElement(
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
);
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (this.state.selectedLinearElement) {
|
||||
let hoverPointIndex = -1;
|
||||
if (
|
||||
isHittingElementNotConsideringBoundingBox(element, this.state, [
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
])
|
||||
) {
|
||||
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
element,
|
||||
this.state.zoom,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
);
|
||||
if (hoverPointIndex >= 0) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
||||
} else {
|
||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.selectedLinearElement.hoverPointIndex !== hoverPointIndex
|
||||
) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
hoverPointIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
}
|
||||
}
|
||||
private handleCanvasPointerDown = (
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
|
@ -3544,17 +3592,26 @@ class App extends React.Component<AppProps, AppState> {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
if (this.state.editingLinearElement) {
|
||||
if (this.state.selectedLinearElement) {
|
||||
const linearElementEditor =
|
||||
this.state.editingLinearElement || this.state.selectedLinearElement;
|
||||
const ret = LinearElementEditor.handlePointerDown(
|
||||
event,
|
||||
this.state,
|
||||
(appState) => this.setState(appState),
|
||||
this.history,
|
||||
pointerDownState.origin,
|
||||
linearElementEditor,
|
||||
);
|
||||
if (ret.hitElement) {
|
||||
pointerDownState.hit.element = ret.hitElement;
|
||||
}
|
||||
if (ret.linearElementEditor) {
|
||||
this.setState({ selectedLinearElement: ret.linearElementEditor });
|
||||
|
||||
if (this.state.editingLinearElement) {
|
||||
this.setState({ editingLinearElement: ret.linearElementEditor });
|
||||
}
|
||||
}
|
||||
if (ret.didAddPoint) {
|
||||
return true;
|
||||
}
|
||||
|
@ -4069,10 +4126,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.state.editingLinearElement) {
|
||||
if (this.state.selectedLinearElement) {
|
||||
const linearElementEditor =
|
||||
this.state.editingLinearElement || this.state.selectedLinearElement;
|
||||
const didDrag = LinearElementEditor.handlePointDragging(
|
||||
this.state,
|
||||
(appState) => this.setState(appState),
|
||||
pointerCoords.x,
|
||||
pointerCoords.y,
|
||||
(element, pointsSceneCoords) => {
|
||||
|
@ -4081,11 +4139,30 @@ class App extends React.Component<AppProps, AppState> {
|
|||
pointsSceneCoords,
|
||||
);
|
||||
},
|
||||
linearElementEditor,
|
||||
);
|
||||
|
||||
if (didDrag) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
pointerDownState.lastCoords.y = pointerCoords.y;
|
||||
if (
|
||||
this.state.editingLinearElement &&
|
||||
!this.state.editingLinearElement.isDragging
|
||||
) {
|
||||
this.setState({
|
||||
editingLinearElement: {
|
||||
...this.state.editingLinearElement,
|
||||
isDragging: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!this.state.selectedLinearElement.isDragging) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
isDragging: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -4104,6 +4181,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
(!this.state.editingLinearElement ||
|
||||
this.state.editingLinearElement?.elementId !==
|
||||
pointerDownState.hit.element?.id ||
|
||||
pointerDownState.hit.hasHitElementInside) &&
|
||||
(!this.state.selectedLinearElement ||
|
||||
this.state.selectedLinearElement?.elementId !==
|
||||
pointerDownState.hit.element?.id ||
|
||||
pointerDownState.hit.hasHitElementInside)
|
||||
) {
|
||||
const selectedElements = getSelectedElements(
|
||||
|
@ -4132,7 +4213,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
// We only drag in one direction if shift is pressed
|
||||
const lockDirection = event.shiftKey;
|
||||
|
||||
dragSelectedElements(
|
||||
pointerDownState,
|
||||
selectedElements,
|
||||
|
@ -4344,6 +4424,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||
elementsWithinSelection[0].link
|
||||
? "info"
|
||||
: false,
|
||||
// select linear element only when we haven't box-selected anything else
|
||||
selectedLinearElement:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
isLinearElement(elementsWithinSelection[0])
|
||||
? new LinearElementEditor(
|
||||
elementsWithinSelection[0],
|
||||
this.scene,
|
||||
)
|
||||
: null,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
),
|
||||
|
@ -4431,6 +4520,46 @@ class App extends React.Component<AppProps, AppState> {
|
|||
});
|
||||
}
|
||||
}
|
||||
} else if (this.state.selectedLinearElement) {
|
||||
if (
|
||||
!pointerDownState.boxSelection.hasOccurred &&
|
||||
(pointerDownState.hit?.element?.id !==
|
||||
this.state.selectedLinearElement.elementId ||
|
||||
!pointerDownState.hit.hasHitElementInside)
|
||||
) {
|
||||
const selectedELements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
// set selectedLinearElement to null if there is more than one element selected since we don't want to show linear element handles
|
||||
if (selectedELements.length > 1) {
|
||||
this.setState({ selectedLinearElement: null });
|
||||
}
|
||||
} else {
|
||||
const linearElementEditor = LinearElementEditor.handlePointerUp(
|
||||
childEvent,
|
||||
this.state.selectedLinearElement,
|
||||
this.state,
|
||||
);
|
||||
|
||||
const { startBindingElement, endBindingElement } =
|
||||
linearElementEditor;
|
||||
const element = this.scene.getElement(linearElementEditor.elementId);
|
||||
if (isBindingElement(element)) {
|
||||
bindOrUnbindLinearElement(
|
||||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
);
|
||||
}
|
||||
|
||||
if (linearElementEditor !== this.state.selectedLinearElement) {
|
||||
this.setState({
|
||||
selectedLinearElement: linearElementEditor,
|
||||
suggestedBindings: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastPointerUp = null;
|
||||
|
@ -4563,6 +4692,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
...prevState.selectedElementIds,
|
||||
[draggingElement.id]: true,
|
||||
},
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
draggingElement,
|
||||
this.scene,
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
this.setState((prevState) => ({
|
||||
|
@ -4614,6 +4747,25 @@ class App extends React.Component<AppProps, AppState> {
|
|||
// Code below handles selection when element(s) weren't
|
||||
// drag or added to selection on pointer down phase.
|
||||
const hitElement = pointerDownState.hit.element;
|
||||
if (
|
||||
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
|
||||
isLinearElement(hitElement)
|
||||
) {
|
||||
const selectedELements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.state,
|
||||
);
|
||||
// set selectedLinearElement when no other element selected except
|
||||
// the one we've hit
|
||||
if (selectedELements.length === 1) {
|
||||
this.setState({
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
hitElement,
|
||||
this.scene,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isEraserActive(this.state)) {
|
||||
const draggedDistance = distance2d(
|
||||
this.lastPointerDown!.clientX,
|
||||
|
@ -4689,23 +4841,38 @@ class App extends React.Component<AppProps, AppState> {
|
|||
} else {
|
||||
// remove element from selection while
|
||||
// keeping prev elements selected
|
||||
this.setState((prevState) =>
|
||||
selectGroupsForSelectedElements(
|
||||
|
||||
this.setState((prevState) => {
|
||||
const newSelectedElementIds = {
|
||||
...prevState.selectedElementIds,
|
||||
[hitElement!.id]: false,
|
||||
};
|
||||
const newSelectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
{ ...prevState, selectedElementIds: newSelectedElementIds },
|
||||
);
|
||||
|
||||
return selectGroupsForSelectedElements(
|
||||
{
|
||||
...prevState,
|
||||
selectedElementIds: {
|
||||
...prevState.selectedElementIds,
|
||||
[hitElement!.id]: false,
|
||||
},
|
||||
selectedElementIds: newSelectedElementIds,
|
||||
// set selectedLinearElement only if thats the only element selected
|
||||
selectedLinearElement:
|
||||
newSelectedElements.length === 1 &&
|
||||
isLinearElement(newSelectedElements[0])
|
||||
? new LinearElementEditor(
|
||||
newSelectedElements[0],
|
||||
this.scene,
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// add element to selection while
|
||||
// keeping prev elements selected
|
||||
|
||||
this.setState((_prevState) => ({
|
||||
selectedElementIds: {
|
||||
..._prevState.selectedElementIds,
|
||||
|
@ -4719,6 +4886,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||
{
|
||||
...prevState,
|
||||
selectedElementIds: { [hitElement.id]: true },
|
||||
selectedLinearElement:
|
||||
isLinearElement(hitElement) &&
|
||||
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
|
||||
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
|
||||
this.state.selectedLinearElement?.elementId !== hitElement.id
|
||||
? new LinearElementEditor(hitElement, this.scene)
|
||||
: this.state.selectedLinearElement,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
),
|
||||
|
@ -4727,6 +4901,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
if (
|
||||
!this.state.selectedLinearElement &&
|
||||
!this.state.editingLinearElement &&
|
||||
!pointerDownState.drag.hasOccurred &&
|
||||
!this.state.isResizing &&
|
||||
|
@ -5474,6 +5649,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
{
|
||||
...this.state,
|
||||
selectedElementIds: { [element.id]: true },
|
||||
selectedLinearElement: isLinearElement(element)
|
||||
? new LinearElementEditor(element, this.scene)
|
||||
: null,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue