improve & granularize ExcalidrawElement types (#991)

* improve & granularize ExcalidrawElement types

* fix incorrectly passing type

* fix tests

* fix more tests

* fix unnecessary spreads & refactor

* add comments
This commit is contained in:
David Luzar 2020-03-17 20:55:40 +01:00 committed by GitHub
parent 1c545c1d47
commit 373d16abe6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 430 additions and 272 deletions

View file

@ -21,6 +21,7 @@ import {
getDrawingVersion,
getSyncableElements,
hasNonDeletedElements,
newLinearElement,
} from "../element";
import {
deleteSelectedElements,
@ -47,7 +48,7 @@ import { restore } from "../data/restore";
import { renderScene } from "../renderer";
import { AppState, GestureEvent, Gesture } from "../types";
import { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement, ExcalidrawLinearElement } from "../element/types";
import {
isWritableElement,
@ -99,6 +100,7 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
import { invalidateShapeForElement } from "../renderer/renderElement";
import { unstable_batchedUpdates } from "react-dom";
import { SceneStateCallbackRemover } from "../scene/globalScene";
import { isLinearElement } from "../element/typeChecks";
import { rescalePoints } from "../points";
function withBatchedUpdates<
@ -707,21 +709,18 @@ export class App extends React.Component<any, AppState> {
window.devicePixelRatio,
);
const element = newTextElement(
newElement(
"text",
x,
y,
this.state.currentItemStrokeColor,
this.state.currentItemBackgroundColor,
this.state.currentItemFillStyle,
this.state.currentItemStrokeWidth,
this.state.currentItemRoughness,
this.state.currentItemOpacity,
),
data.text,
this.state.currentItemFont,
);
const element = newTextElement({
x: x,
y: y,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
text: data.text,
font: this.state.currentItemFont,
});
globalSceneState.replaceAllElements([
...globalSceneState.getAllElements(),
@ -960,21 +959,18 @@ export class App extends React.Component<any, AppState> {
const element =
elementAtPosition && isTextElement(elementAtPosition)
? elementAtPosition
: newTextElement(
newElement(
"text",
x,
y,
this.state.currentItemStrokeColor,
this.state.currentItemBackgroundColor,
this.state.currentItemFillStyle,
this.state.currentItemStrokeWidth,
this.state.currentItemRoughness,
this.state.currentItemOpacity,
),
"", // default text
this.state.currentItemFont, // default font
);
: newTextElement({
x: x,
y: y,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
text: "",
font: this.state.currentItemFont,
});
this.setState({ editingElement: element });
@ -1044,11 +1040,8 @@ export class App extends React.Component<any, AppState> {
if (text) {
globalSceneState.replaceAllElements([
...globalSceneState.getAllElements(),
{
// we need to recreate the element to update dimensions &
// position
...newTextElement(element, text, element.font),
},
// we need to recreate the element to update dimensions & position
newTextElement({ ...element, text, font: element.font }),
]);
}
this.setState(prevState => ({
@ -1332,22 +1325,6 @@ export class App extends React.Component<any, AppState> {
const originX = x;
const originY = y;
let element = newElement(
this.state.elementType,
x,
y,
this.state.currentItemStrokeColor,
this.state.currentItemBackgroundColor,
this.state.currentItemFillStyle,
this.state.currentItemStrokeWidth,
this.state.currentItemRoughness,
this.state.currentItemOpacity,
);
if (isTextElement(element)) {
element = newTextElement(element, "", this.state.currentItemFont);
}
type ResizeTestType = ReturnType<typeof resizeTest>;
let resizeHandle: ResizeTestType = false;
let isResizingElements = false;
@ -1437,30 +1414,30 @@ export class App extends React.Component<any, AppState> {
this.setState({ selectedElementIds: {} });
}
if (isTextElement(element)) {
if (this.state.elementType === "text") {
// if we're currently still editing text, clicking outside
// should only finalize it, not create another (irrespective
// of state.elementLocked)
if (this.state.editingElement?.type === "text") {
return;
}
if (elementIsAddedToSelection) {
element = hitElement!;
}
let textX = event.clientX;
let textY = event.clientY;
if (!event.altKey) {
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
x,
y,
);
if (snappedToCenterPosition) {
element.x = snappedToCenterPosition.elementCenterX;
element.y = snappedToCenterPosition.elementCenterY;
textX = snappedToCenterPosition.wysiwygX;
textY = snappedToCenterPosition.wysiwygY;
}
}
const snappedToCenterPosition = event.altKey
? null
: this.getTextWysiwygSnappedToCenterPosition(x, y);
const element = newTextElement({
x: snappedToCenterPosition?.elementCenterX ?? x,
y: snappedToCenterPosition?.elementCenterY ?? y,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
text: "",
font: this.state.currentItemFont,
});
const resetSelection = () => {
this.setState({
@ -1471,8 +1448,8 @@ export class App extends React.Component<any, AppState> {
textWysiwyg({
initText: "",
x: textX,
y: textY,
x: snappedToCenterPosition?.wysiwygX ?? event.clientX,
y: snappedToCenterPosition?.wysiwygY ?? event.clientY,
strokeColor: this.state.currentItemStrokeColor,
opacity: this.state.currentItemOpacity,
font: this.state.currentItemFont,
@ -1481,9 +1458,11 @@ export class App extends React.Component<any, AppState> {
if (text) {
globalSceneState.replaceAllElements([
...globalSceneState.getAllElements(),
{
...newTextElement(element, text, this.state.currentItemFont),
},
newTextElement({
...element,
text,
font: this.state.currentItemFont,
}),
]);
}
this.setState(prevState => ({
@ -1531,6 +1510,17 @@ export class App extends React.Component<any, AppState> {
points: [...multiElement.points, [x - rx, y - ry]],
});
} else {
const element = newLinearElement({
type: this.state.elementType,
x: x,
y: y,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
});
this.setState(prevState => ({
selectedElementIds: {
...prevState.selectedElementIds,
@ -1549,26 +1539,40 @@ export class App extends React.Component<any, AppState> {
editingElement: element,
});
}
} else if (element.type === "selection") {
this.setState({
selectionElement: element,
draggingElement: element,
});
} else {
globalSceneState.replaceAllElements([
...globalSceneState.getAllElements(),
element,
]);
this.setState({
multiElement: null,
draggingElement: element,
editingElement: element,
const element = newElement({
type: this.state.elementType,
x: x,
y: y,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
});
if (element.type === "selection") {
this.setState({
selectionElement: element,
draggingElement: element,
});
} else {
globalSceneState.replaceAllElements([
...globalSceneState.getAllElements(),
element,
]);
this.setState({
multiElement: null,
draggingElement: element,
editingElement: element,
});
}
}
let resizeArrowFn:
| ((
element: ExcalidrawElement,
element: ExcalidrawLinearElement,
pointIndex: number,
deltaX: number,
deltaY: number,
@ -1579,7 +1583,7 @@ export class App extends React.Component<any, AppState> {
| null = null;
const arrowResizeOrigin = (
element: ExcalidrawElement,
element: ExcalidrawLinearElement,
pointIndex: number,
deltaX: number,
deltaY: number,
@ -1604,7 +1608,9 @@ export class App extends React.Component<any, AppState> {
x: dx,
y: dy,
points: element.points.map((point, i) =>
i === pointIndex ? [absPx - element.x, absPy - element.y] : point,
i === pointIndex
? ([absPx - element.x, absPy - element.y] as const)
: point,
),
});
} else {
@ -1612,14 +1618,16 @@ export class App extends React.Component<any, AppState> {
x: element.x + deltaX,
y: element.y + deltaY,
points: element.points.map((point, i) =>
i === pointIndex ? [p1[0] - deltaX, p1[1] - deltaY] : point,
i === pointIndex
? ([p1[0] - deltaX, p1[1] - deltaY] as const)
: point,
),
});
}
};
const arrowResizeEnd = (
element: ExcalidrawElement,
element: ExcalidrawLinearElement,
pointIndex: number,
deltaX: number,
deltaY: number,
@ -1636,13 +1644,15 @@ export class App extends React.Component<any, AppState> {
);
mutateElement(element, {
points: element.points.map((point, i) =>
i === pointIndex ? [width, height] : point,
i === pointIndex ? ([width, height] as const) : point,
),
});
} else {
mutateElement(element, {
points: element.points.map((point, i) =>
i === pointIndex ? [p1[0] + deltaX, p1[1] + deltaY] : point,
i === pointIndex
? ([p1[0] + deltaX, p1[1] + deltaY] as const)
: point,
),
});
}
@ -1711,10 +1721,9 @@ export class App extends React.Component<any, AppState> {
const deltaX = x - lastX;
const deltaY = y - lastY;
const element = selectedElements[0];
const isLinear = element.type === "line" || element.type === "arrow";
switch (resizeHandle) {
case "nw":
if (isLinear && element.points.length === 2) {
if (isLinearElement(element) && element.points.length === 2) {
const [, p1] = element.points;
if (!resizeArrowFn) {
@ -1739,7 +1748,7 @@ export class App extends React.Component<any, AppState> {
}
break;
case "ne":
if (isLinear && element.points.length === 2) {
if (isLinearElement(element) && element.points.length === 2) {
const [, p1] = element.points;
if (!resizeArrowFn) {
if (p1[0] >= 0) {
@ -1761,7 +1770,7 @@ export class App extends React.Component<any, AppState> {
}
break;
case "sw":
if (isLinear && element.points.length === 2) {
if (isLinearElement(element) && element.points.length === 2) {
const [, p1] = element.points;
if (!resizeArrowFn) {
if (p1[0] <= 0) {
@ -1782,7 +1791,7 @@ export class App extends React.Component<any, AppState> {
}
break;
case "se":
if (isLinear && element.points.length === 2) {
if (isLinearElement(element) && element.points.length === 2) {
const [, p1] = element.points;
if (!resizeArrowFn) {
if (p1[0] > 0 || p1[1] > 0) {
@ -1807,14 +1816,18 @@ export class App extends React.Component<any, AppState> {
break;
}
mutateElement(element, {
height,
y: element.y + deltaY,
points:
element.points.length > 0
? rescalePoints(1, height, element.points)
: undefined,
});
if (isLinearElement(element)) {
mutateElement(element, {
height,
y: element.y + deltaY,
points: rescalePoints(1, height, element.points),
});
} else {
mutateElement(element, {
height,
y: element.y + deltaY,
});
}
break;
}
@ -1825,15 +1838,18 @@ export class App extends React.Component<any, AppState> {
// Someday we should implement logic to flip the shape. But for now, just stop.
break;
}
mutateElement(element, {
width,
x: element.x + deltaX,
points:
element.points.length > 0
? rescalePoints(0, width, element.points)
: undefined,
});
if (isLinearElement(element)) {
mutateElement(element, {
width,
x: element.x + deltaX,
points: rescalePoints(0, width, element.points),
});
} else {
mutateElement(element, {
width,
x: element.x + deltaX,
});
}
break;
}
case "s": {
@ -1842,14 +1858,16 @@ export class App extends React.Component<any, AppState> {
break;
}
mutateElement(element, {
height,
points:
element.points.length > 0
? rescalePoints(1, height, element.points)
: undefined,
});
if (isLinearElement(element)) {
mutateElement(element, {
height,
points: rescalePoints(1, height, element.points),
});
} else {
mutateElement(element, {
height,
});
}
break;
}
case "e": {
@ -1858,13 +1876,16 @@ export class App extends React.Component<any, AppState> {
break;
}
mutateElement(element, {
width,
points:
element.points.length > 0
? rescalePoints(0, width, element.points)
: undefined,
});
if (isLinearElement(element)) {
mutateElement(element, {
width,
points: rescalePoints(0, width, element.points),
});
} else {
mutateElement(element, {
width,
});
}
break;
}
}
@ -1934,10 +1955,7 @@ export class App extends React.Component<any, AppState> {
let width = distance(originX, x);
let height = distance(originY, y);
const isLinear =
this.state.elementType === "line" || this.state.elementType === "arrow";
if (isLinear) {
if (isLinearElement(draggingElement)) {
draggingOccurred = true;
const points = draggingElement.points;
let dx = x - draggingElement.x;
@ -2023,7 +2041,7 @@ export class App extends React.Component<any, AppState> {
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp);
if (elementType === "arrow" || elementType === "line") {
if (isLinearElement(draggingElement)) {
if (draggingElement!.points.length > 1) {
history.resumeRecording();
}
@ -2041,7 +2059,7 @@ export class App extends React.Component<any, AppState> {
],
});
this.setState({
multiElement: this.state.draggingElement,
multiElement: draggingElement,
editingElement: this.state.draggingElement,
});
} else if (draggingOccurred && !multiElement) {
@ -2215,12 +2233,12 @@ export class App extends React.Component<any, AppState> {
const dx = x - elementsCenterX;
const dy = y - elementsCenterY;
const newElements = clipboardElements.map(clipboardElements => {
const duplicate = duplicateElement(clipboardElements);
duplicate.x += dx - minX;
duplicate.y += dy - minY;
return duplicate;
});
const newElements = clipboardElements.map(element =>
duplicateElement(element, {
x: element.x + dx - minX,
y: element.y + dy - minY,
}),
);
globalSceneState.replaceAllElements([
...globalSceneState.getAllElements(),

View file

@ -5,6 +5,7 @@ import { getSelectedElements } from "../scene";
import "./HintViewer.scss";
import { AppState } from "../types";
import { isLinearElement } from "../element/typeChecks";
interface Hint {
appState: AppState;
@ -23,12 +24,8 @@ const getHints = ({ appState, elements }: Hint) => {
if (isResizing) {
const selectedElements = getSelectedElements(elements, appState);
if (
selectedElements.length === 1 &&
(selectedElements[0].type === "arrow" ||
selectedElements[0].type === "line") &&
selectedElements[0].points.length > 2
) {
const targetElement = selectedElements[0];
if (isLinearElement(targetElement) && targetElement.points.length > 2) {
return null;
}
return t("hints.resize");