feat: improved freedraw (#3512)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
198800136e
commit
49c6bdd520
@ -35,6 +35,7 @@
|
|||||||
"nanoid": "3.1.22",
|
"nanoid": "3.1.22",
|
||||||
"open-color": "1.8.0",
|
"open-color": "1.8.0",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
|
"perfect-freehand": "0.4.7",
|
||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
"png-chunks-encode": "1.0.0",
|
"png-chunks-encode": "1.0.0",
|
||||||
"png-chunks-extract": "1.0.0",
|
"png-chunks-extract": "1.0.0",
|
||||||
|
@ -56,14 +56,14 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
const multiPointElement = appState.multiElement
|
const multiPointElement = appState.multiElement
|
||||||
? appState.multiElement
|
? appState.multiElement
|
||||||
: appState.editingElement?.type === "draw"
|
: appState.editingElement?.type === "freedraw"
|
||||||
? appState.editingElement
|
? appState.editingElement
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (multiPointElement) {
|
if (multiPointElement) {
|
||||||
// pen and mouse have hover
|
// pen and mouse have hover
|
||||||
if (
|
if (
|
||||||
multiPointElement.type !== "draw" &&
|
multiPointElement.type !== "freedraw" &&
|
||||||
appState.lastPointerDownWith !== "touch"
|
appState.lastPointerDownWith !== "touch"
|
||||||
) {
|
) {
|
||||||
const { points, lastCommittedPoint } = multiPointElement;
|
const { points, lastCommittedPoint } = multiPointElement;
|
||||||
@ -86,7 +86,7 @@ export const actionFinalize = register({
|
|||||||
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
||||||
if (
|
if (
|
||||||
multiPointElement.type === "line" ||
|
multiPointElement.type === "line" ||
|
||||||
multiPointElement.type === "draw"
|
multiPointElement.type === "freedraw"
|
||||||
) {
|
) {
|
||||||
if (isLoop) {
|
if (isLoop) {
|
||||||
const linePoints = multiPointElement.points;
|
const linePoints = multiPointElement.points;
|
||||||
@ -118,22 +118,24 @@ export const actionFinalize = register({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appState.elementLocked && appState.elementType !== "draw") {
|
if (!appState.elementLocked && appState.elementType !== "freedraw") {
|
||||||
appState.selectedElementIds[multiPointElement.id] = true;
|
appState.selectedElementIds[multiPointElement.id] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!appState.elementLocked && appState.elementType !== "draw") ||
|
(!appState.elementLocked && appState.elementType !== "freedraw") ||
|
||||||
!multiPointElement
|
!multiPointElement
|
||||||
) {
|
) {
|
||||||
resetCursor(canvas);
|
resetCursor(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: newElements,
|
elements: newElements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
elementType:
|
elementType:
|
||||||
(appState.elementLocked || appState.elementType === "draw") &&
|
(appState.elementLocked || appState.elementType === "freedraw") &&
|
||||||
multiPointElement
|
multiPointElement
|
||||||
? appState.elementType
|
? appState.elementType
|
||||||
: "selection",
|
: "selection",
|
||||||
@ -145,14 +147,14 @@ export const actionFinalize = register({
|
|||||||
selectedElementIds:
|
selectedElementIds:
|
||||||
multiPointElement &&
|
multiPointElement &&
|
||||||
!appState.elementLocked &&
|
!appState.elementLocked &&
|
||||||
appState.elementType !== "draw"
|
appState.elementType !== "freedraw"
|
||||||
? {
|
? {
|
||||||
...appState.selectedElementIds,
|
...appState.selectedElementIds,
|
||||||
[multiPointElement.id]: true,
|
[multiPointElement.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: appState.selectedElementIds,
|
||||||
},
|
},
|
||||||
commitToHistory: appState.elementType === "draw",
|
commitToHistory: appState.elementType === "freedraw",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event, appState) =>
|
keyTest: (event, appState) =>
|
||||||
|
@ -6,7 +6,7 @@ import { ExcalidrawElement, NonDeleted } from "../element/types";
|
|||||||
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getTransformHandles } from "../element/transformHandles";
|
import { getTransformHandles } from "../element/transformHandles";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
|
||||||
import { updateBoundElements } from "../element/binding";
|
import { updateBoundElements } from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ const flipElement = (
|
|||||||
const originalAngle = normalizeAngle(element.angle);
|
const originalAngle = normalizeAngle(element.angle);
|
||||||
|
|
||||||
let finalOffsetX = 0;
|
let finalOffsetX = 0;
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
finalOffsetX =
|
finalOffsetX =
|
||||||
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
||||||
element.width;
|
element.width;
|
||||||
|
@ -52,6 +52,7 @@ export type ActionName =
|
|||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
| "changeFillStyle"
|
| "changeFillStyle"
|
||||||
| "changeStrokeWidth"
|
| "changeStrokeWidth"
|
||||||
|
| "changeStrokeShape"
|
||||||
| "changeSloppiness"
|
| "changeSloppiness"
|
||||||
| "changeStrokeStyle"
|
| "changeStrokeStyle"
|
||||||
| "changeArrowhead"
|
| "changeArrowhead"
|
||||||
|
@ -9,7 +9,8 @@ import {
|
|||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
getTargetElements,
|
getTargetElements,
|
||||||
hasBackground,
|
hasBackground,
|
||||||
hasStroke,
|
hasStrokeStyle,
|
||||||
|
hasStrokeWidth,
|
||||||
hasText,
|
hasText,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
@ -53,10 +54,17 @@ export const SelectedShapeActions = ({
|
|||||||
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStroke(elementType) ||
|
{(hasStrokeWidth(elementType) ||
|
||||||
targetElements.some((element) => hasStroke(element.type))) && (
|
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
||||||
|
renderAction("changeStrokeWidth")}
|
||||||
|
|
||||||
|
{(elementType === "freedraw" ||
|
||||||
|
targetElements.some((element) => element.type === "freedraw")) &&
|
||||||
|
renderAction("changeStrokeShape")}
|
||||||
|
|
||||||
|
{(hasStrokeStyle(elementType) ||
|
||||||
|
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeStrokeWidth")}
|
|
||||||
{renderAction("changeStrokeStyle")}
|
{renderAction("changeStrokeStyle")}
|
||||||
{renderAction("changeSloppiness")}
|
{renderAction("changeSloppiness")}
|
||||||
</>
|
</>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Point, simplify } from "points-on-curve";
|
|
||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
@ -70,7 +69,7 @@ import {
|
|||||||
import { loadFromBlob } from "../data";
|
import { loadFromBlob } from "../data";
|
||||||
import { isValidLibrary } from "../data/json";
|
import { isValidLibrary } from "../data/json";
|
||||||
import Library from "../data/library";
|
import Library from "../data/library";
|
||||||
import { restore } from "../data/restore";
|
import { restore, restoreElements } from "../data/restore";
|
||||||
import {
|
import {
|
||||||
dragNewElement,
|
dragNewElement,
|
||||||
dragSelectedElements,
|
dragSelectedElements,
|
||||||
@ -111,7 +110,7 @@ import {
|
|||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import { deepCopyElement } from "../element/newElement";
|
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
||||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
@ -122,6 +121,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@ -1266,7 +1266,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
} else if (data.elements) {
|
} else if (data.elements) {
|
||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements: data.elements,
|
elements: restoreElements(data.elements),
|
||||||
position: "cursor",
|
position: "cursor",
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
@ -2341,7 +2341,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
this.state.elementType === "arrow" ||
|
this.state.elementType === "arrow" ||
|
||||||
this.state.elementType === "draw" ||
|
|
||||||
this.state.elementType === "line"
|
this.state.elementType === "line"
|
||||||
) {
|
) {
|
||||||
this.handleLinearElementOnPointerDown(
|
this.handleLinearElementOnPointerDown(
|
||||||
@ -2349,6 +2348,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.elementType,
|
this.state.elementType,
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
);
|
);
|
||||||
|
} else if (this.state.elementType === "freedraw") {
|
||||||
|
this.handleFreeDrawElementOnPointerDown(
|
||||||
|
event,
|
||||||
|
this.state.elementType,
|
||||||
|
pointerDownState,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.createGenericElementOnPointerDown(
|
this.createGenericElementOnPointerDown(
|
||||||
this.state.elementType,
|
this.state.elementType,
|
||||||
@ -2845,6 +2850,65 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleFreeDrawElementOnPointerDown = (
|
||||||
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
|
elementType: ExcalidrawFreeDrawElement["type"],
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
) => {
|
||||||
|
// Begin a mark capture. This does not have to update state yet.
|
||||||
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const element = newFreeDrawElement({
|
||||||
|
type: elementType,
|
||||||
|
x: gridX,
|
||||||
|
y: gridY,
|
||||||
|
strokeColor: this.state.currentItemStrokeColor,
|
||||||
|
backgroundColor: this.state.currentItemBackgroundColor,
|
||||||
|
fillStyle: this.state.currentItemFillStyle,
|
||||||
|
strokeWidth: this.state.currentItemStrokeWidth,
|
||||||
|
strokeStyle: this.state.currentItemStrokeStyle,
|
||||||
|
roughness: this.state.currentItemRoughness,
|
||||||
|
opacity: this.state.currentItemOpacity,
|
||||||
|
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
||||||
|
simulatePressure: event.pressure === 0.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
selectedElementIds: {
|
||||||
|
...prevState.selectedElementIds,
|
||||||
|
[element.id]: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const pressures = element.simulatePressure
|
||||||
|
? element.pressures
|
||||||
|
: [...element.pressures, event.pressure];
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
points: [[0, 0]],
|
||||||
|
pressures,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundElement = getHoveredElementForBinding(
|
||||||
|
pointerDownState.origin,
|
||||||
|
this.scene,
|
||||||
|
);
|
||||||
|
this.scene.replaceAllElements([
|
||||||
|
...this.scene.getElementsIncludingDeleted(),
|
||||||
|
element,
|
||||||
|
]);
|
||||||
|
this.setState({
|
||||||
|
draggingElement: element,
|
||||||
|
editingElement: element,
|
||||||
|
startBoundElement: boundElement,
|
||||||
|
suggestedBindings: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private handleLinearElementOnPointerDown = (
|
private handleLinearElementOnPointerDown = (
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
elementType: ExcalidrawLinearElement["type"],
|
elementType: ExcalidrawLinearElement["type"],
|
||||||
@ -2899,7 +2963,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerDownState.origin.x,
|
pointerDownState.origin.x,
|
||||||
pointerDownState.origin.y,
|
pointerDownState.origin.y,
|
||||||
elementType === "draw" ? null : this.state.gridSize,
|
this.state.gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
/* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
|
/* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
|
||||||
@ -3107,6 +3171,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const hasHitASelectedElement = pointerDownState.hit.allHitElements.some(
|
const hasHitASelectedElement = pointerDownState.hit.allHitElements.some(
|
||||||
(element) => this.isASelectedElement(element),
|
(element) => this.isASelectedElement(element),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasHitASelectedElement ||
|
hasHitASelectedElement ||
|
||||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
||||||
@ -3207,18 +3272,24 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLinearElement(draggingElement)) {
|
if (draggingElement.type === "freedraw") {
|
||||||
|
const points = draggingElement.points;
|
||||||
|
const dx = pointerCoords.x - draggingElement.x;
|
||||||
|
const dy = pointerCoords.y - draggingElement.y;
|
||||||
|
|
||||||
|
const pressures = draggingElement.simulatePressure
|
||||||
|
? draggingElement.pressures
|
||||||
|
: [...draggingElement.pressures, event.pressure];
|
||||||
|
|
||||||
|
mutateElement(draggingElement, {
|
||||||
|
points: [...points, [dx, dy]],
|
||||||
|
pressures,
|
||||||
|
});
|
||||||
|
} else if (isLinearElement(draggingElement)) {
|
||||||
pointerDownState.drag.hasOccurred = true;
|
pointerDownState.drag.hasOccurred = true;
|
||||||
const points = draggingElement.points;
|
const points = draggingElement.points;
|
||||||
let dx: number;
|
let dx = gridX - draggingElement.x;
|
||||||
let dy: number;
|
let dy = gridY - draggingElement.y;
|
||||||
if (draggingElement.type === "draw") {
|
|
||||||
dx = pointerCoords.x - draggingElement.x;
|
|
||||||
dy = pointerCoords.y - draggingElement.y;
|
|
||||||
} else {
|
|
||||||
dx = gridX - draggingElement.x;
|
|
||||||
dy = gridY - draggingElement.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getRotateWithDiscreteAngleKey(event) && points.length === 2) {
|
if (getRotateWithDiscreteAngleKey(event) && points.length === 2) {
|
||||||
({ width: dx, height: dy } = getPerfectElementSize(
|
({ width: dx, height: dy } = getPerfectElementSize(
|
||||||
@ -3231,19 +3302,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (points.length === 1) {
|
if (points.length === 1) {
|
||||||
mutateElement(draggingElement, { points: [...points, [dx, dy]] });
|
mutateElement(draggingElement, { points: [...points, [dx, dy]] });
|
||||||
} else if (points.length > 1) {
|
} else if (points.length > 1) {
|
||||||
if (draggingElement.type === "draw") {
|
mutateElement(draggingElement, {
|
||||||
mutateElement(draggingElement, {
|
points: [...points.slice(0, -1), [dx, dy]],
|
||||||
points: simplify(
|
});
|
||||||
[...(points as Point[]), [dx, dy]],
|
|
||||||
0.7 / this.state.zoom.value,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
mutateElement(draggingElement, {
|
|
||||||
points: [...points.slice(0, -1), [dx, dy]],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBindingElement(draggingElement)) {
|
if (isBindingElement(draggingElement)) {
|
||||||
// When creating a linear element by dragging
|
// When creating a linear element by dragging
|
||||||
this.maybeSuggestBindingForLinearElementAtCursor(
|
this.maybeSuggestBindingForLinearElementAtCursor(
|
||||||
@ -3383,8 +3446,33 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState.eventListeners.onKeyUp!,
|
pointerDownState.eventListeners.onKeyUp!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (draggingElement?.type === "draw") {
|
if (draggingElement?.type === "freedraw") {
|
||||||
|
const pointerCoords = viewportCoordsToSceneCoords(
|
||||||
|
childEvent,
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
const points = draggingElement.points;
|
||||||
|
let dx = pointerCoords.x - draggingElement.x;
|
||||||
|
let dy = pointerCoords.y - draggingElement.y;
|
||||||
|
|
||||||
|
// Allows dots to avoid being flagged as infinitely small
|
||||||
|
if (dx === points[0][0] && dy === points[0][1]) {
|
||||||
|
dy += 0.0001;
|
||||||
|
dx += 0.0001;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pressures = draggingElement.simulatePressure
|
||||||
|
? []
|
||||||
|
: [...draggingElement.pressures, childEvent.pressure];
|
||||||
|
|
||||||
|
mutateElement(draggingElement, {
|
||||||
|
points: [...points, [dx, dy]],
|
||||||
|
pressures,
|
||||||
|
});
|
||||||
|
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3428,7 +3516,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||||
if (!elementLocked && elementType !== "draw") {
|
if (!elementLocked) {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.canvas);
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
@ -3575,7 +3663,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!elementLocked && elementType !== "draw" && draggingElement) {
|
if (!elementLocked && elementType !== "freedraw" && draggingElement) {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
...prevState.selectedElementIds,
|
...prevState.selectedElementIds,
|
||||||
@ -3599,7 +3687,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!elementLocked && elementType !== "draw") {
|
if (!elementLocked && elementType !== "freedraw") {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.canvas);
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
|
@ -153,7 +153,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
||||||
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("toolBar.draw")}
|
label={t("toolBar.freedraw")}
|
||||||
shortcuts={["Shift+P", "7"]}
|
shortcuts={["Shift+P", "7"]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||||
|
@ -23,7 +23,7 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
return t("hints.linearElementMulti");
|
return t("hints.linearElementMulti");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elementType === "draw") {
|
if (elementType === "freedraw") {
|
||||||
return t("hints.freeDraw");
|
return t("hints.freeDraw");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,10 +37,12 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
|
|||||||
|
|
||||||
const restoreElementWithProperties = <T extends ExcalidrawElement>(
|
const restoreElementWithProperties = <T extends ExcalidrawElement>(
|
||||||
element: Required<T>,
|
element: Required<T>,
|
||||||
extra: Omit<Required<T>, keyof ExcalidrawElement>,
|
extra: Omit<Required<T>, keyof ExcalidrawElement> & {
|
||||||
|
type?: ExcalidrawElement["type"];
|
||||||
|
},
|
||||||
): T => {
|
): T => {
|
||||||
const base: Pick<T, keyof ExcalidrawElement> = {
|
const base: Pick<T, keyof ExcalidrawElement> = {
|
||||||
type: element.type,
|
type: extra.type || element.type,
|
||||||
// all elements must have version > 0 so getSceneVersion() will pick up
|
// all elements must have version > 0 so getSceneVersion() will pick up
|
||||||
// newly added elements
|
// newly added elements
|
||||||
version: element.version || 1,
|
version: element.version || 1,
|
||||||
@ -97,6 +99,14 @@ const restoreElement = (
|
|||||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||||
});
|
});
|
||||||
|
case "freedraw": {
|
||||||
|
return restoreElementWithProperties(element, {
|
||||||
|
points: element.points,
|
||||||
|
lastCommittedPoint: null,
|
||||||
|
simulatePressure: element.simulatePressure,
|
||||||
|
pressures: element.pressures,
|
||||||
|
});
|
||||||
|
}
|
||||||
case "draw":
|
case "draw":
|
||||||
case "line":
|
case "line":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
@ -106,6 +116,7 @@ const restoreElement = (
|
|||||||
} = element;
|
} = element;
|
||||||
|
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
|
type: element.type === "draw" ? "line" : element.type,
|
||||||
startBinding: element.startBinding,
|
startBinding: element.startBinding,
|
||||||
endBinding: element.endBinding,
|
endBinding: element.endBinding,
|
||||||
points:
|
points:
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { ExcalidrawElement, ExcalidrawLinearElement, Arrowhead } from "./types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
Arrowhead,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
} from "./types";
|
||||||
import { distance2d, rotate } from "../math";
|
import { distance2d, rotate } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { Drawable, Op } from "roughjs/bin/core";
|
import { Drawable, Op } from "roughjs/bin/core";
|
||||||
@ -7,7 +12,7 @@ import {
|
|||||||
getShapeForElement,
|
getShapeForElement,
|
||||||
generateRoughOptions,
|
generateRoughOptions,
|
||||||
} from "../renderer/renderElement";
|
} from "../renderer/renderElement";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
// x and y position of top left corner, x and y position of bottom right corner
|
// x and y position of top left corner, x and y position of bottom right corner
|
||||||
@ -18,7 +23,9 @@ export type Bounds = readonly [number, number, number, number];
|
|||||||
export const getElementAbsoluteCoords = (
|
export const getElementAbsoluteCoords = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
if (isLinearElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
|
return getFreeDrawElementAbsoluteCoords(element);
|
||||||
|
} else if (isLinearElement(element)) {
|
||||||
return getLinearElementAbsoluteCoords(element);
|
return getLinearElementAbsoluteCoords(element);
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
@ -120,9 +127,42 @@ const getMinMaxXYFromCurvePathOps = (
|
|||||||
return [minX, minY, maxX, maxY];
|
return [minX, minY, maxX, maxY];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBoundsFromPoints = (
|
||||||
|
points: ExcalidrawFreeDrawElement["points"],
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
|
||||||
|
for (const [x, y] of points) {
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [minX, minY, maxX, maxY];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFreeDrawElementAbsoluteCoords = (
|
||||||
|
element: ExcalidrawFreeDrawElement,
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
const [minX, minY, maxX, maxY] = getBoundsFromPoints(element.points);
|
||||||
|
|
||||||
|
return [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
const getLinearElementAbsoluteCoords = (
|
const getLinearElementAbsoluteCoords = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
|
let coords: [number, number, number, number];
|
||||||
|
|
||||||
if (element.points.length < 2 || !getShapeForElement(element)) {
|
if (element.points.length < 2 || !getShapeForElement(element)) {
|
||||||
// XXX this is just a poor estimate and not very useful
|
// XXX this is just a poor estimate and not very useful
|
||||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||||
@ -137,7 +177,21 @@ const getLinearElementAbsoluteCoords = (
|
|||||||
},
|
},
|
||||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||||
);
|
);
|
||||||
return [
|
coords = [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const shape = getShapeForElement(element) as Drawable[];
|
||||||
|
|
||||||
|
// first element is always the curve
|
||||||
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
|
||||||
|
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||||
|
|
||||||
|
coords = [
|
||||||
minX + element.x,
|
minX + element.x,
|
||||||
minY + element.y,
|
minY + element.y,
|
||||||
maxX + element.x,
|
maxX + element.x,
|
||||||
@ -145,19 +199,7 @@ const getLinearElementAbsoluteCoords = (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const shape = getShapeForElement(element) as Drawable[];
|
return coords;
|
||||||
|
|
||||||
// first element is always the curve
|
|
||||||
const ops = getCurvePathOps(shape[0]);
|
|
||||||
|
|
||||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
|
||||||
|
|
||||||
return [
|
|
||||||
minX + element.x,
|
|
||||||
minY + element.y,
|
|
||||||
maxX + element.x,
|
|
||||||
maxY + element.y,
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getArrowheadPoints = (
|
export const getArrowheadPoints = (
|
||||||
@ -231,7 +273,7 @@ export const getArrowheadPoints = (
|
|||||||
const ys = y2 - ny * minSize;
|
const ys = y2 - ny * minSize;
|
||||||
|
|
||||||
if (arrowhead === "dot") {
|
if (arrowhead === "dot") {
|
||||||
const r = Math.hypot(ys - y2, xs - x2);
|
const r = Math.hypot(ys - y2, xs - x2) + element.strokeWidth;
|
||||||
return [x2, y2, r];
|
return [x2, y2, r];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,16 +319,31 @@ const getLinearElementRotatedBounds = (
|
|||||||
return getMinMaxXYFromCurvePathOps(ops, transformXY);
|
return getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We could cache this stuff
|
||||||
export const getElementBounds = (
|
export const getElementBounds = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
|
let bounds: [number, number, number, number];
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
if (isLinearElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
return getLinearElementRotatedBounds(element, cx, cy);
|
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
||||||
}
|
element.points.map(([x, y]) =>
|
||||||
if (element.type === "diamond") {
|
rotate(x, y, cx - element.x, cy - element.y, element.angle),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
} else if (isLinearElement(element)) {
|
||||||
|
bounds = getLinearElementRotatedBounds(element, cx, cy);
|
||||||
|
} else if (element.type === "diamond") {
|
||||||
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
||||||
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
||||||
const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
|
const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
|
||||||
@ -295,26 +352,28 @@ export const getElementBounds = (
|
|||||||
const minY = Math.min(y11, y12, y22, y21);
|
const minY = Math.min(y11, y12, y22, y21);
|
||||||
const maxX = Math.max(x11, x12, x22, x21);
|
const maxX = Math.max(x11, x12, x22, x21);
|
||||||
const maxY = Math.max(y11, y12, y22, y21);
|
const maxY = Math.max(y11, y12, y22, y21);
|
||||||
return [minX, minY, maxX, maxY];
|
bounds = [minX, minY, maxX, maxY];
|
||||||
}
|
} else if (element.type === "ellipse") {
|
||||||
if (element.type === "ellipse") {
|
|
||||||
const w = (x2 - x1) / 2;
|
const w = (x2 - x1) / 2;
|
||||||
const h = (y2 - y1) / 2;
|
const h = (y2 - y1) / 2;
|
||||||
const cos = Math.cos(element.angle);
|
const cos = Math.cos(element.angle);
|
||||||
const sin = Math.sin(element.angle);
|
const sin = Math.sin(element.angle);
|
||||||
const ww = Math.hypot(w * cos, h * sin);
|
const ww = Math.hypot(w * cos, h * sin);
|
||||||
const hh = Math.hypot(h * cos, w * sin);
|
const hh = Math.hypot(h * cos, w * sin);
|
||||||
return [cx - ww, cy - hh, cx + ww, cy + hh];
|
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
||||||
|
} else {
|
||||||
|
const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
|
||||||
|
const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
|
||||||
|
const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
|
||||||
|
const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
|
||||||
|
const minX = Math.min(x11, x12, x22, x21);
|
||||||
|
const minY = Math.min(y11, y12, y22, y21);
|
||||||
|
const maxX = Math.max(x11, x12, x22, x21);
|
||||||
|
const maxY = Math.max(y11, y12, y22, y21);
|
||||||
|
bounds = [minX, minY, maxX, maxY];
|
||||||
}
|
}
|
||||||
const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
|
|
||||||
const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
|
return bounds;
|
||||||
const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
|
|
||||||
const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
|
|
||||||
const minX = Math.min(x11, x12, x22, x21);
|
|
||||||
const minY = Math.min(y11, y12, y22, y21);
|
|
||||||
const maxX = Math.max(x11, x12, x22, x21);
|
|
||||||
const maxY = Math.max(y11, y12, y22, y21);
|
|
||||||
return [minX, minY, maxX, maxY];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
@ -345,7 +404,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
if (!isLinearElement(element)) {
|
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
||||||
return [
|
return [
|
||||||
element.x,
|
element.x,
|
||||||
element.y,
|
element.y,
|
||||||
@ -360,16 +419,29 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
rescalePoints(1, nextHeight, element.points),
|
rescalePoints(1, nextHeight, element.points),
|
||||||
);
|
);
|
||||||
|
|
||||||
const gen = rough.generator();
|
let bounds: [number, number, number, number];
|
||||||
const curve =
|
|
||||||
element.strokeSharpness === "sharp"
|
if (isFreeDrawElement(element)) {
|
||||||
? gen.linearPath(
|
// Free Draw
|
||||||
points as [number, number][],
|
bounds = getBoundsFromPoints(points);
|
||||||
generateRoughOptions(element),
|
} else {
|
||||||
)
|
// Line
|
||||||
: gen.curve(points as [number, number][], generateRoughOptions(element));
|
const gen = rough.generator();
|
||||||
const ops = getCurvePathOps(curve);
|
const curve =
|
||||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
element.strokeSharpness === "sharp"
|
||||||
|
? gen.linearPath(
|
||||||
|
points as [number, number][],
|
||||||
|
generateRoughOptions(element),
|
||||||
|
)
|
||||||
|
: gen.curve(
|
||||||
|
points as [number, number][],
|
||||||
|
generateRoughOptions(element),
|
||||||
|
);
|
||||||
|
const ops = getCurvePathOps(curve);
|
||||||
|
bounds = getMinMaxXYFromCurvePathOps(ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [minX, minY, maxX, maxY] = bounds;
|
||||||
return [
|
return [
|
||||||
minX + element.x,
|
minX + element.x,
|
||||||
minY + element.y,
|
minY + element.y,
|
||||||
|
@ -4,7 +4,13 @@ import * as GADirection from "../gadirections";
|
|||||||
import * as GALine from "../galines";
|
import * as GALine from "../galines";
|
||||||
import * as GATransform from "../gatransforms";
|
import * as GATransform from "../gatransforms";
|
||||||
|
|
||||||
import { isPathALoop, isPointInPolygon, rotate } from "../math";
|
import {
|
||||||
|
distance2d,
|
||||||
|
rotatePoint,
|
||||||
|
isPathALoop,
|
||||||
|
isPointInPolygon,
|
||||||
|
rotate,
|
||||||
|
} from "../math";
|
||||||
import { pointsOnBezierCurves } from "points-on-curve";
|
import { pointsOnBezierCurves } from "points-on-curve";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,6 +22,7 @@ import {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
|
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
|
||||||
@ -30,10 +37,17 @@ const isElementDraggableFromInside = (
|
|||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (element.type === "freedraw") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const isDraggableFromInside = element.backgroundColor !== "transparent";
|
const isDraggableFromInside = element.backgroundColor !== "transparent";
|
||||||
if (element.type === "line" || element.type === "draw") {
|
|
||||||
|
if (element.type === "line") {
|
||||||
return isDraggableFromInside && isPathALoop(element.points);
|
return isDraggableFromInside && isPathALoop(element.points);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isDraggableFromInside;
|
return isDraggableFromInside;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,6 +95,7 @@ const isHittingElementNotConsideringBoundingBox = (
|
|||||||
: isElementDraggableFromInside(element)
|
: isElementDraggableFromInside(element)
|
||||||
? isInsideCheck
|
? isInsideCheck
|
||||||
: isNearCheck;
|
: isNearCheck;
|
||||||
|
|
||||||
return hitTestPointAgainstElement({ element, point, threshold, check });
|
return hitTestPointAgainstElement({ element, point, threshold, check });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -151,6 +166,18 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
|||||||
case "ellipse":
|
case "ellipse":
|
||||||
const distance = distanceToBindableElement(args.element, args.point);
|
const distance = distanceToBindableElement(args.element, args.point);
|
||||||
return args.check(distance, args.threshold);
|
return args.check(distance, args.threshold);
|
||||||
|
case "freedraw": {
|
||||||
|
if (
|
||||||
|
!args.check(
|
||||||
|
distanceToRectangle(args.element, args.point),
|
||||||
|
args.threshold,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hitTestFreeDrawElement(args.element, args.point, args.threshold);
|
||||||
|
}
|
||||||
case "arrow":
|
case "arrow":
|
||||||
case "line":
|
case "line":
|
||||||
case "draw":
|
case "draw":
|
||||||
@ -195,7 +222,10 @@ const isOutsideCheck = (distance: number, threshold: number): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const distanceToRectangle = (
|
const distanceToRectangle = (
|
||||||
element: ExcalidrawRectangleElement | ExcalidrawTextElement,
|
element:
|
||||||
|
| ExcalidrawRectangleElement
|
||||||
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawFreeDrawElement,
|
||||||
point: Point,
|
point: Point,
|
||||||
): number => {
|
): number => {
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
||||||
@ -267,6 +297,71 @@ const ellipseParamsForTest = (
|
|||||||
return [pointRel, tangent];
|
return [pointRel, tangent];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hitTestFreeDrawElement = (
|
||||||
|
element: ExcalidrawFreeDrawElement,
|
||||||
|
point: Point,
|
||||||
|
threshold: number,
|
||||||
|
): boolean => {
|
||||||
|
// Check point-distance-to-line-segment for every segment in the
|
||||||
|
// element's points (its input points, not its outline points).
|
||||||
|
// This is... okay? It's plenty fast, but the GA library may
|
||||||
|
// have a faster option.
|
||||||
|
|
||||||
|
let x: number;
|
||||||
|
let y: number;
|
||||||
|
|
||||||
|
if (element.angle === 0) {
|
||||||
|
x = point[0] - element.x;
|
||||||
|
y = point[1] - element.y;
|
||||||
|
} else {
|
||||||
|
// Counter-rotate the point around center before testing
|
||||||
|
const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(element);
|
||||||
|
const rotatedPoint = rotatePoint(
|
||||||
|
point,
|
||||||
|
[minX + (maxX - minX) / 2, minY + (maxY - minY) / 2],
|
||||||
|
-element.angle,
|
||||||
|
);
|
||||||
|
x = rotatedPoint[0] - element.x;
|
||||||
|
y = rotatedPoint[1] - element.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [A, B] = element.points;
|
||||||
|
let P: readonly [number, number];
|
||||||
|
|
||||||
|
// For freedraw dots
|
||||||
|
if (element.points.length === 2) {
|
||||||
|
return (
|
||||||
|
distance2d(A[0], A[1], x, y) < threshold ||
|
||||||
|
distance2d(B[0], B[1], x, y) < threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For freedraw lines
|
||||||
|
for (let i = 1; i < element.points.length - 1; i++) {
|
||||||
|
const delta = [B[0] - A[0], B[1] - A[1]];
|
||||||
|
const length = Math.hypot(delta[1], delta[0]);
|
||||||
|
|
||||||
|
const U = [delta[0] / length, delta[1] / length];
|
||||||
|
const C = [x - A[0], y - A[1]];
|
||||||
|
const d = (C[0] * U[0] + C[1] * U[1]) / Math.hypot(U[1], U[0]);
|
||||||
|
P = [A[0] + U[0] * d, A[1] + U[1] * d];
|
||||||
|
|
||||||
|
const da = distance2d(P[0], P[1], A[0], A[1]);
|
||||||
|
const db = distance2d(P[0], P[1], B[0], B[1]);
|
||||||
|
|
||||||
|
P = db < da && da > length ? B : da < db && db > length ? A : P;
|
||||||
|
|
||||||
|
if (Math.hypot(y - P[1], x - P[0]) < threshold) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
A = B;
|
||||||
|
B = element.points[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const hitTestLinear = (args: HitTestArgs): boolean => {
|
const hitTestLinear = (args: HitTestArgs): boolean => {
|
||||||
const { element, threshold } = args;
|
const { element, threshold } = args;
|
||||||
if (!getShapeForElement(element)) {
|
if (!getShapeForElement(element)) {
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
GroupId,
|
GroupId,
|
||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { measureText, getFontString } from "../utils";
|
import { measureText, getFontString } from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
@ -212,6 +213,22 @@ export const updateTextElement = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const newFreeDrawElement = (
|
||||||
|
opts: {
|
||||||
|
type: "freedraw";
|
||||||
|
points?: ExcalidrawFreeDrawElement["points"];
|
||||||
|
simulatePressure: boolean;
|
||||||
|
} & ElementConstructorOpts,
|
||||||
|
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
||||||
|
return {
|
||||||
|
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
||||||
|
points: opts.points || [],
|
||||||
|
pressures: [],
|
||||||
|
simulatePressure: opts.simulatePressure,
|
||||||
|
lastCommittedPoint: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const newLinearElement = (
|
export const newLinearElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawLinearElement["type"];
|
type: ExcalidrawLinearElement["type"];
|
||||||
|
@ -18,7 +18,11 @@ import {
|
|||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { isLinearElement, isTextElement } from "./typeChecks";
|
import {
|
||||||
|
isFreeDrawElement,
|
||||||
|
isLinearElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { measureText, getFontString } from "../utils";
|
import { measureText, getFontString } from "../utils";
|
||||||
@ -244,7 +248,7 @@ const rescalePointsInElement = (
|
|||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
) =>
|
) =>
|
||||||
isLinearElement(element)
|
isLinearElement(element) || isFreeDrawElement(element)
|
||||||
? {
|
? {
|
||||||
points: rescalePoints(
|
points: rescalePoints(
|
||||||
0,
|
0,
|
||||||
@ -404,7 +408,7 @@ export const resizeSingleElement = (
|
|||||||
-stateAtResizeStart.angle,
|
-stateAtResizeStart.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
//Get bounds corners rendered on screen
|
// Get bounds corners rendered on screen
|
||||||
const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
|
const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
element.width,
|
element.width,
|
||||||
@ -644,11 +648,14 @@ const resizeMultipleElements = (
|
|||||||
font = { fontSize: nextFont.size, baseline: nextFont.baseline };
|
font = { fontSize: nextFont.size, baseline: nextFont.baseline };
|
||||||
}
|
}
|
||||||
const origCoords = getElementAbsoluteCoords(element);
|
const origCoords = getElementAbsoluteCoords(element);
|
||||||
|
|
||||||
const rescaledPoints = rescalePointsInElement(element, width, height);
|
const rescaledPoints = rescalePointsInElement(element, width, height);
|
||||||
|
|
||||||
updateBoundElements(element, {
|
updateBoundElements(element, {
|
||||||
newSize: { width, height },
|
newSize: { width, height },
|
||||||
simultaneouslyUpdated: elements,
|
simultaneouslyUpdated: elements,
|
||||||
});
|
});
|
||||||
|
|
||||||
const finalCoords = getResizedElementAbsoluteCoords(
|
const finalCoords = getResizedElementAbsoluteCoords(
|
||||||
{
|
{
|
||||||
...element,
|
...element,
|
||||||
@ -657,6 +664,7 @@ const resizeMultipleElements = (
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { x, y } = getNextXY(element, origCoords, finalCoords);
|
const { x, y } = getNextXY(element, origCoords, finalCoords);
|
||||||
return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
|
return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { ExcalidrawElement } from "./types";
|
import { ExcalidrawElement } from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
|
|
||||||
export const isInvisiblySmallElement = (
|
export const isInvisiblySmallElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(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;
|
||||||
@ -26,7 +26,7 @@ export const getPerfectElementSize = (
|
|||||||
if (
|
if (
|
||||||
elementType === "line" ||
|
elementType === "line" ||
|
||||||
elementType === "arrow" ||
|
elementType === "arrow" ||
|
||||||
elementType === "draw"
|
elementType === "freedraw"
|
||||||
) {
|
) {
|
||||||
const lockedAngle =
|
const lockedAngle =
|
||||||
Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
|
Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
|
||||||
|
@ -225,7 +225,7 @@ export const getTransformHandles = (
|
|||||||
if (
|
if (
|
||||||
element.type === "arrow" ||
|
element.type === "arrow" ||
|
||||||
element.type === "line" ||
|
element.type === "line" ||
|
||||||
element.type === "draw"
|
element.type === "freedraw"
|
||||||
) {
|
) {
|
||||||
if (element.points.length === 2) {
|
if (element.points.length === 2) {
|
||||||
// only check the last point because starting point is always (0,0)
|
// only check the last point because starting point is always (0,0)
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isGenericElement = (
|
export const isGenericElement = (
|
||||||
@ -24,6 +25,18 @@ export const isTextElement = (
|
|||||||
return element != null && element.type === "text";
|
return element != null && element.type === "text";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFreeDrawElement = (
|
||||||
|
element?: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawFreeDrawElement => {
|
||||||
|
return element != null && isFreeDrawElementType(element.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isFreeDrawElementType = (
|
||||||
|
elementType: ExcalidrawElement["type"],
|
||||||
|
): boolean => {
|
||||||
|
return elementType === "freedraw";
|
||||||
|
};
|
||||||
|
|
||||||
export const isLinearElement = (
|
export const isLinearElement = (
|
||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawLinearElement => {
|
): element is ExcalidrawLinearElement => {
|
||||||
@ -34,7 +47,7 @@ export const isLinearElementType = (
|
|||||||
elementType: ExcalidrawElement["type"],
|
elementType: ExcalidrawElement["type"],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return (
|
return (
|
||||||
elementType === "arrow" || elementType === "line" || elementType === "draw"
|
elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,7 +82,7 @@ export const isExcalidrawElement = (element: any): boolean => {
|
|||||||
element?.type === "rectangle" ||
|
element?.type === "rectangle" ||
|
||||||
element?.type === "ellipse" ||
|
element?.type === "ellipse" ||
|
||||||
element?.type === "arrow" ||
|
element?.type === "arrow" ||
|
||||||
element?.type === "draw" ||
|
element?.type === "freedraw" ||
|
||||||
element?.type === "line"
|
element?.type === "line"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -78,7 +78,8 @@ export type ExcalidrawGenericElement =
|
|||||||
export type ExcalidrawElement =
|
export type ExcalidrawElement =
|
||||||
| ExcalidrawGenericElement
|
| ExcalidrawGenericElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawLinearElement;
|
| ExcalidrawLinearElement
|
||||||
|
| ExcalidrawFreeDrawElement;
|
||||||
|
|
||||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||||
isDeleted: false;
|
isDeleted: false;
|
||||||
@ -121,3 +122,12 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
startArrowhead: Arrowhead | null;
|
startArrowhead: Arrowhead | null;
|
||||||
endArrowhead: Arrowhead | null;
|
endArrowhead: Arrowhead | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||||
|
Readonly<{
|
||||||
|
type: "freedraw";
|
||||||
|
points: readonly Point[];
|
||||||
|
pressures: readonly number[];
|
||||||
|
simulatePressure: boolean;
|
||||||
|
lastCommittedPoint: Point | null;
|
||||||
|
}>;
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "تحديد",
|
"selection": "تحديد",
|
||||||
"draw": "الكتابة الحرة",
|
"freedraw": "الكتابة الحرة",
|
||||||
"rectangle": "مستطيل",
|
"rectangle": "مستطيل",
|
||||||
"diamond": "مضلع",
|
"diamond": "مضلع",
|
||||||
"ellipse": "دائرة",
|
"ellipse": "دائرة",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Селекция",
|
"selection": "Селекция",
|
||||||
"draw": "Рисуване",
|
"freedraw": "Рисуване",
|
||||||
"rectangle": "Правоъгълник",
|
"rectangle": "Правоъгълник",
|
||||||
"diamond": "Диамант",
|
"diamond": "Диамант",
|
||||||
"ellipse": "Елипс",
|
"ellipse": "Елипс",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecció",
|
"selection": "Selecció",
|
||||||
"draw": "Dibuix lliure",
|
"freedraw": "Dibuix lliure",
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"diamond": "Rombe",
|
"diamond": "Rombe",
|
||||||
"ellipse": "El·lipse",
|
"ellipse": "El·lipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Auswahl",
|
"selection": "Auswahl",
|
||||||
"draw": "Freies Zeichnen",
|
"freedraw": "Freies Zeichnen",
|
||||||
"rectangle": "Rechteck",
|
"rectangle": "Rechteck",
|
||||||
"diamond": "Raute",
|
"diamond": "Raute",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Επιλογή",
|
"selection": "Επιλογή",
|
||||||
"draw": "Ελεύθερο σχέδιο",
|
"freedraw": "Ελεύθερο σχέδιο",
|
||||||
"rectangle": "Ορθογώνιο",
|
"rectangle": "Ορθογώνιο",
|
||||||
"diamond": "Ρόμβος",
|
"diamond": "Ρόμβος",
|
||||||
"ellipse": "Έλλειψη",
|
"ellipse": "Έλλειψη",
|
||||||
|
@ -20,6 +20,10 @@
|
|||||||
"background": "Background",
|
"background": "Background",
|
||||||
"fill": "Fill",
|
"fill": "Fill",
|
||||||
"strokeWidth": "Stroke width",
|
"strokeWidth": "Stroke width",
|
||||||
|
"strokeShape": "Stroke shape",
|
||||||
|
"strokeShape_gel": "Gel pen",
|
||||||
|
"strokeShape_fountain": "Fountain pen",
|
||||||
|
"strokeShape_brush": "Brush pen",
|
||||||
"strokeStyle": "Stroke style",
|
"strokeStyle": "Stroke style",
|
||||||
"strokeStyle_solid": "Solid",
|
"strokeStyle_solid": "Solid",
|
||||||
"strokeStyle_dashed": "Dashed",
|
"strokeStyle_dashed": "Dashed",
|
||||||
@ -153,7 +157,6 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
"draw": "Free draw",
|
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"diamond": "Diamond",
|
"diamond": "Diamond",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selección",
|
"selection": "Selección",
|
||||||
"draw": "Dibujo libre",
|
"freedraw": "Dibujo libre",
|
||||||
"rectangle": "Rectángulo",
|
"rectangle": "Rectángulo",
|
||||||
"diamond": "Diamante",
|
"diamond": "Diamante",
|
||||||
"ellipse": "Elipse",
|
"ellipse": "Elipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "گزینش",
|
"selection": "گزینش",
|
||||||
"draw": "طراحی آزاد",
|
"freedraw": "طراحی آزاد",
|
||||||
"rectangle": "مستطیل",
|
"rectangle": "مستطیل",
|
||||||
"diamond": "لوزی",
|
"diamond": "لوزی",
|
||||||
"ellipse": "بیضی",
|
"ellipse": "بیضی",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Valinta",
|
"selection": "Valinta",
|
||||||
"draw": "Vapaa piirto",
|
"freedraw": "Vapaa piirto",
|
||||||
"rectangle": "Suorakulmio",
|
"rectangle": "Suorakulmio",
|
||||||
"diamond": "Vinoneliö",
|
"diamond": "Vinoneliö",
|
||||||
"ellipse": "Soikio",
|
"ellipse": "Soikio",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Sélection",
|
"selection": "Sélection",
|
||||||
"draw": "Dessin libre",
|
"freedraw": "Dessin libre",
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"diamond": "Losange",
|
"diamond": "Losange",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "בחירה",
|
"selection": "בחירה",
|
||||||
"draw": "ציור חופשי",
|
"freedraw": "ציור חופשי",
|
||||||
"rectangle": "מרובע",
|
"rectangle": "מרובע",
|
||||||
"diamond": "מעוין",
|
"diamond": "מעוין",
|
||||||
"ellipse": "אליפסה",
|
"ellipse": "אליפסה",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "चयन",
|
"selection": "चयन",
|
||||||
"draw": "मुफ्त ड्रा",
|
"freedraw": "मुफ्त ड्रा",
|
||||||
"rectangle": "आयात",
|
"rectangle": "आयात",
|
||||||
"diamond": "तिर्यग्वर्ग",
|
"diamond": "तिर्यग्वर्ग",
|
||||||
"ellipse": "दीर्घवृत्त",
|
"ellipse": "दीर्घवृत्त",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Kijelölés",
|
"selection": "Kijelölés",
|
||||||
"draw": "Szabadkézi rajz",
|
"freedraw": "Szabadkézi rajz",
|
||||||
"rectangle": "Téglalap",
|
"rectangle": "Téglalap",
|
||||||
"diamond": "Rombusz",
|
"diamond": "Rombusz",
|
||||||
"ellipse": "Ellipszis",
|
"ellipse": "Ellipszis",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Pilihan",
|
"selection": "Pilihan",
|
||||||
"draw": "Menggambar bebas",
|
"freedraw": "Menggambar bebas",
|
||||||
"rectangle": "Persegi",
|
"rectangle": "Persegi",
|
||||||
"diamond": "Berlian",
|
"diamond": "Berlian",
|
||||||
"ellipse": "Elips",
|
"ellipse": "Elips",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selezione",
|
"selection": "Selezione",
|
||||||
"draw": "Disegno libero",
|
"freedraw": "Disegno libero",
|
||||||
"rectangle": "Rettangolo",
|
"rectangle": "Rettangolo",
|
||||||
"diamond": "Rombo",
|
"diamond": "Rombo",
|
||||||
"ellipse": "Ellisse",
|
"ellipse": "Ellisse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "選択",
|
"selection": "選択",
|
||||||
"draw": "手書き",
|
"freedraw": "手書き",
|
||||||
"rectangle": "矩形",
|
"rectangle": "矩形",
|
||||||
"diamond": "ひし形",
|
"diamond": "ひし形",
|
||||||
"ellipse": "楕円",
|
"ellipse": "楕円",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Tafrayt",
|
"selection": "Tafrayt",
|
||||||
"draw": "Unuɣ ilelli",
|
"freedraw": "Unuɣ ilelli",
|
||||||
"rectangle": "Asrem",
|
"rectangle": "Asrem",
|
||||||
"diamond": "Ameɣṛun",
|
"diamond": "Ameɣṛun",
|
||||||
"ellipse": "Taglayt",
|
"ellipse": "Taglayt",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "선택",
|
"selection": "선택",
|
||||||
"draw": "자유롭게 그리기",
|
"freedraw": "자유롭게 그리기",
|
||||||
"rectangle": "사각형",
|
"rectangle": "사각형",
|
||||||
"diamond": "다이아몬드",
|
"diamond": "다이아몬드",
|
||||||
"ellipse": "타원",
|
"ellipse": "타원",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "ရွေးချယ်",
|
"selection": "ရွေးချယ်",
|
||||||
"draw": "အလွတ်ရေးဆွဲ",
|
"freedraw": "အလွတ်ရေးဆွဲ",
|
||||||
"rectangle": "စတုဂံ",
|
"rectangle": "စတုဂံ",
|
||||||
"diamond": "စိန်",
|
"diamond": "စိန်",
|
||||||
"ellipse": "အဝိုင်း",
|
"ellipse": "အဝိုင်း",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Velg",
|
"selection": "Velg",
|
||||||
"draw": "Frihåndstegning",
|
"freedraw": "Frihåndstegning",
|
||||||
"rectangle": "Rektangel",
|
"rectangle": "Rektangel",
|
||||||
"diamond": "Diamant",
|
"diamond": "Diamant",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selectie",
|
"selection": "Selectie",
|
||||||
"draw": "Vrij tekenen",
|
"freedraw": "Vrij tekenen",
|
||||||
"rectangle": "Rechthoek",
|
"rectangle": "Rechthoek",
|
||||||
"diamond": "Ruit",
|
"diamond": "Ruit",
|
||||||
"ellipse": "Ovaal",
|
"ellipse": "Ovaal",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Vel",
|
"selection": "Vel",
|
||||||
"draw": "Frihandsteikning",
|
"freedraw": "Frihandsteikning",
|
||||||
"rectangle": "Rektangel",
|
"rectangle": "Rektangel",
|
||||||
"diamond": "Diamant",
|
"diamond": "Diamant",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Seleccion",
|
"selection": "Seleccion",
|
||||||
"draw": "Dessenh liure",
|
"freedraw": "Dessenh liure",
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"diamond": "Lausange",
|
"diamond": "Lausange",
|
||||||
"ellipse": "Ellipsa",
|
"ellipse": "Ellipsa",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "ਚੋਣਕਾਰ",
|
"selection": "ਚੋਣਕਾਰ",
|
||||||
"draw": "ਖੁੱਲ੍ਹੀ ਵਾਹੀ",
|
"freedraw": "ਖੁੱਲ੍ਹੀ ਵਾਹੀ",
|
||||||
"rectangle": "ਆਇਤ",
|
"rectangle": "ਆਇਤ",
|
||||||
"diamond": "ਹੀਰਾ",
|
"diamond": "ਹੀਰਾ",
|
||||||
"ellipse": "ਅੰਡਾਕਾਰ",
|
"ellipse": "ਅੰਡਾਕਾਰ",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Zaznaczenie",
|
"selection": "Zaznaczenie",
|
||||||
"draw": "Swobodne rysowanie",
|
"freedraw": "Swobodne rysowanie",
|
||||||
"rectangle": "Prostokąt",
|
"rectangle": "Prostokąt",
|
||||||
"diamond": "Romb",
|
"diamond": "Romb",
|
||||||
"ellipse": "Elipsa",
|
"ellipse": "Elipsa",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Seleção",
|
"selection": "Seleção",
|
||||||
"draw": "Desenho livre",
|
"freedraw": "Desenho livre",
|
||||||
"rectangle": "Retângulo",
|
"rectangle": "Retângulo",
|
||||||
"diamond": "Losango",
|
"diamond": "Losango",
|
||||||
"ellipse": "Elipse",
|
"ellipse": "Elipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Seleção",
|
"selection": "Seleção",
|
||||||
"draw": "Desenho livre",
|
"freedraw": "Desenho livre",
|
||||||
"rectangle": "Retângulo",
|
"rectangle": "Retângulo",
|
||||||
"diamond": "Losango",
|
"diamond": "Losango",
|
||||||
"ellipse": "Elipse",
|
"ellipse": "Elipse",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecție",
|
"selection": "Selecție",
|
||||||
"draw": "Desenare liberă",
|
"freedraw": "Desenare liberă",
|
||||||
"rectangle": "Dreptunghi",
|
"rectangle": "Dreptunghi",
|
||||||
"diamond": "Romb",
|
"diamond": "Romb",
|
||||||
"ellipse": "Elipsă",
|
"ellipse": "Elipsă",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Выделение области",
|
"selection": "Выделение области",
|
||||||
"draw": "Свободное рисование",
|
"freedraw": "Свободное рисование",
|
||||||
"rectangle": "Прямоугольник",
|
"rectangle": "Прямоугольник",
|
||||||
"diamond": "Ромб",
|
"diamond": "Ромб",
|
||||||
"ellipse": "Эллипс",
|
"ellipse": "Эллипс",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Výber",
|
"selection": "Výber",
|
||||||
"draw": "Voľné kreslenie",
|
"freedraw": "Voľné kreslenie",
|
||||||
"rectangle": "Obdĺžnik",
|
"rectangle": "Obdĺžnik",
|
||||||
"diamond": "Diamant",
|
"diamond": "Diamant",
|
||||||
"ellipse": "Elipsa",
|
"ellipse": "Elipsa",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Markering",
|
"selection": "Markering",
|
||||||
"draw": "Frihand",
|
"freedraw": "Frihand",
|
||||||
"rectangle": "Rektangel",
|
"rectangle": "Rektangel",
|
||||||
"diamond": "Diamant",
|
"diamond": "Diamant",
|
||||||
"ellipse": "Ellips",
|
"ellipse": "Ellips",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Seçme",
|
"selection": "Seçme",
|
||||||
"draw": "Serbest çizim",
|
"freedraw": "Serbest çizim",
|
||||||
"rectangle": "Dikdörtgen",
|
"rectangle": "Dikdörtgen",
|
||||||
"diamond": "Elmas",
|
"diamond": "Elmas",
|
||||||
"ellipse": "Elips",
|
"ellipse": "Elips",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Виділення",
|
"selection": "Виділення",
|
||||||
"draw": "Вільне креслення",
|
"freedraw": "Вільне креслення",
|
||||||
"rectangle": "Прямокутник",
|
"rectangle": "Прямокутник",
|
||||||
"diamond": "Ромб",
|
"diamond": "Ромб",
|
||||||
"ellipse": "Еліпс",
|
"ellipse": "Еліпс",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "选择",
|
"selection": "选择",
|
||||||
"draw": "自由书写",
|
"freedraw": "自由书写",
|
||||||
"rectangle": "矩形",
|
"rectangle": "矩形",
|
||||||
"diamond": "菱形",
|
"diamond": "菱形",
|
||||||
"ellipse": "椭圆",
|
"ellipse": "椭圆",
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "選取",
|
"selection": "選取",
|
||||||
"draw": "繪圖",
|
"freedraw": "繪圖",
|
||||||
"rectangle": "長方形",
|
"rectangle": "長方形",
|
||||||
"diamond": "菱形",
|
"diamond": "菱形",
|
||||||
"ellipse": "橢圓",
|
"ellipse": "橢圓",
|
||||||
|
@ -249,6 +249,7 @@ const doSegmentsIntersect = (p1: Point, q1: Point, p2: Point, q2: Point) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Rounding this point causes some shake when free drawing
|
||||||
export const getGridPoint = (
|
export const getGridPoint = (
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
@ -8,6 +8,7 @@ export const getSizeFromPoints = (points: readonly Point[]) => {
|
|||||||
height: Math.max(...ys) - Math.min(...ys),
|
height: Math.max(...ys) - Math.min(...ys),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rescalePoints = (
|
export const rescalePoints = (
|
||||||
dimension: 0 | 1,
|
dimension: 0 | 1,
|
||||||
nextDimensionSize: number,
|
nextDimensionSize: number,
|
||||||
|
@ -4,8 +4,13 @@ import {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { isTextElement, isLinearElement } from "../element/typeChecks";
|
import {
|
||||||
|
isTextElement,
|
||||||
|
isLinearElement,
|
||||||
|
isFreeDrawElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
@ -27,14 +32,17 @@ import { isPathALoop } from "../math";
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { Zoom } from "../types";
|
import { Zoom } from "../types";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import getFreeDrawShape from "perfect-freehand";
|
||||||
|
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
|
|
||||||
const CANVAS_PADDING = 20;
|
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth];
|
const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth];
|
||||||
|
|
||||||
|
const getCanvasPadding = (element: ExcalidrawElement) =>
|
||||||
|
element.type === "freedraw" ? element.strokeWidth * 12 : 20;
|
||||||
|
|
||||||
export interface ExcalidrawElementWithCanvas {
|
export interface ExcalidrawElementWithCanvas {
|
||||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
@ -49,18 +57,25 @@ const generateElementCanvas = (
|
|||||||
): ExcalidrawElementWithCanvas => {
|
): ExcalidrawElementWithCanvas => {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
|
const padding = getCanvasPadding(element);
|
||||||
|
|
||||||
let canvasOffsetX = 0;
|
let canvasOffsetX = 0;
|
||||||
let canvasOffsetY = 0;
|
let canvasOffsetY = 0;
|
||||||
|
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
|
|
||||||
|
x1 = Math.floor(x1);
|
||||||
|
x2 = Math.ceil(x2);
|
||||||
|
y1 = Math.floor(y1);
|
||||||
|
y2 = Math.ceil(y2);
|
||||||
|
|
||||||
canvas.width =
|
canvas.width =
|
||||||
distance(x1, x2) * window.devicePixelRatio * zoom.value +
|
distance(x1, x2) * window.devicePixelRatio * zoom.value +
|
||||||
CANVAS_PADDING * zoom.value * 2;
|
padding * zoom.value * 2;
|
||||||
canvas.height =
|
canvas.height =
|
||||||
distance(y1, y2) * window.devicePixelRatio * zoom.value +
|
distance(y1, y2) * window.devicePixelRatio * zoom.value +
|
||||||
CANVAS_PADDING * zoom.value * 2;
|
padding * zoom.value * 2;
|
||||||
|
|
||||||
canvasOffsetX =
|
canvasOffsetX =
|
||||||
element.x > x1
|
element.x > x1
|
||||||
@ -80,13 +95,13 @@ const generateElementCanvas = (
|
|||||||
} else {
|
} else {
|
||||||
canvas.width =
|
canvas.width =
|
||||||
element.width * window.devicePixelRatio * zoom.value +
|
element.width * window.devicePixelRatio * zoom.value +
|
||||||
CANVAS_PADDING * zoom.value * 2;
|
padding * zoom.value * 2;
|
||||||
canvas.height =
|
canvas.height =
|
||||||
element.height * window.devicePixelRatio * zoom.value +
|
element.height * window.devicePixelRatio * zoom.value +
|
||||||
CANVAS_PADDING * zoom.value * 2;
|
padding * zoom.value * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.translate(CANVAS_PADDING * zoom.value, CANVAS_PADDING * zoom.value);
|
context.translate(padding * zoom.value, padding * zoom.value);
|
||||||
|
|
||||||
context.scale(
|
context.scale(
|
||||||
window.devicePixelRatio * zoom.value,
|
window.devicePixelRatio * zoom.value,
|
||||||
@ -94,11 +109,10 @@ const generateElementCanvas = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const rc = rough.canvas(canvas);
|
const rc = rough.canvas(canvas);
|
||||||
|
|
||||||
drawElementOnCanvas(element, rc, context);
|
drawElementOnCanvas(element, rc, context);
|
||||||
context.translate(
|
|
||||||
-(CANVAS_PADDING * zoom.value),
|
context.translate(-(padding * zoom.value), -(padding * zoom.value));
|
||||||
-(CANVAS_PADDING * zoom.value),
|
|
||||||
);
|
|
||||||
context.scale(
|
context.scale(
|
||||||
1 / (window.devicePixelRatio * zoom.value),
|
1 / (window.devicePixelRatio * zoom.value),
|
||||||
1 / (window.devicePixelRatio * zoom.value),
|
1 / (window.devicePixelRatio * zoom.value),
|
||||||
@ -138,6 +152,19 @@ const drawElementOnCanvas = (
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "freedraw": {
|
||||||
|
// Draw directly to canvas
|
||||||
|
context.save();
|
||||||
|
context.fillStyle = element.strokeColor;
|
||||||
|
|
||||||
|
const path = getFreeDrawPath2D(element) as Path2D;
|
||||||
|
|
||||||
|
context.fillStyle = element.strokeColor;
|
||||||
|
context.fill(path);
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
const rtl = isRTL(element.text);
|
const rtl = isRTL(element.text);
|
||||||
@ -243,10 +270,8 @@ export const generateRoughOptions = (element: ExcalidrawElement): Options => {
|
|||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
case "line":
|
case "draw":
|
||||||
case "draw": {
|
case "line": {
|
||||||
// If shape is a line and is a closed shape,
|
|
||||||
// fill the shape if a color is set.
|
|
||||||
if (isPathALoop(element.points)) {
|
if (isPathALoop(element.points)) {
|
||||||
options.fillStyle = element.fillStyle;
|
options.fillStyle = element.fillStyle;
|
||||||
options.fill =
|
options.fill =
|
||||||
@ -256,6 +281,7 @@ export const generateRoughOptions = (element: ExcalidrawElement): Options => {
|
|||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
case "freedraw":
|
||||||
case "arrow":
|
case "arrow":
|
||||||
return options;
|
return options;
|
||||||
default: {
|
default: {
|
||||||
@ -264,11 +290,17 @@ export const generateRoughOptions = (element: ExcalidrawElement): Options => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the element's shape and puts it into the cache.
|
||||||
|
* @param element
|
||||||
|
* @param generator
|
||||||
|
*/
|
||||||
const generateElementShape = (
|
const generateElementShape = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
generator: RoughGenerator,
|
generator: RoughGenerator,
|
||||||
) => {
|
) => {
|
||||||
let shape = shapeCache.get(element) || null;
|
let shape = shapeCache.get(element) || null;
|
||||||
|
|
||||||
if (!shape) {
|
if (!shape) {
|
||||||
elementWithCanvasCache.delete(element);
|
elementWithCanvasCache.delete(element);
|
||||||
|
|
||||||
@ -327,8 +359,8 @@ const generateElementShape = (
|
|||||||
generateRoughOptions(element),
|
generateRoughOptions(element),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "line":
|
|
||||||
case "draw":
|
case "draw":
|
||||||
|
case "line":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
const options = generateRoughOptions(element);
|
const options = generateRoughOptions(element);
|
||||||
|
|
||||||
@ -380,15 +412,18 @@ const generateElementShape = (
|
|||||||
...options,
|
...options,
|
||||||
fill: element.strokeColor,
|
fill: element.strokeColor,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
|
stroke: "none",
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrow arrowheads
|
// Arrow arrowheads
|
||||||
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
const [x2, y2, x3, y3, x4, y4] = arrowheadPoints;
|
||||||
|
|
||||||
if (element.strokeStyle === "dotted") {
|
if (element.strokeStyle === "dotted") {
|
||||||
// for dotted arrows caps, reduce gap to make it more legible
|
// for dotted arrows caps, reduce gap to make it more legible
|
||||||
options.strokeLineDash = [3, 4];
|
const dash = getDashArrayDotted(element.strokeWidth - 1);
|
||||||
|
options.strokeLineDash = [dash[0], dash[1] - 1];
|
||||||
} else {
|
} else {
|
||||||
// for solid/dashed, keep solid arrow cap
|
// for solid/dashed, keep solid arrow cap
|
||||||
delete options.strokeLineDash;
|
delete options.strokeLineDash;
|
||||||
@ -423,6 +458,12 @@ const generateElementShape = (
|
|||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "freedraw": {
|
||||||
|
generateFreeDrawShape(element);
|
||||||
|
shape = [];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
@ -447,7 +488,9 @@ const generateElementWithCanvas = (
|
|||||||
!sceneState?.shouldCacheIgnoreZoom;
|
!sceneState?.shouldCacheIgnoreZoom;
|
||||||
if (!prevElementWithCanvas || shouldRegenerateBecauseZoom) {
|
if (!prevElementWithCanvas || shouldRegenerateBecauseZoom) {
|
||||||
const elementWithCanvas = generateElementCanvas(element, zoom);
|
const elementWithCanvas = generateElementCanvas(element, zoom);
|
||||||
|
|
||||||
elementWithCanvasCache.set(element, elementWithCanvas);
|
elementWithCanvasCache.set(element, elementWithCanvas);
|
||||||
|
|
||||||
return elementWithCanvas;
|
return elementWithCanvas;
|
||||||
}
|
}
|
||||||
return prevElementWithCanvas;
|
return prevElementWithCanvas;
|
||||||
@ -460,20 +503,29 @@ const drawElementFromCanvas = (
|
|||||||
sceneState: SceneState,
|
sceneState: SceneState,
|
||||||
) => {
|
) => {
|
||||||
const element = elementWithCanvas.element;
|
const element = elementWithCanvas.element;
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const padding = getCanvasPadding(element);
|
||||||
|
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
|
|
||||||
|
// Free draw elements will otherwise "shuffle" as the min x and y change
|
||||||
|
if (isFreeDrawElement(element)) {
|
||||||
|
x1 = Math.floor(x1);
|
||||||
|
x2 = Math.ceil(x2);
|
||||||
|
y1 = Math.floor(y1);
|
||||||
|
y2 = Math.ceil(y2);
|
||||||
|
}
|
||||||
|
|
||||||
const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio;
|
const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio;
|
||||||
const cy = ((y1 + y2) / 2 + sceneState.scrollY) * window.devicePixelRatio;
|
const cy = ((y1 + y2) / 2 + sceneState.scrollY) * window.devicePixelRatio;
|
||||||
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
context.rotate(element.angle);
|
context.rotate(element.angle);
|
||||||
|
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
elementWithCanvas.canvas!,
|
elementWithCanvas.canvas!,
|
||||||
(-(x2 - x1) / 2) * window.devicePixelRatio -
|
(-(x2 - x1) / 2) * window.devicePixelRatio -
|
||||||
(CANVAS_PADDING * elementWithCanvas.canvasZoom) /
|
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
||||||
elementWithCanvas.canvasZoom,
|
|
||||||
(-(y2 - y1) / 2) * window.devicePixelRatio -
|
(-(y2 - y1) / 2) * window.devicePixelRatio -
|
||||||
(CANVAS_PADDING * elementWithCanvas.canvasZoom) /
|
(padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom,
|
||||||
elementWithCanvas.canvasZoom,
|
|
||||||
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
|
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
|
||||||
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
|
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
|
||||||
);
|
);
|
||||||
@ -508,11 +560,37 @@ export const renderElement = (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "freedraw": {
|
||||||
|
generateElementShape(element, generator);
|
||||||
|
|
||||||
|
if (renderOptimizations) {
|
||||||
|
const elementWithCanvas = generateElementWithCanvas(
|
||||||
|
element,
|
||||||
|
sceneState,
|
||||||
|
);
|
||||||
|
drawElementFromCanvas(elementWithCanvas, rc, context, sceneState);
|
||||||
|
} else {
|
||||||
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
|
const cx = (x1 + x2) / 2 + sceneState.scrollX;
|
||||||
|
const cy = (y1 + y2) / 2 + sceneState.scrollY;
|
||||||
|
const shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||||
|
const shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||||
|
context.translate(cx, cy);
|
||||||
|
context.rotate(element.angle);
|
||||||
|
context.translate(-shiftX, -shiftY);
|
||||||
|
drawElementOnCanvas(element, rc, context);
|
||||||
|
context.translate(shiftX, shiftY);
|
||||||
|
context.rotate(-element.angle);
|
||||||
|
context.translate(-cx, -cy);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
case "line":
|
|
||||||
case "draw":
|
case "draw":
|
||||||
|
case "line":
|
||||||
case "arrow":
|
case "arrow":
|
||||||
case "text": {
|
case "text": {
|
||||||
generateElementShape(element, generator);
|
generateElementShape(element, generator);
|
||||||
@ -583,8 +661,8 @@ export const renderElementToSvg = (
|
|||||||
svgRoot.appendChild(node);
|
svgRoot.appendChild(node);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "line":
|
|
||||||
case "draw":
|
case "draw":
|
||||||
|
case "line":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
generateElementShape(element, generator);
|
generateElementShape(element, generator);
|
||||||
const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||||
@ -604,7 +682,7 @@ export const renderElementToSvg = (
|
|||||||
}) rotate(${degree} ${cx} ${cy})`,
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
(element.type === "line" || element.type === "draw") &&
|
element.type === "line" &&
|
||||||
isPathALoop(element.points) &&
|
isPathALoop(element.points) &&
|
||||||
element.backgroundColor !== "transparent"
|
element.backgroundColor !== "transparent"
|
||||||
) {
|
) {
|
||||||
@ -615,6 +693,28 @@ export const renderElementToSvg = (
|
|||||||
svgRoot.appendChild(group);
|
svgRoot.appendChild(group);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "freedraw": {
|
||||||
|
generateFreeDrawShape(element);
|
||||||
|
const opacity = element.opacity / 100;
|
||||||
|
const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||||
|
if (opacity !== 1) {
|
||||||
|
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||||
|
node.setAttribute("fill-opacity", `${opacity}`);
|
||||||
|
}
|
||||||
|
node.setAttribute(
|
||||||
|
"transform",
|
||||||
|
`translate(${offsetX || 0} ${
|
||||||
|
offsetY || 0
|
||||||
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
|
);
|
||||||
|
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
||||||
|
node.setAttribute("stroke", "none");
|
||||||
|
node.setAttribute("fill", element.strokeStyle);
|
||||||
|
path.setAttribute("d", getFreeDrawSvgPath(element));
|
||||||
|
node.appendChild(path);
|
||||||
|
svgRoot.appendChild(node);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
const opacity = element.opacity / 100;
|
const opacity = element.opacity / 100;
|
||||||
@ -666,3 +766,55 @@ export const renderElementToSvg = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const pathsCache = new WeakMap<ExcalidrawFreeDrawElement, Path2D>([]);
|
||||||
|
|
||||||
|
export function generateFreeDrawShape(element: ExcalidrawFreeDrawElement) {
|
||||||
|
const svgPathData = getFreeDrawSvgPath(element);
|
||||||
|
const path = new Path2D(svgPathData);
|
||||||
|
pathsCache.set(element, path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFreeDrawPath2D(element: ExcalidrawFreeDrawElement) {
|
||||||
|
return pathsCache.get(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
|
||||||
|
const inputPoints = element.simulatePressure
|
||||||
|
? element.points
|
||||||
|
: element.points.length
|
||||||
|
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
|
||||||
|
: [[0, 0, 0]];
|
||||||
|
|
||||||
|
// Consider changing the options for simulated pressure vs real pressure
|
||||||
|
const options = {
|
||||||
|
simulatePressure: element.simulatePressure,
|
||||||
|
size: element.strokeWidth * 6,
|
||||||
|
thinning: 0.5,
|
||||||
|
smoothing: 0.5,
|
||||||
|
streamline: 0.5,
|
||||||
|
easing: (t: number) => t * (2 - t),
|
||||||
|
last: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const points = getFreeDrawShape(inputPoints as number[][], options);
|
||||||
|
const d: (string | number)[] = [];
|
||||||
|
|
||||||
|
let [p0, p1] = points;
|
||||||
|
|
||||||
|
d.push("M", p0[0], p0[1], "Q");
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
d.push(p0[0], p0[1], (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
|
||||||
|
p0 = p1;
|
||||||
|
p1 = points[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
p1 = points[0];
|
||||||
|
d.push(p0[0], p0[1], (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
|
||||||
|
|
||||||
|
d.push("Z");
|
||||||
|
|
||||||
|
return d.join(" ");
|
||||||
|
}
|
||||||
|
@ -201,11 +201,12 @@ export const renderScene = (
|
|||||||
renderGrid?: boolean;
|
renderGrid?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
if (!canvas) {
|
if (canvas === null) {
|
||||||
return { atLeastOneVisibleElement: false };
|
return { atLeastOneVisibleElement: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
|
|
||||||
context.scale(scale, scale);
|
context.scale(scale, scale);
|
||||||
|
|
||||||
// When doing calculations based on canvas width we should used normalized one
|
// When doing calculations based on canvas width we should used normalized one
|
||||||
|
@ -9,22 +9,25 @@ export const hasBackground = (type: string) =>
|
|||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "ellipse" ||
|
type === "ellipse" ||
|
||||||
type === "diamond" ||
|
type === "diamond" ||
|
||||||
type === "draw" ||
|
|
||||||
type === "line";
|
type === "line";
|
||||||
|
|
||||||
export const hasStroke = (type: string) =>
|
export const hasStrokeWidth = (type: string) =>
|
||||||
|
type === "rectangle" ||
|
||||||
|
type === "ellipse" ||
|
||||||
|
type === "diamond" ||
|
||||||
|
type === "freedraw" ||
|
||||||
|
type === "arrow" ||
|
||||||
|
type === "line";
|
||||||
|
|
||||||
|
export const hasStrokeStyle = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "ellipse" ||
|
type === "ellipse" ||
|
||||||
type === "diamond" ||
|
type === "diamond" ||
|
||||||
type === "arrow" ||
|
type === "arrow" ||
|
||||||
type === "draw" ||
|
|
||||||
type === "line";
|
type === "line";
|
||||||
|
|
||||||
export const canChangeSharpness = (type: string) =>
|
export const canChangeSharpness = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" || type === "arrow" || type === "line";
|
||||||
type === "arrow" ||
|
|
||||||
type === "draw" ||
|
|
||||||
type === "line";
|
|
||||||
|
|
||||||
export const hasText = (type: string) => type === "text";
|
export const hasText = (type: string) => type === "text";
|
||||||
|
|
||||||
|
@ -9,7 +9,8 @@ export {
|
|||||||
export { calculateScrollCenter } from "./scroll";
|
export { calculateScrollCenter } from "./scroll";
|
||||||
export {
|
export {
|
||||||
hasBackground,
|
hasBackground,
|
||||||
hasStroke,
|
hasStrokeWidth,
|
||||||
|
hasStrokeStyle,
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
canChangeSharpness,
|
canChangeSharpness,
|
||||||
getElementAtPosition,
|
getElementAtPosition,
|
||||||
|
@ -80,7 +80,7 @@ export const SHAPES = [
|
|||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "draw",
|
value: "freedraw",
|
||||||
key: KEYS.X,
|
key: KEYS.X,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -6280,7 +6280,7 @@ Object {
|
|||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
"editingLinearElement": null,
|
"editingLinearElement": null,
|
||||||
"elementLocked": false,
|
"elementLocked": false,
|
||||||
"elementType": "draw",
|
"elementType": "freedraw",
|
||||||
"errorMessage": null,
|
"errorMessage": null,
|
||||||
"exportBackground": true,
|
"exportBackground": true,
|
||||||
"exportEmbedScene": false,
|
"exportEmbedScene": false,
|
||||||
@ -6596,8 +6596,6 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElementIds": null,
|
"boundElementIds": null,
|
||||||
"endArrowhead": null,
|
|
||||||
"endBinding": null,
|
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
@ -6614,18 +6612,26 @@ Object {
|
|||||||
50,
|
50,
|
||||||
10,
|
10,
|
||||||
],
|
],
|
||||||
|
Array [
|
||||||
|
50,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"pressures": Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 941653321,
|
"seed": 941653321,
|
||||||
"startArrowhead": null,
|
"simulatePressure": false,
|
||||||
"startBinding": null,
|
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeSharpness": "round",
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "draw",
|
"type": "freedraw",
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 1402203177,
|
"versionNonce": 1359939303,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 550,
|
"x": 550,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
@ -8246,8 +8252,6 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElementIds": null,
|
"boundElementIds": null,
|
||||||
"endArrowhead": null,
|
|
||||||
"endBinding": null,
|
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
@ -8264,18 +8268,26 @@ Object {
|
|||||||
50,
|
50,
|
||||||
10,
|
10,
|
||||||
],
|
],
|
||||||
|
Array [
|
||||||
|
50,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"pressures": Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 941653321,
|
"seed": 941653321,
|
||||||
"startArrowhead": null,
|
"simulatePressure": false,
|
||||||
"startBinding": null,
|
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeSharpness": "round",
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "draw",
|
"type": "freedraw",
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 1402203177,
|
"versionNonce": 1359939303,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 550,
|
"x": 550,
|
||||||
"y": -10,
|
"y": -10,
|
||||||
@ -10355,7 +10367,7 @@ exports[`regression tests key 6 selects line tool: [end of test] number of eleme
|
|||||||
|
|
||||||
exports[`regression tests key 6 selects line tool: [end of test] number of renders 1`] = `8`;
|
exports[`regression tests key 6 selects line tool: [end of test] number of renders 1`] = `8`;
|
||||||
|
|
||||||
exports[`regression tests key 7 selects draw tool: [end of test] appState 1`] = `
|
exports[`regression tests key 7 selects freedraw tool: [end of test] appState 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
@ -10379,7 +10391,7 @@ Object {
|
|||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
"editingLinearElement": null,
|
"editingLinearElement": null,
|
||||||
"elementLocked": false,
|
"elementLocked": false,
|
||||||
"elementType": "draw",
|
"elementType": "freedraw",
|
||||||
"errorMessage": null,
|
"errorMessage": null,
|
||||||
"exportBackground": true,
|
"exportBackground": true,
|
||||||
"exportEmbedScene": false,
|
"exportEmbedScene": false,
|
||||||
@ -10434,13 +10446,11 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`regression tests key 7 selects draw tool: [end of test] element 0 1`] = `
|
exports[`regression tests key 7 selects freedraw tool: [end of test] element 0 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElementIds": null,
|
"boundElementIds": null,
|
||||||
"endArrowhead": null,
|
|
||||||
"endBinding": null,
|
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
@ -10457,25 +10467,33 @@ Object {
|
|||||||
10,
|
10,
|
||||||
10,
|
10,
|
||||||
],
|
],
|
||||||
|
Array [
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"pressures": Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startArrowhead": null,
|
"simulatePressure": false,
|
||||||
"startBinding": null,
|
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeSharpness": "round",
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "draw",
|
"type": "freedraw",
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 453191,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`regression tests key 7 selects draw tool: [end of test] history 1`] = `
|
exports[`regression tests key 7 selects freedraw tool: [end of test] history 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"recording": false,
|
"recording": false,
|
||||||
"redoStack": Array [],
|
"redoStack": Array [],
|
||||||
@ -10505,8 +10523,6 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElementIds": null,
|
"boundElementIds": null,
|
||||||
"endArrowhead": null,
|
|
||||||
"endBinding": null,
|
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
@ -10523,18 +10539,26 @@ Object {
|
|||||||
10,
|
10,
|
||||||
10,
|
10,
|
||||||
],
|
],
|
||||||
|
Array [
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"pressures": Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startArrowhead": null,
|
"simulatePressure": false,
|
||||||
"startBinding": null,
|
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeSharpness": "round",
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "draw",
|
"type": "freedraw",
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 453191,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -10545,9 +10569,9 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`regression tests key 7 selects draw tool: [end of test] number of elements 1`] = `1`;
|
exports[`regression tests key 7 selects freedraw tool: [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`regression tests key 7 selects draw tool: [end of test] number of renders 1`] = `8`;
|
exports[`regression tests key 7 selects freedraw tool: [end of test] number of renders 1`] = `8`;
|
||||||
|
|
||||||
exports[`regression tests key a selects arrow tool: [end of test] appState 1`] = `
|
exports[`regression tests key a selects arrow tool: [end of test] appState 1`] = `
|
||||||
Object {
|
Object {
|
||||||
@ -11429,7 +11453,7 @@ exports[`regression tests key r selects rectangle tool: [end of test] number of
|
|||||||
|
|
||||||
exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `8`;
|
exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `8`;
|
||||||
|
|
||||||
exports[`regression tests key x selects draw tool: [end of test] appState 1`] = `
|
exports[`regression tests key x selects freedraw tool: [end of test] appState 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
@ -11453,7 +11477,7 @@ Object {
|
|||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
"editingLinearElement": null,
|
"editingLinearElement": null,
|
||||||
"elementLocked": false,
|
"elementLocked": false,
|
||||||
"elementType": "draw",
|
"elementType": "freedraw",
|
||||||
"errorMessage": null,
|
"errorMessage": null,
|
||||||
"exportBackground": true,
|
"exportBackground": true,
|
||||||
"exportEmbedScene": false,
|
"exportEmbedScene": false,
|
||||||
@ -11508,13 +11532,11 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`regression tests key x selects draw tool: [end of test] element 0 1`] = `
|
exports[`regression tests key x selects freedraw tool: [end of test] element 0 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElementIds": null,
|
"boundElementIds": null,
|
||||||
"endArrowhead": null,
|
|
||||||
"endBinding": null,
|
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
@ -11531,25 +11553,33 @@ Object {
|
|||||||
10,
|
10,
|
||||||
10,
|
10,
|
||||||
],
|
],
|
||||||
|
Array [
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"pressures": Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startArrowhead": null,
|
"simulatePressure": false,
|
||||||
"startBinding": null,
|
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeSharpness": "round",
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "draw",
|
"type": "freedraw",
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 453191,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`regression tests key x selects draw tool: [end of test] history 1`] = `
|
exports[`regression tests key x selects freedraw tool: [end of test] history 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"recording": false,
|
"recording": false,
|
||||||
"redoStack": Array [],
|
"redoStack": Array [],
|
||||||
@ -11579,8 +11609,6 @@ Object {
|
|||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElementIds": null,
|
"boundElementIds": null,
|
||||||
"endArrowhead": null,
|
|
||||||
"endBinding": null,
|
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
@ -11597,18 +11625,26 @@ Object {
|
|||||||
10,
|
10,
|
||||||
10,
|
10,
|
||||||
],
|
],
|
||||||
|
Array [
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"pressures": Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"seed": 337897,
|
"seed": 337897,
|
||||||
"startArrowhead": null,
|
"simulatePressure": false,
|
||||||
"startBinding": null,
|
|
||||||
"strokeColor": "#000000",
|
"strokeColor": "#000000",
|
||||||
"strokeSharpness": "round",
|
"strokeSharpness": "round",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "draw",
|
"type": "freedraw",
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 453191,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -11619,9 +11655,9 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`regression tests key x selects draw tool: [end of test] number of elements 1`] = `1`;
|
exports[`regression tests key x selects freedraw tool: [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`regression tests key x selects draw tool: [end of test] number of renders 1`] = `8`;
|
exports[`regression tests key x selects freedraw tool: [end of test] number of renders 1`] = `8`;
|
||||||
|
|
||||||
exports[`regression tests make a group and duplicate it: [end of test] appState 1`] = `
|
exports[`regression tests make a group and duplicate it: [end of test] appState 1`] = `
|
||||||
Object {
|
Object {
|
||||||
|
@ -71,7 +71,7 @@ const createAndSelectOneLine = (angle: number = 0) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createAndReturnOneDraw = (angle: number = 0) => {
|
const createAndReturnOneDraw = (angle: number = 0) => {
|
||||||
return UI.createElement("draw", {
|
return UI.createElement("freedraw", {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 50,
|
width: 50,
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { newElement, newTextElement, newLinearElement } from "../../element";
|
import { newElement, newTextElement, newLinearElement } from "../../element";
|
||||||
import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
|
import { DEFAULT_VERTICAL_ALIGN } from "../../constants";
|
||||||
@ -12,6 +13,7 @@ import fs from "fs";
|
|||||||
import util from "util";
|
import util from "util";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getMimeType } from "../../data/blob";
|
import { getMimeType } from "../../data/blob";
|
||||||
|
import { newFreeDrawElement } from "../../element/newElement";
|
||||||
|
|
||||||
const readFile = util.promisify(fs.readFile);
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
|
||||||
@ -81,8 +83,10 @@ export class API {
|
|||||||
verticalAlign?: T extends "text"
|
verticalAlign?: T extends "text"
|
||||||
? ExcalidrawTextElement["verticalAlign"]
|
? ExcalidrawTextElement["verticalAlign"]
|
||||||
: never;
|
: never;
|
||||||
}): T extends "arrow" | "line" | "draw"
|
}): T extends "arrow" | "line"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
|
: T extends "freedraw"
|
||||||
|
? ExcalidrawFreeDrawElement
|
||||||
: T extends "text"
|
: T extends "text"
|
||||||
? ExcalidrawTextElement
|
? ExcalidrawTextElement
|
||||||
: ExcalidrawGenericElement => {
|
: ExcalidrawGenericElement => {
|
||||||
@ -125,11 +129,17 @@ export class API {
|
|||||||
verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "freedraw":
|
||||||
|
element = newFreeDrawElement({
|
||||||
|
type: type as "freedraw",
|
||||||
|
simulatePressure: true,
|
||||||
|
...base,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case "arrow":
|
case "arrow":
|
||||||
case "line":
|
case "line":
|
||||||
case "draw":
|
|
||||||
element = newLinearElement({
|
element = newLinearElement({
|
||||||
type: type as "arrow" | "line" | "draw",
|
type: type as "arrow" | "line",
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
...base,
|
...base,
|
||||||
|
@ -213,14 +213,14 @@ export class UI {
|
|||||||
height?: number;
|
height?: number;
|
||||||
angle?: number;
|
angle?: number;
|
||||||
} = {},
|
} = {},
|
||||||
): (T extends "arrow" | "line" | "draw"
|
): (T extends "arrow" | "line" | "freedraw"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
: T extends "text"
|
: T extends "text"
|
||||||
? ExcalidrawTextElement
|
? ExcalidrawTextElement
|
||||||
: ExcalidrawElement) & {
|
: ExcalidrawElement) & {
|
||||||
/** Returns the actual, current element from the elements array, instead
|
/** Returns the actual, current element from the elements array, instead
|
||||||
of the proxy */
|
of the proxy */
|
||||||
get(): T extends "arrow" | "line" | "draw"
|
get(): T extends "arrow" | "line" | "freedraw"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
: T extends "text"
|
: T extends "text"
|
||||||
? ExcalidrawTextElement
|
? ExcalidrawTextElement
|
||||||
|
@ -7,7 +7,7 @@ const toolMap = {
|
|||||||
ellipse: "ellipse",
|
ellipse: "ellipse",
|
||||||
arrow: "arrow",
|
arrow: "arrow",
|
||||||
line: "line",
|
line: "line",
|
||||||
draw: "draw",
|
freedraw: "freedraw",
|
||||||
text: "text",
|
text: "text",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ describe("regression tests", () => {
|
|||||||
mouse.click(30, 10);
|
mouse.click(30, 10);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
UI.clickTool("draw");
|
UI.clickTool("freedraw");
|
||||||
mouse.down(40, -20);
|
mouse.down(40, -20);
|
||||||
mouse.up(50, 10);
|
mouse.up(50, 10);
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ describe("regression tests", () => {
|
|||||||
"line",
|
"line",
|
||||||
"arrow",
|
"arrow",
|
||||||
"line",
|
"line",
|
||||||
"draw",
|
"freedraw",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ describe("regression tests", () => {
|
|||||||
[`4${KEYS.E}`, "ellipse", true],
|
[`4${KEYS.E}`, "ellipse", true],
|
||||||
[`5${KEYS.A}`, "arrow", true],
|
[`5${KEYS.A}`, "arrow", true],
|
||||||
[`6${KEYS.L}`, "line", true],
|
[`6${KEYS.L}`, "line", true],
|
||||||
[`7${KEYS.X}`, "draw", false],
|
[`7${KEYS.X}`, "freedraw", false],
|
||||||
] as [string, ExcalidrawElement["type"], boolean][]) {
|
] as [string, ExcalidrawElement["type"], boolean][]) {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
it(`key ${key} selects ${shape} tool`, () => {
|
it(`key ${key} selects ${shape} tool`, () => {
|
||||||
|
@ -9249,6 +9249,11 @@ pepjs@0.5.3:
|
|||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
resolved "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz"
|
resolved "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz"
|
||||||
|
|
||||||
|
perfect-freehand@0.4.7:
|
||||||
|
version "0.4.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-0.4.7.tgz#4d85fd64881ba81b2a4eaa6ac4e8983ccb21dd43"
|
||||||
|
integrity sha512-SSSFL8VzXiOHQdUTyNyOb0JC+btVZRy9bi6jos7Nb7PBTI0PHX5jM6RgCTSrubQ8Ul9qOYWmWgJBrwVGHwyJZQ==
|
||||||
|
|
||||||
performance-now@^2.1.0:
|
performance-now@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user