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:
Aakansha Doshi 2022-08-03 20:58:17 +05:30 committed by GitHub
parent fe7fbff7f6
commit 08ce7c7fc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 600 additions and 210 deletions

View file

@ -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(),
),