Multi Point Lines (based on Multi Point Arrows) (#660)
* Enable multi points in lines * Stop retrieving arrow points for lines * Migrate lines to new spec during load * Clean up and refactor some code - Normalize shape dimensions during load - Rename getArrowAbsoluteBounds * Fix linter issues
This commit is contained in:
parent
f70bd0081c
commit
dab35c9033
@ -17,51 +17,27 @@ const _ce = ({ x, y, w, h }: { x: number; y: number; w: number; h: number }) =>
|
||||
} as ExcalidrawElement);
|
||||
|
||||
describe("getElementAbsoluteCoords", () => {
|
||||
it("test x1 coordinate if width is positive or zero", () => {
|
||||
it("test x1 coordinate", () => {
|
||||
const [x1] = getElementAbsoluteCoords(_ce({ x: 10, y: 0, w: 10, h: 0 }));
|
||||
expect(x1).toEqual(10);
|
||||
});
|
||||
|
||||
it("test x1 coordinate if width is negative", () => {
|
||||
const [x1] = getElementAbsoluteCoords(_ce({ x: 20, y: 0, w: -10, h: 0 }));
|
||||
expect(x1).toEqual(10);
|
||||
});
|
||||
|
||||
it("test x2 coordinate if width is positive or zero", () => {
|
||||
it("test x2 coordinate", () => {
|
||||
const [, , x2] = getElementAbsoluteCoords(
|
||||
_ce({ x: 10, y: 0, w: 10, h: 0 }),
|
||||
);
|
||||
expect(x2).toEqual(20);
|
||||
});
|
||||
|
||||
it("test x2 coordinate if width is negative", () => {
|
||||
const [, , x2] = getElementAbsoluteCoords(
|
||||
_ce({ x: 10, y: 0, w: -10, h: 0 }),
|
||||
);
|
||||
expect(x2).toEqual(10);
|
||||
});
|
||||
|
||||
it("test y1 coordinate if height is positive or zero", () => {
|
||||
it("test y1 coordinate", () => {
|
||||
const [, y1] = getElementAbsoluteCoords(_ce({ x: 0, y: 10, w: 0, h: 10 }));
|
||||
expect(y1).toEqual(10);
|
||||
});
|
||||
|
||||
it("test y1 coordinate if height is negative", () => {
|
||||
const [, y1] = getElementAbsoluteCoords(_ce({ x: 0, y: 20, w: 0, h: -10 }));
|
||||
expect(y1).toEqual(10);
|
||||
});
|
||||
|
||||
it("test y2 coordinate if height is positive or zero", () => {
|
||||
it("test y2 coordinate", () => {
|
||||
const [, , , y2] = getElementAbsoluteCoords(
|
||||
_ce({ x: 0, y: 10, w: 0, h: 10 }),
|
||||
);
|
||||
expect(y2).toEqual(20);
|
||||
});
|
||||
|
||||
it("test y2 coordinate if height is negative", () => {
|
||||
const [, , , y2] = getElementAbsoluteCoords(
|
||||
_ce({ x: 0, y: 10, w: 0, h: -10 }),
|
||||
);
|
||||
expect(y2).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
@ -5,17 +5,15 @@ import { Point } from "roughjs/bin/geometry";
|
||||
|
||||
// If the element is created from right to left, the width is going to be negative
|
||||
// This set of functions retrieves the absolute position of the 4 points.
|
||||
// We can't just always normalize it since we need to remember the fact that an arrow
|
||||
// is pointing left or right.
|
||||
export function getElementAbsoluteCoords(element: ExcalidrawElement) {
|
||||
if (element.type === "arrow") {
|
||||
return getArrowAbsoluteBounds(element);
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
return getLinearElementAbsoluteBounds(element);
|
||||
}
|
||||
return [
|
||||
element.width >= 0 ? element.x : element.x + element.width, // x1
|
||||
element.height >= 0 ? element.y : element.y + element.height, // y1
|
||||
element.width >= 0 ? element.x + element.width : element.x, // x2
|
||||
element.height >= 0 ? element.y + element.height : element.y, // y2
|
||||
element.x,
|
||||
element.y,
|
||||
element.x + element.width,
|
||||
element.y + element.height,
|
||||
];
|
||||
}
|
||||
|
||||
@ -34,7 +32,7 @@ export function getDiamondPoints(element: ExcalidrawElement) {
|
||||
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
||||
}
|
||||
|
||||
export function getArrowAbsoluteBounds(element: ExcalidrawElement) {
|
||||
export function getLinearElementAbsoluteBounds(element: ExcalidrawElement) {
|
||||
if (element.points.length < 2 || !element.shape) {
|
||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||
(limits, [x, y]) => {
|
||||
@ -58,7 +56,8 @@ export function getArrowAbsoluteBounds(element: ExcalidrawElement) {
|
||||
|
||||
const shape = element.shape as Drawable[];
|
||||
|
||||
const ops = shape[1].sets[0].ops;
|
||||
// first element is always the curve
|
||||
const ops = shape[0].sets[0].ops;
|
||||
|
||||
let currentP: Point = [0, 0];
|
||||
|
||||
@ -138,15 +137,6 @@ export function getArrowPoints(element: ExcalidrawElement) {
|
||||
return [x2, y2, x3, y3, x4, y4];
|
||||
}
|
||||
|
||||
export function getLinePoints(element: ExcalidrawElement) {
|
||||
const x1 = 0;
|
||||
const y1 = 0;
|
||||
const x2 = element.width;
|
||||
const y2 = element.height;
|
||||
|
||||
return [x1, y1, x2, y2];
|
||||
}
|
||||
|
||||
export function getCommonBounds(elements: readonly ExcalidrawElement[]) {
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
|
@ -4,8 +4,7 @@ import { ExcalidrawElement } from "./types";
|
||||
import {
|
||||
getDiamondPoints,
|
||||
getElementAbsoluteCoords,
|
||||
getLinePoints,
|
||||
getArrowAbsoluteBounds,
|
||||
getLinearElementAbsoluteBounds,
|
||||
} from "./bounds";
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
import { Drawable, OpSet } from "roughjs/bin/core";
|
||||
@ -148,18 +147,13 @@ export function hitTest(
|
||||
distanceBetweenPointAndSegment(x, y, leftX, leftY, topX, topY) <
|
||||
lineThreshold
|
||||
);
|
||||
} else if (element.type === "arrow") {
|
||||
} else if (element.type === "arrow" || element.type === "line") {
|
||||
if (!element.shape) {
|
||||
return false;
|
||||
}
|
||||
const shape = element.shape as Drawable[];
|
||||
// If shape does not consist of curve and two line segments
|
||||
// for arrow shape, return false
|
||||
if (shape.length < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [x1, y1, x2, y2] = getArrowAbsoluteBounds(element);
|
||||
const [x1, y1, x2, y2] = getLinearElementAbsoluteBounds(element);
|
||||
if (x < x1 || y < y1 - 10 || x > x2 || y > y2 + 10) {
|
||||
return false;
|
||||
}
|
||||
@ -167,19 +161,8 @@ export function hitTest(
|
||||
const relX = x - element.x;
|
||||
const relY = y - element.y;
|
||||
|
||||
// hit test curve and lien segments for arrow
|
||||
return (
|
||||
hitTestRoughShape(shape[0].sets, relX, relY) ||
|
||||
hitTestRoughShape(shape[1].sets, relX, relY) ||
|
||||
hitTestRoughShape(shape[2].sets, relX, relY)
|
||||
);
|
||||
} else if (element.type === "line") {
|
||||
const [x1, y1, x2, y2] = getLinePoints(element);
|
||||
// The computation is done at the origin, we need to add a translation
|
||||
x -= element.x;
|
||||
y -= element.y;
|
||||
|
||||
return distanceBetweenPointAndSegment(x, y, x1, y1, x2, y2) < lineThreshold;
|
||||
// hit thest all "subshapes" of the linear element
|
||||
return shape.some(s => hitTestRoughShape(s.sets, relX, relY));
|
||||
} else if (element.type === "text") {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { SceneScroll } from "../scene/types";
|
||||
import { getArrowAbsoluteBounds } from "./bounds";
|
||||
import { getLinearElementAbsoluteBounds } from "./bounds";
|
||||
|
||||
type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se";
|
||||
|
||||
@ -16,10 +16,13 @@ export function handlerRectangles(
|
||||
let marginY = -8;
|
||||
|
||||
const minimumSize = 40;
|
||||
if (element.type === "arrow") {
|
||||
[elementX1, elementY1, elementX2, elementY2] = getArrowAbsoluteBounds(
|
||||
element,
|
||||
);
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
[
|
||||
elementX1,
|
||||
elementY1,
|
||||
elementX2,
|
||||
elementY2,
|
||||
] = getLinearElementAbsoluteBounds(element);
|
||||
} else {
|
||||
elementX1 = element.x;
|
||||
elementX2 = element.x + element.width;
|
||||
@ -90,12 +93,7 @@ export function handlerRectangles(
|
||||
8,
|
||||
]; // se
|
||||
|
||||
if (element.type === "line") {
|
||||
return {
|
||||
nw: handlers.nw,
|
||||
se: handlers.se,
|
||||
} as typeof handlers;
|
||||
} else if (element.type === "arrow") {
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
if (element.points.length === 2) {
|
||||
// only check the last point because starting point is always (0,0)
|
||||
const [, p1] = element.points;
|
||||
|
@ -4,8 +4,7 @@ export {
|
||||
getCommonBounds,
|
||||
getDiamondPoints,
|
||||
getArrowPoints,
|
||||
getLinePoints,
|
||||
getArrowAbsoluteBounds,
|
||||
getLinearElementAbsoluteBounds,
|
||||
} from "./bounds";
|
||||
|
||||
export { handlerRectangles } from "./handlerRectangles";
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
|
||||
export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
return element.points.length === 0;
|
||||
}
|
||||
return element.width === 0 && element.height === 0;
|
||||
}
|
||||
|
||||
|
107
src/index.tsx
107
src/index.tsx
@ -16,7 +16,6 @@ import {
|
||||
getCommonBounds,
|
||||
getCursorForResizingElement,
|
||||
getPerfectElementSize,
|
||||
resizePerfectLineForNWHandler,
|
||||
normalizeDimensions,
|
||||
} from "./element";
|
||||
import {
|
||||
@ -1050,7 +1049,10 @@ export class App extends React.Component<any, AppState> {
|
||||
editingElement: element,
|
||||
});
|
||||
return;
|
||||
} else if (this.state.elementType === "arrow") {
|
||||
} else if (
|
||||
this.state.elementType === "arrow" ||
|
||||
this.state.elementType === "line"
|
||||
) {
|
||||
if (this.state.multiElement) {
|
||||
const { multiElement } = this.state;
|
||||
const { x: rx, y: ry } = multiElement;
|
||||
@ -1107,7 +1109,7 @@ export class App extends React.Component<any, AppState> {
|
||||
const absPy = p1[1] + element.y;
|
||||
|
||||
const { width, height } = getPerfectElementSize(
|
||||
"arrow",
|
||||
element.type,
|
||||
mouseX - element.x - p1[0],
|
||||
mouseY - element.y - p1[1],
|
||||
);
|
||||
@ -1137,7 +1139,7 @@ export class App extends React.Component<any, AppState> {
|
||||
) => {
|
||||
if (perfect) {
|
||||
const { width, height } = getPerfectElementSize(
|
||||
"arrow",
|
||||
element.type,
|
||||
mouseX - element.x,
|
||||
mouseY - element.y,
|
||||
);
|
||||
@ -1179,7 +1181,11 @@ export class App extends React.Component<any, AppState> {
|
||||
// to ensure we don't create a 2-point arrow by mistake when
|
||||
// user clicks mouse in a way that it moves a tiny bit (thus
|
||||
// triggering mousemove)
|
||||
if (!draggingOccurred && this.state.elementType === "arrow") {
|
||||
if (
|
||||
!draggingOccurred &&
|
||||
(this.state.elementType === "arrow" ||
|
||||
this.state.elementType === "line")
|
||||
) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(e, this.state);
|
||||
if (distance2d(x, y, originX, originY) < DRAGGING_THRESHOLD) {
|
||||
return;
|
||||
@ -1199,10 +1205,7 @@ export class App extends React.Component<any, AppState> {
|
||||
element.type === "line" || element.type === "arrow";
|
||||
switch (resizeHandle) {
|
||||
case "nw":
|
||||
if (
|
||||
element.type === "arrow" &&
|
||||
element.points.length === 2
|
||||
) {
|
||||
if (isLinear && element.points.length === 2) {
|
||||
const [, p1] = element.points;
|
||||
|
||||
if (!resizeArrowFn) {
|
||||
@ -1226,12 +1229,8 @@ export class App extends React.Component<any, AppState> {
|
||||
element.x += deltaX;
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (isLinear) {
|
||||
resizePerfectLineForNWHandler(element, x, y);
|
||||
} else {
|
||||
element.y += element.height - element.width;
|
||||
element.height = element.width;
|
||||
}
|
||||
element.y += element.height - element.width;
|
||||
element.height = element.width;
|
||||
} else {
|
||||
element.height -= deltaY;
|
||||
element.y += deltaY;
|
||||
@ -1239,10 +1238,7 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
break;
|
||||
case "ne":
|
||||
if (
|
||||
element.type === "arrow" &&
|
||||
element.points.length === 2
|
||||
) {
|
||||
if (isLinear && element.points.length === 2) {
|
||||
const [, p1] = element.points;
|
||||
if (!resizeArrowFn) {
|
||||
if (p1[0] >= 0) {
|
||||
@ -1272,10 +1268,7 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
break;
|
||||
case "sw":
|
||||
if (
|
||||
element.type === "arrow" &&
|
||||
element.points.length === 2
|
||||
) {
|
||||
if (isLinear && element.points.length === 2) {
|
||||
const [, p1] = element.points;
|
||||
if (!resizeArrowFn) {
|
||||
if (p1[0] <= 0) {
|
||||
@ -1304,10 +1297,7 @@ export class App extends React.Component<any, AppState> {
|
||||
}
|
||||
break;
|
||||
case "se":
|
||||
if (
|
||||
element.type === "arrow" &&
|
||||
element.points.length === 2
|
||||
) {
|
||||
if (isLinear && element.points.length === 2) {
|
||||
const [, p1] = element.points;
|
||||
if (!resizeArrowFn) {
|
||||
if (p1[0] > 0 || p1[1] > 0) {
|
||||
@ -1327,18 +1317,8 @@ export class App extends React.Component<any, AppState> {
|
||||
);
|
||||
} else {
|
||||
if (e.shiftKey) {
|
||||
if (isLinear) {
|
||||
const { width, height } = getPerfectElementSize(
|
||||
element.type,
|
||||
x - element.x,
|
||||
y - element.y,
|
||||
);
|
||||
element.width = width;
|
||||
element.height = height;
|
||||
} else {
|
||||
element.width += deltaX;
|
||||
element.height = element.width;
|
||||
}
|
||||
element.width += deltaX;
|
||||
element.height = element.width;
|
||||
} else {
|
||||
element.width += deltaX;
|
||||
element.height += deltaY;
|
||||
@ -1473,34 +1453,7 @@ export class App extends React.Component<any, AppState> {
|
||||
this.state.elementType === "line" ||
|
||||
this.state.elementType === "arrow";
|
||||
|
||||
if (isLinear && x < originX) {
|
||||
width = -width;
|
||||
}
|
||||
if (isLinear && y < originY) {
|
||||
height = -height;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
this.state.elementType,
|
||||
width,
|
||||
!isLinear && y < originY ? -height : height,
|
||||
));
|
||||
|
||||
if (!isLinear && height < 0) {
|
||||
height = -height;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLinear) {
|
||||
draggingElement.x = x < originX ? originX - width : originX;
|
||||
draggingElement.y = y < originY ? originY - height : originY;
|
||||
}
|
||||
|
||||
draggingElement.width = width;
|
||||
draggingElement.height = height;
|
||||
|
||||
if (this.state.elementType === "arrow") {
|
||||
if (isLinear) {
|
||||
draggingOccurred = true;
|
||||
const points = draggingElement.points;
|
||||
let dx = x - draggingElement.x;
|
||||
@ -1521,6 +1474,24 @@ export class App extends React.Component<any, AppState> {
|
||||
pnt[0] = dx;
|
||||
pnt[1] = dy;
|
||||
}
|
||||
} else {
|
||||
if (e.shiftKey) {
|
||||
({ width, height } = getPerfectElementSize(
|
||||
this.state.elementType,
|
||||
width,
|
||||
y < originY ? -height : height,
|
||||
));
|
||||
|
||||
if (height < 0) {
|
||||
height = -height;
|
||||
}
|
||||
}
|
||||
|
||||
draggingElement.x = x < originX ? originX - width : originX;
|
||||
draggingElement.y = y < originY ? originY - height : originY;
|
||||
|
||||
draggingElement.width = width;
|
||||
draggingElement.height = height;
|
||||
}
|
||||
|
||||
draggingElement.shape = null;
|
||||
@ -1558,7 +1529,7 @@ export class App extends React.Component<any, AppState> {
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
|
||||
if (elementType === "arrow") {
|
||||
if (elementType === "arrow" || elementType === "line") {
|
||||
if (draggingElement!.points.length > 1) {
|
||||
history.resumeRecording();
|
||||
}
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { isTextElement } from "../element/typeChecks";
|
||||
import {
|
||||
getDiamondPoints,
|
||||
getArrowPoints,
|
||||
getLinePoints,
|
||||
} from "../element/bounds";
|
||||
import { getDiamondPoints, getArrowPoints } from "../element/bounds";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { Drawable } from "roughjs/bin/core";
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
@ -89,8 +85,8 @@ function generateElement(
|
||||
},
|
||||
);
|
||||
break;
|
||||
case "line":
|
||||
case "arrow": {
|
||||
const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
|
||||
const options = {
|
||||
stroke: element.strokeColor,
|
||||
strokeWidth: element.strokeWidth,
|
||||
@ -102,25 +98,21 @@ function generateElement(
|
||||
const points: Point[] = element.points.length
|
||||
? element.points
|
||||
: [[0, 0]];
|
||||
element.shape = [
|
||||
// \
|
||||
generator.line(x3, y3, x2, y2, options),
|
||||
// -----
|
||||
generator.curve(points, options),
|
||||
// /
|
||||
generator.line(x4, y4, x2, y2, options),
|
||||
];
|
||||
break;
|
||||
}
|
||||
case "line": {
|
||||
const [x1, y1, x2, y2] = getLinePoints(element);
|
||||
const options = {
|
||||
stroke: element.strokeColor,
|
||||
strokeWidth: element.strokeWidth,
|
||||
roughness: element.roughness,
|
||||
seed: element.seed,
|
||||
};
|
||||
element.shape = generator.line(x1, y1, x2, y2, options);
|
||||
|
||||
// curve is always the first element
|
||||
// this simplifies finding the curve for an element
|
||||
element.shape = [generator.curve(points, options)];
|
||||
|
||||
// add lines only in arrow
|
||||
if (element.type === "arrow") {
|
||||
const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element);
|
||||
element.shape.push(
|
||||
...[
|
||||
generator.line(x3, y3, x2, y2, options),
|
||||
generator.line(x4, y4, x2, y2, options),
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -144,13 +136,12 @@ export function renderElement(
|
||||
case "rectangle":
|
||||
case "diamond":
|
||||
case "ellipse":
|
||||
case "line": {
|
||||
generateElement(element, generator);
|
||||
context.globalAlpha = element.opacity / 100;
|
||||
rc.draw(element.shape as Drawable);
|
||||
context.globalAlpha = 1;
|
||||
break;
|
||||
}
|
||||
case "line":
|
||||
case "arrow": {
|
||||
generateElement(element, generator);
|
||||
context.globalAlpha = element.opacity / 100;
|
||||
|
@ -7,7 +7,7 @@ import { ExportType, PreviousScene } from "./types";
|
||||
import { exportToCanvas, exportToSvg } from "./export";
|
||||
import nanoid from "nanoid";
|
||||
import { fileOpen, fileSave } from "browser-nativefs";
|
||||
import { getCommonBounds } from "../element";
|
||||
import { getCommonBounds, normalizeDimensions } from "../element";
|
||||
|
||||
import { Point } from "roughjs/bin/geometry";
|
||||
import { t } from "../i18n";
|
||||
@ -291,6 +291,19 @@ function restore(
|
||||
[element.width, element.height],
|
||||
];
|
||||
}
|
||||
} else if (element.type === "line") {
|
||||
// old spec, pre-arrows
|
||||
// old spec, post-arrows
|
||||
if (!Array.isArray(element.points) || element.points.length === 0) {
|
||||
points = [
|
||||
[0, 0],
|
||||
[element.width, element.height],
|
||||
];
|
||||
} else {
|
||||
points = element.points;
|
||||
}
|
||||
} else {
|
||||
normalizeDimensions(element);
|
||||
}
|
||||
|
||||
return {
|
||||
|
Loading…
x
Reference in New Issue
Block a user