mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
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:
parent
1c545c1d47
commit
373d16abe6
22 changed files with 430 additions and 272 deletions
|
@ -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(),
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue