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
@ -11,9 +11,10 @@ export const actionDuplicateSelection = register({
|
|||||||
elements: elements.reduce(
|
elements: elements.reduce(
|
||||||
(acc: Array<ExcalidrawElement>, element: ExcalidrawElement) => {
|
(acc: Array<ExcalidrawElement>, element: ExcalidrawElement) => {
|
||||||
if (appState.selectedElementIds[element.id]) {
|
if (appState.selectedElementIds[element.id]) {
|
||||||
const newElement = duplicateElement(element);
|
const newElement = duplicateElement(element, {
|
||||||
newElement.x = newElement.x + 10;
|
x: element.x + 10,
|
||||||
newElement.y = newElement.y + 10;
|
y: element.y + 10,
|
||||||
|
});
|
||||||
appState.selectedElementIds[newElement.id] = true;
|
appState.selectedElementIds[newElement.id] = true;
|
||||||
delete appState.selectedElementIds[element.id];
|
delete appState.selectedElementIds[element.id];
|
||||||
return acc.concat([element, newElement]);
|
return acc.concat([element, newElement]);
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
getDrawingVersion,
|
getDrawingVersion,
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
hasNonDeletedElements,
|
hasNonDeletedElements,
|
||||||
|
newLinearElement,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import {
|
import {
|
||||||
deleteSelectedElements,
|
deleteSelectedElements,
|
||||||
@ -47,7 +48,7 @@ import { restore } from "../data/restore";
|
|||||||
|
|
||||||
import { renderScene } from "../renderer";
|
import { renderScene } from "../renderer";
|
||||||
import { AppState, GestureEvent, Gesture } from "../types";
|
import { AppState, GestureEvent, Gesture } from "../types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement, ExcalidrawLinearElement } from "../element/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isWritableElement,
|
isWritableElement,
|
||||||
@ -99,6 +100,7 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
|
|||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
import { unstable_batchedUpdates } from "react-dom";
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
import { SceneStateCallbackRemover } from "../scene/globalScene";
|
import { SceneStateCallbackRemover } from "../scene/globalScene";
|
||||||
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
function withBatchedUpdates<
|
function withBatchedUpdates<
|
||||||
@ -707,21 +709,18 @@ export class App extends React.Component<any, AppState> {
|
|||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
const element = newTextElement(
|
const element = newTextElement({
|
||||||
newElement(
|
x: x,
|
||||||
"text",
|
y: y,
|
||||||
x,
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
y,
|
backgroundColor: this.state.currentItemBackgroundColor,
|
||||||
this.state.currentItemStrokeColor,
|
fillStyle: this.state.currentItemFillStyle,
|
||||||
this.state.currentItemBackgroundColor,
|
strokeWidth: this.state.currentItemStrokeWidth,
|
||||||
this.state.currentItemFillStyle,
|
roughness: this.state.currentItemRoughness,
|
||||||
this.state.currentItemStrokeWidth,
|
opacity: this.state.currentItemOpacity,
|
||||||
this.state.currentItemRoughness,
|
text: data.text,
|
||||||
this.state.currentItemOpacity,
|
font: this.state.currentItemFont,
|
||||||
),
|
});
|
||||||
data.text,
|
|
||||||
this.state.currentItemFont,
|
|
||||||
);
|
|
||||||
|
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getAllElements(),
|
||||||
@ -960,21 +959,18 @@ export class App extends React.Component<any, AppState> {
|
|||||||
const element =
|
const element =
|
||||||
elementAtPosition && isTextElement(elementAtPosition)
|
elementAtPosition && isTextElement(elementAtPosition)
|
||||||
? elementAtPosition
|
? elementAtPosition
|
||||||
: newTextElement(
|
: newTextElement({
|
||||||
newElement(
|
x: x,
|
||||||
"text",
|
y: y,
|
||||||
x,
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
y,
|
backgroundColor: this.state.currentItemBackgroundColor,
|
||||||
this.state.currentItemStrokeColor,
|
fillStyle: this.state.currentItemFillStyle,
|
||||||
this.state.currentItemBackgroundColor,
|
strokeWidth: this.state.currentItemStrokeWidth,
|
||||||
this.state.currentItemFillStyle,
|
roughness: this.state.currentItemRoughness,
|
||||||
this.state.currentItemStrokeWidth,
|
opacity: this.state.currentItemOpacity,
|
||||||
this.state.currentItemRoughness,
|
text: "",
|
||||||
this.state.currentItemOpacity,
|
font: this.state.currentItemFont,
|
||||||
),
|
});
|
||||||
"", // default text
|
|
||||||
this.state.currentItemFont, // default font
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setState({ editingElement: element });
|
this.setState({ editingElement: element });
|
||||||
|
|
||||||
@ -1044,11 +1040,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
if (text) {
|
if (text) {
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getAllElements(),
|
||||||
{
|
// we need to recreate the element to update dimensions & position
|
||||||
// we need to recreate the element to update dimensions &
|
newTextElement({ ...element, text, font: element.font }),
|
||||||
// position
|
|
||||||
...newTextElement(element, text, element.font),
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
@ -1332,22 +1325,6 @@ export class App extends React.Component<any, AppState> {
|
|||||||
const originX = x;
|
const originX = x;
|
||||||
const originY = y;
|
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>;
|
type ResizeTestType = ReturnType<typeof resizeTest>;
|
||||||
let resizeHandle: ResizeTestType = false;
|
let resizeHandle: ResizeTestType = false;
|
||||||
let isResizingElements = false;
|
let isResizingElements = false;
|
||||||
@ -1437,30 +1414,30 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.setState({ selectedElementIds: {} });
|
this.setState({ selectedElementIds: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (this.state.elementType === "text") {
|
||||||
// if we're currently still editing text, clicking outside
|
// if we're currently still editing text, clicking outside
|
||||||
// should only finalize it, not create another (irrespective
|
// should only finalize it, not create another (irrespective
|
||||||
// of state.elementLocked)
|
// of state.elementLocked)
|
||||||
if (this.state.editingElement?.type === "text") {
|
if (this.state.editingElement?.type === "text") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (elementIsAddedToSelection) {
|
|
||||||
element = hitElement!;
|
const snappedToCenterPosition = event.altKey
|
||||||
}
|
? null
|
||||||
let textX = event.clientX;
|
: this.getTextWysiwygSnappedToCenterPosition(x, y);
|
||||||
let textY = event.clientY;
|
|
||||||
if (!event.altKey) {
|
const element = newTextElement({
|
||||||
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
|
x: snappedToCenterPosition?.elementCenterX ?? x,
|
||||||
x,
|
y: snappedToCenterPosition?.elementCenterY ?? y,
|
||||||
y,
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
);
|
backgroundColor: this.state.currentItemBackgroundColor,
|
||||||
if (snappedToCenterPosition) {
|
fillStyle: this.state.currentItemFillStyle,
|
||||||
element.x = snappedToCenterPosition.elementCenterX;
|
strokeWidth: this.state.currentItemStrokeWidth,
|
||||||
element.y = snappedToCenterPosition.elementCenterY;
|
roughness: this.state.currentItemRoughness,
|
||||||
textX = snappedToCenterPosition.wysiwygX;
|
opacity: this.state.currentItemOpacity,
|
||||||
textY = snappedToCenterPosition.wysiwygY;
|
text: "",
|
||||||
}
|
font: this.state.currentItemFont,
|
||||||
}
|
});
|
||||||
|
|
||||||
const resetSelection = () => {
|
const resetSelection = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -1471,8 +1448,8 @@ export class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
textWysiwyg({
|
textWysiwyg({
|
||||||
initText: "",
|
initText: "",
|
||||||
x: textX,
|
x: snappedToCenterPosition?.wysiwygX ?? event.clientX,
|
||||||
y: textY,
|
y: snappedToCenterPosition?.wysiwygY ?? event.clientY,
|
||||||
strokeColor: this.state.currentItemStrokeColor,
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
font: this.state.currentItemFont,
|
font: this.state.currentItemFont,
|
||||||
@ -1481,9 +1458,11 @@ export class App extends React.Component<any, AppState> {
|
|||||||
if (text) {
|
if (text) {
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getAllElements(),
|
||||||
{
|
newTextElement({
|
||||||
...newTextElement(element, text, this.state.currentItemFont),
|
...element,
|
||||||
},
|
text,
|
||||||
|
font: this.state.currentItemFont,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
@ -1531,6 +1510,17 @@ export class App extends React.Component<any, AppState> {
|
|||||||
points: [...multiElement.points, [x - rx, y - ry]],
|
points: [...multiElement.points, [x - rx, y - ry]],
|
||||||
});
|
});
|
||||||
} else {
|
} 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 => ({
|
this.setState(prevState => ({
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
...prevState.selectedElementIds,
|
...prevState.selectedElementIds,
|
||||||
@ -1549,26 +1539,40 @@ export class App extends React.Component<any, AppState> {
|
|||||||
editingElement: element,
|
editingElement: element,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (element.type === "selection") {
|
|
||||||
this.setState({
|
|
||||||
selectionElement: element,
|
|
||||||
draggingElement: element,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
globalSceneState.replaceAllElements([
|
const element = newElement({
|
||||||
...globalSceneState.getAllElements(),
|
type: this.state.elementType,
|
||||||
element,
|
x: x,
|
||||||
]);
|
y: y,
|
||||||
this.setState({
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
multiElement: null,
|
backgroundColor: this.state.currentItemBackgroundColor,
|
||||||
draggingElement: element,
|
fillStyle: this.state.currentItemFillStyle,
|
||||||
editingElement: element,
|
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:
|
let resizeArrowFn:
|
||||||
| ((
|
| ((
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawLinearElement,
|
||||||
pointIndex: number,
|
pointIndex: number,
|
||||||
deltaX: number,
|
deltaX: number,
|
||||||
deltaY: number,
|
deltaY: number,
|
||||||
@ -1579,7 +1583,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
| null = null;
|
| null = null;
|
||||||
|
|
||||||
const arrowResizeOrigin = (
|
const arrowResizeOrigin = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawLinearElement,
|
||||||
pointIndex: number,
|
pointIndex: number,
|
||||||
deltaX: number,
|
deltaX: number,
|
||||||
deltaY: number,
|
deltaY: number,
|
||||||
@ -1604,7 +1608,9 @@ export class App extends React.Component<any, AppState> {
|
|||||||
x: dx,
|
x: dx,
|
||||||
y: dy,
|
y: dy,
|
||||||
points: element.points.map((point, i) =>
|
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 {
|
} else {
|
||||||
@ -1612,14 +1618,16 @@ export class App extends React.Component<any, AppState> {
|
|||||||
x: element.x + deltaX,
|
x: element.x + deltaX,
|
||||||
y: element.y + deltaY,
|
y: element.y + deltaY,
|
||||||
points: element.points.map((point, i) =>
|
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 = (
|
const arrowResizeEnd = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawLinearElement,
|
||||||
pointIndex: number,
|
pointIndex: number,
|
||||||
deltaX: number,
|
deltaX: number,
|
||||||
deltaY: number,
|
deltaY: number,
|
||||||
@ -1636,13 +1644,15 @@ export class App extends React.Component<any, AppState> {
|
|||||||
);
|
);
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: element.points.map((point, i) =>
|
points: element.points.map((point, i) =>
|
||||||
i === pointIndex ? [width, height] : point,
|
i === pointIndex ? ([width, height] as const) : point,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: element.points.map((point, i) =>
|
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 deltaX = x - lastX;
|
||||||
const deltaY = y - lastY;
|
const deltaY = y - lastY;
|
||||||
const element = selectedElements[0];
|
const element = selectedElements[0];
|
||||||
const isLinear = element.type === "line" || element.type === "arrow";
|
|
||||||
switch (resizeHandle) {
|
switch (resizeHandle) {
|
||||||
case "nw":
|
case "nw":
|
||||||
if (isLinear && element.points.length === 2) {
|
if (isLinearElement(element) && element.points.length === 2) {
|
||||||
const [, p1] = element.points;
|
const [, p1] = element.points;
|
||||||
|
|
||||||
if (!resizeArrowFn) {
|
if (!resizeArrowFn) {
|
||||||
@ -1739,7 +1748,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ne":
|
case "ne":
|
||||||
if (isLinear && element.points.length === 2) {
|
if (isLinearElement(element) && element.points.length === 2) {
|
||||||
const [, p1] = element.points;
|
const [, p1] = element.points;
|
||||||
if (!resizeArrowFn) {
|
if (!resizeArrowFn) {
|
||||||
if (p1[0] >= 0) {
|
if (p1[0] >= 0) {
|
||||||
@ -1761,7 +1770,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "sw":
|
case "sw":
|
||||||
if (isLinear && element.points.length === 2) {
|
if (isLinearElement(element) && element.points.length === 2) {
|
||||||
const [, p1] = element.points;
|
const [, p1] = element.points;
|
||||||
if (!resizeArrowFn) {
|
if (!resizeArrowFn) {
|
||||||
if (p1[0] <= 0) {
|
if (p1[0] <= 0) {
|
||||||
@ -1782,7 +1791,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "se":
|
case "se":
|
||||||
if (isLinear && element.points.length === 2) {
|
if (isLinearElement(element) && element.points.length === 2) {
|
||||||
const [, p1] = element.points;
|
const [, p1] = element.points;
|
||||||
if (!resizeArrowFn) {
|
if (!resizeArrowFn) {
|
||||||
if (p1[0] > 0 || p1[1] > 0) {
|
if (p1[0] > 0 || p1[1] > 0) {
|
||||||
@ -1807,14 +1816,18 @@ export class App extends React.Component<any, AppState> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, {
|
if (isLinearElement(element)) {
|
||||||
height,
|
mutateElement(element, {
|
||||||
y: element.y + deltaY,
|
height,
|
||||||
points:
|
y: element.y + deltaY,
|
||||||
element.points.length > 0
|
points: rescalePoints(1, height, element.points),
|
||||||
? rescalePoints(1, height, element.points)
|
});
|
||||||
: undefined,
|
} else {
|
||||||
});
|
mutateElement(element, {
|
||||||
|
height,
|
||||||
|
y: element.y + deltaY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
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.
|
// Someday we should implement logic to flip the shape. But for now, just stop.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (isLinearElement(element)) {
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
width,
|
width,
|
||||||
x: element.x + deltaX,
|
x: element.x + deltaX,
|
||||||
points:
|
points: rescalePoints(0, width, element.points),
|
||||||
element.points.length > 0
|
});
|
||||||
? rescalePoints(0, width, element.points)
|
} else {
|
||||||
: undefined,
|
mutateElement(element, {
|
||||||
});
|
width,
|
||||||
|
x: element.x + deltaX,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "s": {
|
case "s": {
|
||||||
@ -1842,14 +1858,16 @@ export class App extends React.Component<any, AppState> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, {
|
if (isLinearElement(element)) {
|
||||||
height,
|
mutateElement(element, {
|
||||||
points:
|
height,
|
||||||
element.points.length > 0
|
points: rescalePoints(1, height, element.points),
|
||||||
? rescalePoints(1, height, element.points)
|
});
|
||||||
: undefined,
|
} else {
|
||||||
});
|
mutateElement(element, {
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "e": {
|
case "e": {
|
||||||
@ -1858,13 +1876,16 @@ export class App extends React.Component<any, AppState> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, {
|
if (isLinearElement(element)) {
|
||||||
width,
|
mutateElement(element, {
|
||||||
points:
|
width,
|
||||||
element.points.length > 0
|
points: rescalePoints(0, width, element.points),
|
||||||
? rescalePoints(0, width, element.points)
|
});
|
||||||
: undefined,
|
} else {
|
||||||
});
|
mutateElement(element, {
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1934,10 +1955,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
let width = distance(originX, x);
|
let width = distance(originX, x);
|
||||||
let height = distance(originY, y);
|
let height = distance(originY, y);
|
||||||
|
|
||||||
const isLinear =
|
if (isLinearElement(draggingElement)) {
|
||||||
this.state.elementType === "line" || this.state.elementType === "arrow";
|
|
||||||
|
|
||||||
if (isLinear) {
|
|
||||||
draggingOccurred = true;
|
draggingOccurred = true;
|
||||||
const points = draggingElement.points;
|
const points = draggingElement.points;
|
||||||
let dx = x - draggingElement.x;
|
let dx = x - draggingElement.x;
|
||||||
@ -2023,7 +2041,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
window.removeEventListener("pointermove", onPointerMove);
|
window.removeEventListener("pointermove", onPointerMove);
|
||||||
window.removeEventListener("pointerup", onPointerUp);
|
window.removeEventListener("pointerup", onPointerUp);
|
||||||
|
|
||||||
if (elementType === "arrow" || elementType === "line") {
|
if (isLinearElement(draggingElement)) {
|
||||||
if (draggingElement!.points.length > 1) {
|
if (draggingElement!.points.length > 1) {
|
||||||
history.resumeRecording();
|
history.resumeRecording();
|
||||||
}
|
}
|
||||||
@ -2041,7 +2059,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
this.setState({
|
this.setState({
|
||||||
multiElement: this.state.draggingElement,
|
multiElement: draggingElement,
|
||||||
editingElement: this.state.draggingElement,
|
editingElement: this.state.draggingElement,
|
||||||
});
|
});
|
||||||
} else if (draggingOccurred && !multiElement) {
|
} else if (draggingOccurred && !multiElement) {
|
||||||
@ -2215,12 +2233,12 @@ export class App extends React.Component<any, AppState> {
|
|||||||
const dx = x - elementsCenterX;
|
const dx = x - elementsCenterX;
|
||||||
const dy = y - elementsCenterY;
|
const dy = y - elementsCenterY;
|
||||||
|
|
||||||
const newElements = clipboardElements.map(clipboardElements => {
|
const newElements = clipboardElements.map(element =>
|
||||||
const duplicate = duplicateElement(clipboardElements);
|
duplicateElement(element, {
|
||||||
duplicate.x += dx - minX;
|
x: element.x + dx - minX,
|
||||||
duplicate.y += dy - minY;
|
y: element.y + dy - minY,
|
||||||
return duplicate;
|
}),
|
||||||
});
|
);
|
||||||
|
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
...globalSceneState.getAllElements(),
|
...globalSceneState.getAllElements(),
|
||||||
|
@ -5,6 +5,7 @@ import { getSelectedElements } from "../scene";
|
|||||||
|
|
||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
|
|
||||||
interface Hint {
|
interface Hint {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -23,12 +24,8 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
|
|
||||||
if (isResizing) {
|
if (isResizing) {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
if (
|
const targetElement = selectedElements[0];
|
||||||
selectedElements.length === 1 &&
|
if (isLinearElement(targetElement) && targetElement.points.length > 2) {
|
||||||
(selectedElements[0].type === "arrow" ||
|
|
||||||
selectedElements[0].type === "line") &&
|
|
||||||
selectedElements[0].points.length > 2
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return t("hints.resize");
|
return t("hints.resize");
|
||||||
|
@ -8,7 +8,9 @@ import nanoid from "nanoid";
|
|||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
|
|
||||||
export function restore(
|
export function restore(
|
||||||
savedElements: readonly ExcalidrawElement[],
|
// we're making the elements mutable for this API because we want to
|
||||||
|
// efficiently remove/tweak properties on them (to migrate old scenes)
|
||||||
|
savedElements: readonly Mutable<ExcalidrawElement>[],
|
||||||
savedState: AppState | null,
|
savedState: AppState | null,
|
||||||
opts?: { scrollToContent: boolean },
|
opts?: { scrollToContent: boolean },
|
||||||
): DataState {
|
): DataState {
|
||||||
@ -35,6 +37,7 @@ export function restore(
|
|||||||
[element.width, element.height],
|
[element.width, element.height],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
element.points = points;
|
||||||
} else if (element.type === "line") {
|
} else if (element.type === "line") {
|
||||||
// old spec, pre-arrows
|
// old spec, pre-arrows
|
||||||
// old spec, post-arrows
|
// old spec, post-arrows
|
||||||
@ -46,8 +49,13 @@ export function restore(
|
|||||||
} else {
|
} else {
|
||||||
points = element.points;
|
points = element.points;
|
||||||
}
|
}
|
||||||
|
element.points = points;
|
||||||
} else {
|
} else {
|
||||||
normalizeDimensions(element);
|
normalizeDimensions(element);
|
||||||
|
// old spec, where non-linear elements used to have empty points arrays
|
||||||
|
if ("points" in element) {
|
||||||
|
delete element.points;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -62,7 +70,6 @@ export function restore(
|
|||||||
element.opacity === null || element.opacity === undefined
|
element.opacity === null || element.opacity === undefined
|
||||||
? 100
|
? 100
|
||||||
: element.opacity,
|
: element.opacity,
|
||||||
points,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { ExcalidrawElement } from "./types";
|
|||||||
|
|
||||||
const _ce = ({ x, y, w, h }: { x: number; y: number; w: number; h: number }) =>
|
const _ce = ({ x, y, w, h }: { x: number; y: number; w: number; h: number }) =>
|
||||||
({
|
({
|
||||||
type: "test",
|
type: "rectangle",
|
||||||
strokeColor: "#000",
|
strokeColor: "#000",
|
||||||
backgroundColor: "#000",
|
backgroundColor: "#000",
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { ExcalidrawElement } from "./types";
|
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { Drawable } from "roughjs/bin/core";
|
import { Drawable } from "roughjs/bin/core";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
// If the element is created from right to left, the width is going to be negative
|
// If the element is created from right to left, the width is going to be negative
|
||||||
// This set of functions retrieves the absolute position of the 4 points.
|
// This set of functions retrieves the absolute position of the 4 points.
|
||||||
export function getElementAbsoluteCoords(element: ExcalidrawElement) {
|
export function getElementAbsoluteCoords(element: ExcalidrawElement) {
|
||||||
if (element.type === "arrow" || element.type === "line") {
|
if (isLinearElement(element)) {
|
||||||
return getLinearElementAbsoluteBounds(element);
|
return getLinearElementAbsoluteBounds(element);
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
@ -33,7 +34,9 @@ export function getDiamondPoints(element: ExcalidrawElement) {
|
|||||||
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
|
export function getLinearElementAbsoluteBounds(
|
||||||
|
element: ExcalidrawLinearElement,
|
||||||
|
) {
|
||||||
if (element.points.length < 2 || !getShapeForElement(element)) {
|
if (element.points.length < 2 || !getShapeForElement(element)) {
|
||||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||||
(limits, [x, y]) => {
|
(limits, [x, y]) => {
|
||||||
@ -119,7 +122,10 @@ export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getArrowPoints(element: ExcalidrawElement, shape: Drawable[]) {
|
export function getArrowPoints(
|
||||||
|
element: ExcalidrawLinearElement,
|
||||||
|
shape: Drawable[],
|
||||||
|
) {
|
||||||
const ops = shape[0].sets[0].ops;
|
const ops = shape[0].sets[0].ops;
|
||||||
|
|
||||||
const data = ops[ops.length - 1].data;
|
const data = ops[ops.length - 1].data;
|
||||||
|
@ -11,6 +11,7 @@ import { Point } from "../types";
|
|||||||
import { Drawable, OpSet } from "roughjs/bin/core";
|
import { Drawable, OpSet } from "roughjs/bin/core";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
function isElementDraggableFromInside(
|
function isElementDraggableFromInside(
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
@ -158,7 +159,7 @@ export function hitTest(
|
|||||||
distanceBetweenPointAndSegment(x, y, leftX, leftY, topX, topY) <
|
distanceBetweenPointAndSegment(x, y, leftX, leftY, topX, topY) <
|
||||||
lineThreshold
|
lineThreshold
|
||||||
);
|
);
|
||||||
} else if (element.type === "arrow" || element.type === "line") {
|
} else if (isLinearElement(element)) {
|
||||||
if (!getShapeForElement(element)) {
|
if (!getShapeForElement(element)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { ExcalidrawElement } from "./types";
|
import { ExcalidrawElement } from "./types";
|
||||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||||
|
|
||||||
export { newElement, newTextElement, duplicateElement } from "./newElement";
|
export {
|
||||||
|
newElement,
|
||||||
|
newTextElement,
|
||||||
|
newLinearElement,
|
||||||
|
duplicateElement,
|
||||||
|
} from "./newElement";
|
||||||
export {
|
export {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
|
@ -13,33 +13,36 @@ type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
|||||||
// The version is used to compare updates when more than one user is working in
|
// The version is used to compare updates when more than one user is working in
|
||||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||||
export function mutateElement<TElement extends ExcalidrawElement>(
|
export function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
) {
|
) {
|
||||||
const mutableElement = element as any;
|
// casting to any because can't use `in` operator
|
||||||
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
|
const { points } = updates as any;
|
||||||
|
|
||||||
if (typeof updates.points !== "undefined") {
|
if (typeof points !== "undefined") {
|
||||||
updates = { ...getSizeFromPoints(updates.points!), ...updates };
|
updates = { ...getSizeFromPoints(points), ...updates };
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in updates) {
|
for (const key in updates) {
|
||||||
const value = (updates as any)[key];
|
const value = (updates as any)[key];
|
||||||
if (typeof value !== "undefined") {
|
if (typeof value !== "undefined") {
|
||||||
mutableElement[key] = value;
|
// @ts-ignore
|
||||||
|
element[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof updates.height !== "undefined" ||
|
typeof updates.height !== "undefined" ||
|
||||||
typeof updates.width !== "undefined" ||
|
typeof updates.width !== "undefined" ||
|
||||||
typeof updates.points !== "undefined"
|
typeof points !== "undefined"
|
||||||
) {
|
) {
|
||||||
invalidateShapeForElement(element);
|
invalidateShapeForElement(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableElement.version++;
|
element.version++;
|
||||||
mutableElement.versionNonce = randomSeed();
|
element.versionNonce = randomSeed();
|
||||||
|
|
||||||
globalSceneState.informMutation();
|
globalSceneState.informMutation();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { newElement, newTextElement, duplicateElement } from "./newElement";
|
import {
|
||||||
|
newTextElement,
|
||||||
|
duplicateElement,
|
||||||
|
newLinearElement,
|
||||||
|
} from "./newElement";
|
||||||
|
import { mutateElement } from "./mutateElement";
|
||||||
|
|
||||||
function isPrimitive(val: any) {
|
function isPrimitive(val: any) {
|
||||||
const type = typeof val;
|
const type = typeof val;
|
||||||
@ -17,25 +22,27 @@ function assertCloneObjects(source: any, clone: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it("clones arrow element", () => {
|
it("clones arrow element", () => {
|
||||||
const element = newElement(
|
const element = newLinearElement({
|
||||||
"arrow",
|
type: "arrow",
|
||||||
0,
|
x: 0,
|
||||||
0,
|
y: 0,
|
||||||
"#000000",
|
strokeColor: "#000000",
|
||||||
"transparent",
|
backgroundColor: "transparent",
|
||||||
"hachure",
|
fillStyle: "hachure",
|
||||||
1,
|
strokeWidth: 1,
|
||||||
1,
|
roughness: 1,
|
||||||
100,
|
opacity: 100,
|
||||||
);
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
element.__proto__ = { hello: "world" };
|
element.__proto__ = { hello: "world" };
|
||||||
|
|
||||||
element.points = [
|
mutateElement(element, {
|
||||||
[1, 2],
|
points: [
|
||||||
[3, 4],
|
[1, 2],
|
||||||
];
|
[3, 4],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(element);
|
const copy = duplicateElement(element);
|
||||||
|
|
||||||
@ -59,17 +66,24 @@ it("clones arrow element", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("clones text element", () => {
|
it("clones text element", () => {
|
||||||
const element = newTextElement(
|
const element = newTextElement({
|
||||||
newElement("text", 0, 0, "#000000", "transparent", "hachure", 1, 1, 100),
|
x: 0,
|
||||||
"hello",
|
y: 0,
|
||||||
"Arial 20px",
|
strokeColor: "#000000",
|
||||||
);
|
backgroundColor: "transparent",
|
||||||
|
fillStyle: "hachure",
|
||||||
|
strokeWidth: 1,
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
text: "hello",
|
||||||
|
font: "Arial 20px",
|
||||||
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(element);
|
const copy = duplicateElement(element);
|
||||||
|
|
||||||
assertCloneObjects(element, copy);
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
expect(copy.points).not.toBe(element.points);
|
expect(copy).not.toHaveProperty("points");
|
||||||
expect(copy).not.toHaveProperty("shape");
|
expect(copy).not.toHaveProperty("shape");
|
||||||
expect(copy.id).not.toBe(element.id);
|
expect(copy.id).not.toBe(element.id);
|
||||||
expect(typeof copy.id).toBe("string");
|
expect(typeof copy.id).toBe("string");
|
||||||
|
@ -1,25 +1,45 @@
|
|||||||
import { randomSeed } from "roughjs/bin/math";
|
import { randomSeed } from "roughjs/bin/math";
|
||||||
import nanoid from "nanoid";
|
import nanoid from "nanoid";
|
||||||
import { Point } from "../types";
|
|
||||||
|
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawGenericElement,
|
||||||
|
} from "../element/types";
|
||||||
import { measureText } from "../utils";
|
import { measureText } from "../utils";
|
||||||
|
|
||||||
export function newElement(
|
type ElementConstructorOpts = {
|
||||||
type: string,
|
x: ExcalidrawGenericElement["x"];
|
||||||
x: number,
|
y: ExcalidrawGenericElement["y"];
|
||||||
y: number,
|
strokeColor: ExcalidrawGenericElement["strokeColor"];
|
||||||
strokeColor: string,
|
backgroundColor: ExcalidrawGenericElement["backgroundColor"];
|
||||||
backgroundColor: string,
|
fillStyle: ExcalidrawGenericElement["fillStyle"];
|
||||||
fillStyle: string,
|
strokeWidth: ExcalidrawGenericElement["strokeWidth"];
|
||||||
strokeWidth: number,
|
roughness: ExcalidrawGenericElement["roughness"];
|
||||||
roughness: number,
|
opacity: ExcalidrawGenericElement["opacity"];
|
||||||
opacity: number,
|
width?: ExcalidrawGenericElement["width"];
|
||||||
width = 0,
|
height?: ExcalidrawGenericElement["height"];
|
||||||
height = 0,
|
};
|
||||||
|
|
||||||
|
function _newElementBase<T extends ExcalidrawElement>(
|
||||||
|
type: T["type"],
|
||||||
|
{
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
strokeColor,
|
||||||
|
backgroundColor,
|
||||||
|
fillStyle,
|
||||||
|
strokeWidth,
|
||||||
|
roughness,
|
||||||
|
opacity,
|
||||||
|
width = 0,
|
||||||
|
height = 0,
|
||||||
|
...rest
|
||||||
|
}: ElementConstructorOpts & Partial<ExcalidrawGenericElement>,
|
||||||
) {
|
) {
|
||||||
const element = {
|
return {
|
||||||
id: nanoid(),
|
id: rest.id || nanoid(),
|
||||||
type,
|
type,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@ -31,29 +51,36 @@ export function newElement(
|
|||||||
strokeWidth,
|
strokeWidth,
|
||||||
roughness,
|
roughness,
|
||||||
opacity,
|
opacity,
|
||||||
seed: randomSeed(),
|
seed: rest.seed ?? randomSeed(),
|
||||||
points: [] as readonly Point[],
|
version: rest.version || 1,
|
||||||
version: 1,
|
versionNonce: rest.versionNonce ?? 0,
|
||||||
versionNonce: 0,
|
isDeleted: rest.isDeleted ?? false,
|
||||||
isDeleted: false,
|
|
||||||
};
|
};
|
||||||
return element;
|
}
|
||||||
|
|
||||||
|
export function newElement(
|
||||||
|
opts: {
|
||||||
|
type: ExcalidrawGenericElement["type"];
|
||||||
|
} & ElementConstructorOpts,
|
||||||
|
): ExcalidrawGenericElement {
|
||||||
|
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newTextElement(
|
export function newTextElement(
|
||||||
element: ExcalidrawElement,
|
opts: {
|
||||||
text: string,
|
text: string;
|
||||||
font: string,
|
font: string;
|
||||||
) {
|
} & ElementConstructorOpts,
|
||||||
|
): ExcalidrawTextElement {
|
||||||
|
const { text, font } = opts;
|
||||||
const metrics = measureText(text, font);
|
const metrics = measureText(text, font);
|
||||||
const textElement: ExcalidrawTextElement = {
|
const textElement = {
|
||||||
...element,
|
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||||
type: "text",
|
|
||||||
text: text,
|
text: text,
|
||||||
font: font,
|
font: font,
|
||||||
// Center the text
|
// Center the text
|
||||||
x: element.x - metrics.width / 2,
|
x: opts.x - metrics.width / 2,
|
||||||
y: element.y - metrics.height / 2,
|
y: opts.y - metrics.height / 2,
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
baseline: metrics.baseline,
|
baseline: metrics.baseline,
|
||||||
@ -62,6 +89,17 @@ export function newTextElement(
|
|||||||
return textElement;
|
return textElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function newLinearElement(
|
||||||
|
opts: {
|
||||||
|
type: "arrow" | "line";
|
||||||
|
} & ElementConstructorOpts,
|
||||||
|
): ExcalidrawLinearElement {
|
||||||
|
return {
|
||||||
|
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||||
|
points: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
|
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
|
||||||
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
|
||||||
//
|
//
|
||||||
@ -100,11 +138,15 @@ function _duplicateElement(val: any, depth: number = 0) {
|
|||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function duplicateElement(
|
export function duplicateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element: ReturnType<typeof newElement>,
|
element: TElement,
|
||||||
): ReturnType<typeof newElement> {
|
overrides?: Partial<TElement>,
|
||||||
const copy = _duplicateElement(element);
|
): TElement {
|
||||||
|
let copy: TElement = _duplicateElement(element);
|
||||||
copy.id = nanoid();
|
copy.id = nanoid();
|
||||||
copy.seed = randomSeed();
|
copy.seed = randomSeed();
|
||||||
|
if (overrides) {
|
||||||
|
copy = Object.assign(copy, overrides);
|
||||||
|
}
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { ExcalidrawElement, PointerType } from "./types";
|
|||||||
|
|
||||||
import { handlerRectangles } from "./handlerRectangles";
|
import { handlerRectangles } from "./handlerRectangles";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
|
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
|
||||||
|
|
||||||
@ -102,11 +103,7 @@ export function normalizeResizeHandle(
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
resizeHandle: HandlerRectanglesRet,
|
resizeHandle: HandlerRectanglesRet,
|
||||||
): HandlerRectanglesRet {
|
): HandlerRectanglesRet {
|
||||||
if (
|
if ((element.width >= 0 && element.height >= 0) || isLinearElement(element)) {
|
||||||
(element.width >= 0 && element.height >= 0) ||
|
|
||||||
element.type === "line" ||
|
|
||||||
element.type === "arrow"
|
|
||||||
) {
|
|
||||||
return resizeHandle;
|
return resizeHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ExcalidrawElement } from "./types";
|
import { ExcalidrawElement } from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
|
import { isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
|
export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
|
||||||
if (element.type === "arrow" || element.type === "line") {
|
if (isLinearElement(element)) {
|
||||||
return element.points.length < 2;
|
return element.points.length < 2;
|
||||||
}
|
}
|
||||||
return element.width === 0 && element.height === 0;
|
return element.width === 0 && element.height === 0;
|
||||||
@ -78,8 +79,7 @@ export function normalizeDimensions(
|
|||||||
if (
|
if (
|
||||||
!element ||
|
!element ||
|
||||||
(element.width >= 0 && element.height >= 0) ||
|
(element.width >= 0 && element.height >= 0) ||
|
||||||
element.type === "line" ||
|
isLinearElement(element)
|
||||||
element.type === "arrow"
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export function isTextElement(
|
export function isTextElement(
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
@ -6,6 +10,14 @@ export function isTextElement(
|
|||||||
return element.type === "text";
|
return element.type === "text";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isLinearElement(
|
||||||
|
element?: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawLinearElement {
|
||||||
|
return (
|
||||||
|
element != null && (element.type === "arrow" || element.type === "line")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isExcalidrawElement(element: any): boolean {
|
export function isExcalidrawElement(element: any): boolean {
|
||||||
return (
|
return (
|
||||||
element?.type === "text" ||
|
element?.type === "text" ||
|
||||||
|
@ -1,20 +1,49 @@
|
|||||||
import { newElement } from "./newElement";
|
import { Point } from "../types";
|
||||||
|
|
||||||
|
type _ExcalidrawElementBase = Readonly<{
|
||||||
|
id: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
strokeColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
fillStyle: string;
|
||||||
|
strokeWidth: number;
|
||||||
|
roughness: number;
|
||||||
|
opacity: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
seed: number;
|
||||||
|
version: number;
|
||||||
|
versionNonce: number;
|
||||||
|
isDeleted: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawGenericElement = _ExcalidrawElementBase & {
|
||||||
|
type: "selection" | "rectangle" | "diamond" | "ellipse";
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
||||||
* no computed data. The list of all ExcalidrawElements should be shareable
|
* no computed data. The list of all ExcalidrawElements should be shareable
|
||||||
* between peers and contain no state local to the peer.
|
* between peers and contain no state local to the peer.
|
||||||
*/
|
*/
|
||||||
export type ExcalidrawElement = Readonly<ReturnType<typeof newElement>>;
|
export type ExcalidrawElement =
|
||||||
|
| ExcalidrawGenericElement
|
||||||
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawLinearElement;
|
||||||
|
|
||||||
export type ExcalidrawTextElement = ExcalidrawElement &
|
export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "text";
|
type: "text";
|
||||||
font: string;
|
font: string;
|
||||||
text: string;
|
text: string;
|
||||||
// for backward compatibility
|
|
||||||
actualBoundingBoxAscent?: number;
|
|
||||||
baseline: number;
|
baseline: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
|
Readonly<{
|
||||||
|
type: "arrow" | "line";
|
||||||
|
points: Point[];
|
||||||
|
}>;
|
||||||
|
|
||||||
export type PointerType = "mouse" | "pen" | "touch";
|
export type PointerType = "mouse" | "pen" | "touch";
|
||||||
|
4
src/global.d.ts
vendored
4
src/global.d.ts
vendored
@ -5,3 +5,7 @@ interface Window {
|
|||||||
interface Clipboard extends EventTarget {
|
interface Clipboard extends EventTarget {
|
||||||
write(data: any[]): Promise<void>;
|
write(data: any[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Mutable<T> = {
|
||||||
|
-readonly [P in keyof T]: T[P];
|
||||||
|
};
|
||||||
|
@ -2,6 +2,7 @@ import { AppState } from "./types";
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
import { clearAppStatePropertiesForHistory } from "./appState";
|
import { clearAppStatePropertiesForHistory } from "./appState";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
|
import { isLinearElement } from "./element/typeChecks";
|
||||||
|
|
||||||
type Result = {
|
type Result = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -24,14 +25,17 @@ export class SceneHistory {
|
|||||||
) {
|
) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
appState: clearAppStatePropertiesForHistory(appState),
|
appState: clearAppStatePropertiesForHistory(appState),
|
||||||
elements: elements.map(element =>
|
elements: elements.map(element => {
|
||||||
newElementWith(element, {
|
if (isLinearElement(element)) {
|
||||||
points:
|
return newElementWith(element, {
|
||||||
appState.multiElement && appState.multiElement.id === element.id
|
points:
|
||||||
? element.points.slice(0, -1)
|
appState.multiElement && appState.multiElement.id === element.id
|
||||||
: element.points,
|
? element.points.slice(0, -1)
|
||||||
}),
|
: element.points,
|
||||||
),
|
});
|
||||||
|
}
|
||||||
|
return newElementWith(element, {});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export function rescalePoints(
|
|||||||
dimension: 0 | 1,
|
dimension: 0 | 1,
|
||||||
nextDimensionSize: number,
|
nextDimensionSize: number,
|
||||||
prevPoints: readonly Point[],
|
prevPoints: readonly Point[],
|
||||||
): readonly Point[] {
|
): Point[] {
|
||||||
const prevDimValues = prevPoints.map(point => point[dimension]);
|
const prevDimValues = prevPoints.map(point => point[dimension]);
|
||||||
const prevMaxDimension = Math.max(...prevDimValues);
|
const prevMaxDimension = Math.max(...prevDimValues);
|
||||||
const prevMinDimension = Math.min(...prevDimValues);
|
const prevMinDimension = Math.min(...prevDimValues);
|
||||||
|
@ -330,6 +330,7 @@ export function renderElement(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
// @ts-ignore
|
||||||
throw new Error(`Unimplemented type ${element.type}`);
|
throw new Error(`Unimplemented type ${element.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -420,6 +421,7 @@ export function renderElementToSvg(
|
|||||||
}
|
}
|
||||||
svgRoot.appendChild(node);
|
svgRoot.appendChild(node);
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
throw new Error(`Unimplemented type ${element.type}`);
|
throw new Error(`Unimplemented type ${element.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { App } from "../components/App";
|
|||||||
import * as Renderer from "../renderer/renderScene";
|
import * as Renderer from "../renderer/renderScene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { render, fireEvent } from "./test-utils";
|
import { render, fireEvent } from "./test-utils";
|
||||||
|
import { ExcalidrawLinearElement } from "../element/types";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@ -122,12 +123,15 @@ describe("add element to the scene when pointer dragging long enough", () => {
|
|||||||
expect(h.appState.selectionElement).toBeNull();
|
expect(h.appState.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.elements[0].type).toEqual("arrow");
|
|
||||||
expect(h.elements[0].x).toEqual(30);
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
expect(h.elements[0].y).toEqual(20);
|
|
||||||
expect(h.elements[0].points.length).toEqual(2);
|
expect(element.type).toEqual("arrow");
|
||||||
expect(h.elements[0].points[0]).toEqual([0, 0]);
|
expect(element.x).toEqual(30);
|
||||||
expect(h.elements[0].points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
|
expect(element.y).toEqual(20);
|
||||||
|
expect(element.points.length).toEqual(2);
|
||||||
|
expect(element.points[0]).toEqual([0, 0]);
|
||||||
|
expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
|
||||||
});
|
});
|
||||||
|
|
||||||
it("line", () => {
|
it("line", () => {
|
||||||
@ -151,12 +155,15 @@ describe("add element to the scene when pointer dragging long enough", () => {
|
|||||||
expect(h.appState.selectionElement).toBeNull();
|
expect(h.appState.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.elements[0].type).toEqual("line");
|
|
||||||
expect(h.elements[0].x).toEqual(30);
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
expect(h.elements[0].y).toEqual(20);
|
|
||||||
expect(h.elements[0].points.length).toEqual(2);
|
expect(element.type).toEqual("line");
|
||||||
expect(h.elements[0].points[0]).toEqual([0, 0]);
|
expect(element.x).toEqual(30);
|
||||||
expect(h.elements[0].points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
|
expect(element.y).toEqual(20);
|
||||||
|
expect(element.points.length).toEqual(2);
|
||||||
|
expect(element.points[0]).toEqual([0, 0]);
|
||||||
|
expect(element.points[1]).toEqual([30, 50]); // (60 - 30, 70 - 20)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { render, fireEvent } from "./test-utils";
|
|||||||
import { App } from "../components/App";
|
import { App } from "../components/App";
|
||||||
import * as Renderer from "../renderer/renderScene";
|
import * as Renderer from "../renderer/renderScene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
import { ExcalidrawLinearElement } from "../element/types";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@ -88,10 +89,12 @@ describe("multi point mode in linear elements", () => {
|
|||||||
expect(renderScene).toHaveBeenCalledTimes(10);
|
expect(renderScene).toHaveBeenCalledTimes(10);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
expect(h.elements[0].type).toEqual("arrow");
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
expect(h.elements[0].x).toEqual(30);
|
|
||||||
expect(h.elements[0].y).toEqual(30);
|
expect(element.type).toEqual("arrow");
|
||||||
expect(h.elements[0].points).toEqual([
|
expect(element.x).toEqual(30);
|
||||||
|
expect(element.y).toEqual(30);
|
||||||
|
expect(element.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[20, 30],
|
[20, 30],
|
||||||
[70, 110],
|
[70, 110],
|
||||||
@ -125,10 +128,12 @@ describe("multi point mode in linear elements", () => {
|
|||||||
expect(renderScene).toHaveBeenCalledTimes(10);
|
expect(renderScene).toHaveBeenCalledTimes(10);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
expect(h.elements[0].type).toEqual("line");
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
expect(h.elements[0].x).toEqual(30);
|
|
||||||
expect(h.elements[0].y).toEqual(30);
|
expect(element.type).toEqual("line");
|
||||||
expect(h.elements[0].points).toEqual([
|
expect(element.x).toEqual(30);
|
||||||
|
expect(element.y).toEqual(30);
|
||||||
|
expect(element.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[20, 30],
|
[20, 30],
|
||||||
[70, 110],
|
[70, 110],
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { ExcalidrawElement, PointerType } from "./element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
PointerType,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
} from "./element/types";
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
|
||||||
@ -8,7 +12,7 @@ export type Point = Readonly<RoughPoint>;
|
|||||||
export type AppState = {
|
export type AppState = {
|
||||||
draggingElement: ExcalidrawElement | null;
|
draggingElement: ExcalidrawElement | null;
|
||||||
resizingElement: ExcalidrawElement | null;
|
resizingElement: ExcalidrawElement | null;
|
||||||
multiElement: ExcalidrawElement | null;
|
multiElement: ExcalidrawLinearElement | null;
|
||||||
selectionElement: ExcalidrawElement | null;
|
selectionElement: ExcalidrawElement | null;
|
||||||
// element being edited, but not necessarily added to elements array yet
|
// element being edited, but not necessarily added to elements array yet
|
||||||
// (e.g. text element when typing into the input)
|
// (e.g. text element when typing into the input)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user