feat: show a mid point for linear elements (#5534)

* feat: Add a mid point for linear elements

* fix tests

* show mid point only on hover

* hacky fix :(

* don't add mid points if present and only add outside editor

* improve styling and always show phantom point instead of just on hover

* fix tests

* fix

* only add polyfill for test

* add hover state for phantom point

* fix tests

* fix

* Add Array.at polyfill

* reuse `centerPoint()` helper

* reuse `distance2d`

* use `Point` type

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Aakansha Doshi 2022-08-11 20:16:25 +05:30 committed by GitHub
parent 731093f631
commit 5a8dbe8030
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 266 additions and 43 deletions

View file

@ -11,6 +11,7 @@ import {
isPathALoop,
getGridPoint,
rotatePoint,
centerPoint,
} from "../math";
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
import { getElementPointsCoords } from "./bounds";
@ -51,6 +52,7 @@ export class LinearElementEditor {
| "keep";
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
public readonly hoverPointIndex: number;
public readonly midPointHovered: boolean;
constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
this.elementId = element.id as string & {
@ -70,6 +72,7 @@ export class LinearElementEditor {
lastClickedPoint: -1,
};
this.hoverPointIndex = -1;
this.midPointHovered = false;
}
// ---------------------------------------------------------------------------
@ -157,7 +160,6 @@ export class LinearElementEditor {
if (!linearElementEditor) {
return false;
}
const { selectedPointsIndices, elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
@ -357,6 +359,59 @@ export class LinearElementEditor {
};
}
static isHittingMidPoint = (
linearElementEditor: LinearElementEditor,
scenePointer: { x: number; y: number },
appState: AppState,
) => {
if (appState.editingLinearElement) {
return false;
}
const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return false;
}
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element,
appState.zoom,
scenePointer.x,
scenePointer.y,
);
if (clickedPointIndex >= 0) {
return false;
}
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
if (points.length >= 3) {
return false;
}
const midPoint = this.getMidPoint(linearElementEditor);
if (midPoint) {
const threshold =
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
const distance = distance2d(
midPoint[0],
midPoint[1],
scenePointer.x,
scenePointer.y,
);
return distance <= threshold;
}
return false;
};
static getMidPoint(linearElementEditor: LinearElementEditor) {
const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return null;
}
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
return centerPoint(points[0], points.at(-1)!);
}
static handlePointerDown(
event: React.PointerEvent<HTMLCanvasElement>,
appState: AppState,
@ -367,11 +422,13 @@ export class LinearElementEditor {
didAddPoint: boolean;
hitElement: NonDeleted<ExcalidrawElement> | null;
linearElementEditor: LinearElementEditor | null;
isMidPoint: boolean;
} {
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
didAddPoint: false,
hitElement: null,
linearElementEditor: null,
isMidPoint: false,
};
if (!linearElementEditor) {
@ -384,6 +441,45 @@ export class LinearElementEditor {
if (!element) {
return ret;
}
const hittingMidPoint = LinearElementEditor.isHittingMidPoint(
linearElementEditor,
scenePointer,
appState,
);
if (
LinearElementEditor.isHittingMidPoint(
linearElementEditor,
scenePointer,
appState,
)
) {
const midPoint = this.getMidPoint(linearElementEditor);
if (midPoint) {
mutateElement(element, {
points: [
element.points[0],
LinearElementEditor.createPointAt(
element,
midPoint[0],
midPoint[1],
appState.gridSize,
),
...element.points.slice(1),
],
});
}
ret.didAddPoint = true;
ret.isMidPoint = true;
ret.linearElementEditor = {
...linearElementEditor,
selectedPointsIndices: element.points[1],
pointerDownState: {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: -1,
},
lastUncommittedPoint: null,
};
}
if (event.altKey && appState.editingLinearElement) {
if (linearElementEditor.lastUncommittedPoint == null) {
mutateElement(element, {
@ -397,6 +493,7 @@ export class LinearElementEditor {
),
],
});
ret.didAddPoint = true;
}
history.resumeRecording();
ret.linearElementEditor = {
@ -426,7 +523,7 @@ export class LinearElementEditor {
// if we clicked on a point, set the element as hitElement otherwise
// it would get deselected if the point is outside the hitbox area
if (clickedPointIndex > -1) {
if (clickedPointIndex >= 0 || hittingMidPoint) {
ret.hitElement = element;
} else {
// You might be wandering why we are storing the binding elements on
@ -567,14 +664,14 @@ export class LinearElementEditor {
/** scene coords */
static getPointsGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>,
) {
): Point[] {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
return element.points.map((point) => {
let { x, y } = element;
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
return [x, y];
return [x, y] as const;
});
}