From 3d2e59bfed4fa41a0cae49ee567a6f95ca26e7bf Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 31 Jan 2020 18:56:55 +0100 Subject: [PATCH] Revert "Feature: Multi Point Arrows (#338)" (#634) This reverts commit 16263e942b7b690bb3e97340383009016129d489. --- src/actions/actionDeleteSelected.tsx | 3 +- src/actions/actionFinalize.tsx | 27 -- src/actions/index.ts | 2 - src/actions/manager.tsx | 2 +- src/actions/types.ts | 4 +- src/appState.ts | 7 - src/element/bounds.ts | 99 +----- src/element/collision.ts | 110 +------ src/element/handlerRectangles.ts | 79 +---- src/element/index.ts | 1 - src/element/newElement.ts | 2 - src/element/resizeTest.ts | 1 - src/index.tsx | 454 +++++---------------------- src/math.ts | 65 ---- src/renderer/renderElement.ts | 11 +- src/renderer/renderScene.ts | 32 +- src/scene/data.ts | 13 +- src/types.ts | 1 - src/utils.ts | 6 - 19 files changed, 130 insertions(+), 789 deletions(-) delete mode 100644 src/actions/actionFinalize.tsx diff --git a/src/actions/actionDeleteSelected.tsx b/src/actions/actionDeleteSelected.tsx index e3a80917c..5d58c9fbc 100644 --- a/src/actions/actionDeleteSelected.tsx +++ b/src/actions/actionDeleteSelected.tsx @@ -4,10 +4,9 @@ import { KEYS } from "../keys"; export const actionDeleteSelected: Action = { name: "deleteSelectedElements", - perform: (elements, appState) => { + perform: elements => { return { elements: deleteSelectedElements(elements), - appState: { ...appState, elementType: "selection", multiElement: null }, }; }, contextItemLabel: "labels.delete", diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx deleted file mode 100644 index 1a96248ab..000000000 --- a/src/actions/actionFinalize.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Action } from "./types"; -import { KEYS } from "../keys"; -import { clearSelection } from "../scene"; - -export const actionFinalize: Action = { - name: "finalize", - perform: (elements, appState) => { - if (window.document.activeElement instanceof HTMLElement) { - window.document.activeElement.blur(); - } - return { - elements: clearSelection(elements), - appState: { - ...appState, - elementType: "selection", - draggingElement: null, - multiElement: null, - }, - }; - }, - keyTest: (event, appState) => - (event.key === KEYS.ESCAPE && - !appState.draggingElement && - appState.multiElement === null) || - ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && - appState.multiElement !== null), -}; diff --git a/src/actions/index.ts b/src/actions/index.ts index 79db254fa..0b60a0fd8 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -23,8 +23,6 @@ export { actionClearCanvas, } from "./actionCanvas"; -export { actionFinalize } from "./actionFinalize"; - export { actionChangeProjectName, actionChangeExportBackground, diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index a36af04b4..757d47ca6 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -34,7 +34,7 @@ export class ActionManager implements ActionsManagerInterface { const data = Object.values(this.actions) .sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0)) .filter( - action => action.keyTest && action.keyTest(event, appState, elements), + action => action.keyTest && action.keyTest(event, elements, appState), ); if (data.length === 0) return null; diff --git a/src/actions/types.ts b/src/actions/types.ts index 178f9bc1a..921709d4f 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -29,8 +29,8 @@ export interface Action { keyPriority?: number; keyTest?: ( event: KeyboardEvent, - appState: AppState, - elements: readonly ExcalidrawElement[], + elements?: readonly ExcalidrawElement[], + appState?: AppState, ) => boolean; contextItemLabel?: string; contextMenuOrder?: number; diff --git a/src/appState.ts b/src/appState.ts index e7a828e81..8ca9fabba 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -7,7 +7,6 @@ export function getDefaultAppState(): AppState { return { draggingElement: null, resizingElement: null, - multiElement: null, editingElement: null, elementType: "selection", elementLocked: false, @@ -27,9 +26,3 @@ export function getDefaultAppState(): AppState { name: DEFAULT_PROJECT_NAME, }; } - -export function cleanAppStateForExport(appState: AppState) { - return { - viewBackgroundColor: appState.viewBackgroundColor, - }; -} diff --git a/src/element/bounds.ts b/src/element/bounds.ts index 08428379a..d3d259bf5 100644 --- a/src/element/bounds.ts +++ b/src/element/bounds.ts @@ -1,16 +1,11 @@ import { ExcalidrawElement } from "./types"; import { rotate } from "../math"; -import { Drawable } from "roughjs/bin/core"; -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); - } return [ element.width >= 0 ? element.x : element.x + element.width, // x1 element.height >= 0 ? element.y : element.y + element.height, // y1 @@ -34,95 +29,11 @@ export function getDiamondPoints(element: ExcalidrawElement) { return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY]; } -export function getArrowAbsoluteBounds(element: ExcalidrawElement) { - if (element.points.length < 2 || !element.shape) { - const { minX, minY, maxX, maxY } = element.points.reduce( - (limits, [x, y]) => { - limits.minY = Math.min(limits.minY, y); - limits.minX = Math.min(limits.minX, x); - - limits.maxX = Math.max(limits.maxX, x); - limits.maxY = Math.max(limits.maxY, y); - - return limits; - }, - { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - ); - return [ - minX + element.x, - minY + element.y, - maxX + element.x, - maxY + element.y, - ]; - } - - const shape = element.shape as Drawable[]; - - const ops = shape[1].sets[0].ops; - - let currentP: Point = [0, 0]; - - const { minX, minY, maxX, maxY } = ops.reduce( - (limits, { op, data }) => { - // There are only four operation types: - // move, bcurveTo, lineTo, and curveTo - if (op === "move") { - // change starting point - currentP = data as Point; - // move operation does not draw anything; so, it always - // returns false - } else if (op === "bcurveTo") { - // create points from bezier curve - // bezier curve stores data as a flattened array of three positions - // [x1, y1, x2, y2, x3, y3] - const p1 = [data[0], data[1]] as Point; - const p2 = [data[2], data[3]] as Point; - const p3 = [data[4], data[5]] as Point; - - const p0 = currentP; - currentP = p3; - - const equation = (t: number, idx: number) => - Math.pow(1 - t, 3) * p3[idx] + - 3 * t * Math.pow(1 - t, 2) * p2[idx] + - 3 * Math.pow(t, 2) * (1 - t) * p1[idx] + - p0[idx] * Math.pow(t, 3); - - let t = 0; - while (t <= 1.0) { - const x = equation(t, 0); - const y = equation(t, 1); - - limits.minY = Math.min(limits.minY, y); - limits.minX = Math.min(limits.minX, x); - - limits.maxX = Math.max(limits.maxX, x); - limits.maxY = Math.max(limits.maxY, y); - - t += 0.1; - } - } else if (op === "lineTo") { - // TODO: Implement this - } else if (op === "qcurveTo") { - // TODO: Implement this - } - return limits; - }, - { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }, - ); - - return [ - minX + element.x, - minY + element.y, - maxX + element.x, - maxY + element.y, - ]; -} - export function getArrowPoints(element: ExcalidrawElement) { - const points = element.points; - const [x1, y1] = points.length >= 2 ? points[points.length - 2] : [0, 0]; - const [x2, y2] = points[points.length - 1]; + const x1 = 0; + const y1 = 0; + const x2 = element.width; + const y2 = element.height; const size = 30; // pixels const distance = Math.hypot(x2 - x1, y2 - y1); @@ -135,7 +46,7 @@ export function getArrowPoints(element: ExcalidrawElement) { const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180); const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180); - return [x2, y2, x3, y3, x4, y4]; + return [x1, y1, x2, y2, x3, y3, x4, y4]; } export function getLinePoints(element: ExcalidrawElement) { diff --git a/src/element/collision.ts b/src/element/collision.ts index 97965f02d..3a4990ebb 100644 --- a/src/element/collision.ts +++ b/src/element/collision.ts @@ -2,13 +2,11 @@ import { distanceBetweenPointAndSegment } from "../math"; import { ExcalidrawElement } from "./types"; import { + getArrowPoints, getDiamondPoints, getElementAbsoluteCoords, getLinePoints, - getArrowAbsoluteBounds, } from "./bounds"; -import { Point } from "roughjs/bin/geometry"; -import { Drawable, OpSet } from "roughjs/bin/core"; function isElementDraggableFromInside(element: ExcalidrawElement): boolean { return element.backgroundColor !== "transparent" || element.isSelected; @@ -147,25 +145,18 @@ export function hitTest( lineThreshold ); } else if (element.type === "arrow") { - 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; + let [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element); + // The computation is done at the origin, we need to add a translation + x -= element.x; + y -= element.y; - const [x1, y1, x2, y2] = getArrowAbsoluteBounds(element); - if (x < x1 || y < y1 - 10 || x > x2 || y > y2 + 10) return false; - - 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) + // \ + distanceBetweenPointAndSegment(x, y, x3, y3, x2, y2) < lineThreshold || + // ----- + distanceBetweenPointAndSegment(x, y, x1, y1, x2, y2) < lineThreshold || + // / + distanceBetweenPointAndSegment(x, y, x4, y4, x2, y2) < lineThreshold ); } else if (element.type === "line") { const [x1, y1, x2, y2] = getLinePoints(element); @@ -185,82 +176,3 @@ export function hitTest( throw new Error("Unimplemented type " + element.type); } } - -const pointInBezierEquation = ( - p0: Point, - p1: Point, - p2: Point, - p3: Point, - [mx, my]: Point, -) => { - // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3 - const equation = (t: number, idx: number) => - Math.pow(1 - t, 3) * p3[idx] + - 3 * t * Math.pow(1 - t, 2) * p2[idx] + - 3 * Math.pow(t, 2) * (1 - t) * p1[idx] + - p0[idx] * Math.pow(t, 3); - - const epsilon = 20; - // go through t in increments of 0.01 - let t = 0; - while (t <= 1.0) { - const tx = equation(t, 0); - const ty = equation(t, 1); - - const diff = Math.sqrt(Math.pow(tx - mx, 2) + Math.pow(ty - my, 2)); - - if (diff < epsilon) { - return true; - } - - t += 0.01; - } - - return false; -}; - -const hitTestRoughShape = (opSet: OpSet[], x: number, y: number) => { - // read operations from first opSet - const ops = opSet[0].ops; - - // set start position as (0,0) just in case - // move operation does not exist (unlikely but it is worth safekeeping it) - let currentP: Point = [0, 0]; - - return ops.some(({ op, data }, idx) => { - // There are only four operation types: - // move, bcurveTo, lineTo, and curveTo - if (op === "move") { - // change starting point - currentP = data as Point; - // move operation does not draw anything; so, it always - // returns false - } else if (op === "bcurveTo") { - // create points from bezier curve - // bezier curve stores data as a flattened array of three positions - // [x1, y1, x2, y2, x3, y3] - const p1 = [data[0], data[1]] as Point; - const p2 = [data[2], data[3]] as Point; - const p3 = [data[4], data[5]] as Point; - - const p0 = currentP; - currentP = p3; - - // check if points are on the curve - // cubic bezier curves require four parameters - // the first parameter is the last stored position (p0) - let retVal = pointInBezierEquation(p0, p1, p2, p3, [x, y]); - - // set end point of bezier curve as the new starting point for - // upcoming operations as each operation is based on the last drawn - // position of the previous operation - return retVal; - } else if (op === "lineTo") { - // TODO: Implement this - } else if (op === "qcurveTo") { - // TODO: Implement this - } - - return false; - }); -}; diff --git a/src/element/handlerRectangles.ts b/src/element/handlerRectangles.ts index d202fd81b..ab5374ba7 100644 --- a/src/element/handlerRectangles.ts +++ b/src/element/handlerRectangles.ts @@ -1,6 +1,5 @@ import { ExcalidrawElement } from "./types"; import { SceneScroll } from "../scene/types"; -import { getArrowAbsoluteBounds } from "./bounds"; type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se"; @@ -8,31 +7,18 @@ export function handlerRectangles( element: ExcalidrawElement, { scrollX, scrollY }: SceneScroll, ) { - let elementX2 = 0; - let elementY2 = 0; - let elementX1 = Infinity; - let elementY1 = Infinity; - let marginX = -8; - let marginY = -8; - - let minimumSize = 40; - if (element.type === "arrow") { - [elementX1, elementY1, elementX2, elementY2] = getArrowAbsoluteBounds( - element, - ); - } else { - elementX1 = element.x; - elementX2 = element.x + element.width; - elementY1 = element.y; - elementY2 = element.y + element.height; - - marginX = element.width < 0 ? 8 : -8; - marginY = element.height < 0 ? 8 : -8; - } + const elementX1 = element.x; + const elementX2 = element.x + element.width; + const elementY1 = element.y; + const elementY2 = element.y + element.height; const margin = 4; + const minimumSize = 40; const handlers = {} as { [T in Sides]: number[] }; + const marginX = element.width < 0 ? 8 : -8; + const marginY = element.height < 0 ? 8 : -8; + if (Math.abs(elementX2 - elementX1) > minimumSize) { handlers["n"] = [ elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4, @@ -90,58 +76,11 @@ export function handlerRectangles( 8, ]; // se - if (element.type === "line") { + if (element.type === "arrow" || element.type === "line") { return { nw: handlers.nw, se: handlers.se, } as typeof handlers; - } else if (element.type === "arrow") { - if (element.points.length === 2) { - // only check the last point because starting point is always (0,0) - const [, p1] = element.points; - - if (p1[0] === 0 || p1[1] === 0) { - return { - nw: handlers.nw, - se: handlers.se, - } as typeof handlers; - } - - if (p1[0] > 0 && p1[1] < 0) { - return { - ne: handlers.ne, - sw: handlers.sw, - } as typeof handlers; - } - - if (p1[0] > 0 && p1[1] > 0) { - return { - nw: handlers.nw, - se: handlers.se, - } as typeof handlers; - } - - if (p1[0] < 0 && p1[1] > 0) { - return { - ne: handlers.ne, - sw: handlers.sw, - } as typeof handlers; - } - - if (p1[0] < 0 && p1[1] < 0) { - return { - nw: handlers.nw, - se: handlers.se, - } as typeof handlers; - } - } - - return { - n: handlers.n, - s: handlers.s, - w: handlers.w, - e: handlers.e, - } as typeof handlers; } return handlers; diff --git a/src/element/index.ts b/src/element/index.ts index 6a68a7276..62ed21254 100644 --- a/src/element/index.ts +++ b/src/element/index.ts @@ -5,7 +5,6 @@ export { getDiamondPoints, getArrowPoints, getLinePoints, - getArrowAbsoluteBounds, } from "./bounds"; export { handlerRectangles } from "./handlerRectangles"; diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 812afb4c5..f5ce32e63 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -1,7 +1,6 @@ import { randomSeed } from "roughjs/bin/math"; import nanoid from "nanoid"; import { Drawable } from "roughjs/bin/core"; -import { Point } from "roughjs/bin/geometry"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { measureText } from "../utils"; @@ -35,7 +34,6 @@ export function newElement( isSelected: false, seed: randomSeed(), shape: null as Drawable | Drawable[] | null, - points: [] as Point[], }; return element; } diff --git a/src/element/resizeTest.ts b/src/element/resizeTest.ts index 532b9b94f..0e8c98a2d 100644 --- a/src/element/resizeTest.ts +++ b/src/element/resizeTest.ts @@ -17,7 +17,6 @@ export function resizeTest( const filter = Object.keys(handlers).filter(key => { const handler = handlers[key as HandlerRectanglesRet]!; - if (!handler) return false; return ( x + scrollX >= handler[0] && diff --git a/src/index.tsx b/src/index.tsx index d1147bd91..0806f01b5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -42,13 +42,7 @@ import { renderScene } from "./renderer"; import { AppState } from "./types"; import { ExcalidrawElement } from "./element/types"; -import { - isInputLike, - debounce, - capitalizeString, - distance, - distance2d, -} from "./utils"; +import { isInputLike, debounce, capitalizeString, distance } from "./utils"; import { KEYS, isArrowKey } from "./keys"; import { findShapeByKey, shapesShortcutKeys, SHAPES } from "./shapes"; @@ -82,7 +76,6 @@ import { actionSaveScene, actionCopyStyles, actionPasteStyles, - actionFinalize, } from "./actions"; import { Action, ActionResult } from "./actions/types"; import { getDefaultAppState } from "./appState"; @@ -95,7 +88,6 @@ import { ExportDialog } from "./components/ExportDialog"; import { withTranslation } from "react-i18next"; import { LanguageList } from "./components/LanguageList"; import i18n, { languages, parseDetectedLang } from "./i18n"; -import { Point } from "roughjs/bin/geometry"; import { StoredScenesList } from "./components/StoredScenesList"; let { elements } = createScene(); @@ -117,7 +109,6 @@ function setCursorForShape(shape: string) { } } -const DRAGGING_THRESHOLD = 10; // 10px const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5; const ELEMENT_TRANSLATE_AMOUNT = 1; const TEXT_TO_CENTER_SNAP_THRESHOLD = 30; @@ -177,7 +168,6 @@ export class App extends React.Component { canvasOnlyActions: Array; constructor(props: any) { super(props); - this.actionManager.registerAction(actionFinalize); this.actionManager.registerAction(actionDeleteSelected); this.actionManager.registerAction(actionSendToBack); this.actionManager.registerAction(actionBringToFront); @@ -338,7 +328,17 @@ export class App extends React.Component { }; private onKeyDown = (event: KeyboardEvent) => { - if (isInputLike(event.target) && event.key !== KEYS.ESCAPE) return; + if (event.key === KEYS.ESCAPE && !this.state.draggingElement) { + elements = clearSelection(elements); + this.setState({}); + this.setState({ elementType: "selection" }); + if (window.document.activeElement instanceof HTMLElement) { + window.document.activeElement.blur(); + } + event.preventDefault(); + return; + } + if (isInputLike(event.target)) return; const actionResult = this.actionManager.handleKeyDown( event, @@ -387,27 +387,19 @@ export class App extends React.Component { } else if (event[KEYS.META] && event.code === "KeyZ") { event.preventDefault(); - if ( - this.state.resizingElement || - this.state.multiElement || - this.state.editingElement - ) { - return; - } - if (event.shiftKey) { // Redo action const data = history.redoOnce(); if (data !== null) { elements = data.elements; - this.setState({ ...data.appState }); + this.setState(data.appState); } } else { // undo action const data = history.undoOnce(); if (data !== null) { elements = data.elements; - this.setState({ ...data.appState }); + this.setState(data.appState); } } } else if (event.key === KEYS.SPACE && !isHoldingMouseButton) { @@ -578,7 +570,7 @@ export class App extends React.Component { aria-label={capitalizeString(label)} aria-keyshortcuts={`${label[0]} ${index + 1}`} onChange={() => { - this.setState({ elementType: value, multiElement: null }); + this.setState({ elementType: value }); elements = clearSelection(elements); document.documentElement.style.cursor = value === "text" ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR; @@ -1044,28 +1036,11 @@ export class App extends React.Component { editingElement: element, }); return; - } else if (this.state.elementType === "arrow") { - if (this.state.multiElement) { - const { multiElement } = this.state; - const { x: rx, y: ry } = multiElement; - multiElement.isSelected = true; - multiElement.points.push([x - rx, y - ry]); - multiElement.shape = null; - this.setState({ draggingElement: multiElement }); - } else { - element.isSelected = false; - element.points.push([0, 0]); - element.shape = null; - elements = [...elements, element]; - this.setState({ - draggingElement: element, - }); - } - } else { - elements = [...elements, element]; - this.setState({ multiElement: null, draggingElement: element }); } + elements = [...elements, element]; + this.setState({ draggingElement: element }); + let lastX = x; let lastY = y; @@ -1074,75 +1049,6 @@ export class App extends React.Component { lastY = e.clientY - CANVAS_WINDOW_OFFSET_TOP; } - let resizeArrowFn: - | (( - element: ExcalidrawElement, - p1: Point, - deltaX: number, - deltaY: number, - mouseX: number, - mouseY: number, - perfect: boolean, - ) => void) - | null = null; - - const arrowResizeOrigin = ( - element: ExcalidrawElement, - p1: Point, - deltaX: number, - deltaY: number, - mouseX: number, - mouseY: number, - perfect: boolean, - ) => { - // TODO: Implement perfect sizing for origin - if (perfect) { - const absPx = p1[0] + element.x; - const absPy = p1[1] + element.y; - - let { width, height } = getPerfectElementSize( - "arrow", - mouseX - element.x - p1[0], - mouseY - element.y - p1[1], - ); - - const dx = element.x + width + p1[0]; - const dy = element.y + height + p1[1]; - element.x = dx; - element.y = dy; - p1[0] = absPx - element.x; - p1[1] = absPy - element.y; - } else { - element.x += deltaX; - element.y += deltaY; - p1[0] -= deltaX; - p1[1] -= deltaY; - } - }; - - const arrowResizeEnd = ( - element: ExcalidrawElement, - p1: Point, - deltaX: number, - deltaY: number, - mouseX: number, - mouseY: number, - perfect: boolean, - ) => { - if (perfect) { - const { width, height } = getPerfectElementSize( - "arrow", - mouseX - element.x, - mouseY - element.y, - ); - p1[0] = width; - p1[1] = height; - } else { - p1[0] += deltaX; - p1[1] += deltaY; - } - }; - const onMouseMove = (e: MouseEvent) => { const target = e.target; if (!(target instanceof HTMLElement)) { @@ -1169,16 +1075,6 @@ export class App extends React.Component { return; } - // for arrows, don't start dragging until a given threshold - // 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") { - const { x, y } = viewportCoordsToSceneCoords(e, this.state); - if (distance2d(x, y, originX, originY) < DRAGGING_THRESHOLD) - return; - } - if (isResizingElements && this.state.resizingElement) { const el = this.state.resizingElement; const selectedElements = elements.filter(el => el.isSelected); @@ -1191,217 +1087,73 @@ export class App extends React.Component { element.type === "line" || element.type === "arrow"; switch (resizeHandle) { case "nw": - if ( - element.type === "arrow" && - element.points.length === 2 - ) { - const [, p1] = element.points; - - if (!resizeArrowFn) { - if (p1[0] < 0 || p1[1] < 0) { - resizeArrowFn = arrowResizeEnd; - } else { - resizeArrowFn = arrowResizeOrigin; - } - } - resizeArrowFn( - element, - p1, - deltaX, - deltaY, - x, - y, - e.shiftKey, - ); - } else { - element.width -= deltaX; - element.x += deltaX; - - if (e.shiftKey) { - if (isLinear) { - resizePerfectLineForNWHandler(element, x, y); - } else { - element.y += element.height - element.width; - element.height = element.width; - } - } else { - element.height -= deltaY; - element.y += deltaY; - } - } - break; - case "ne": - if ( - element.type === "arrow" && - element.points.length === 2 - ) { - const [, p1] = element.points; - if (!resizeArrowFn) { - if (p1[0] >= 0) { - resizeArrowFn = arrowResizeEnd; - } else { - resizeArrowFn = arrowResizeOrigin; - } - } - resizeArrowFn( - element, - p1, - deltaX, - deltaY, - x, - y, - e.shiftKey, - ); - } else { - element.width += deltaX; - if (e.shiftKey) { - element.y += element.height - element.width; - element.height = element.width; - } else { - element.height -= deltaY; - element.y += deltaY; - } - } - break; - case "sw": - if ( - element.type === "arrow" && - element.points.length === 2 - ) { - const [, p1] = element.points; - if (!resizeArrowFn) { - if (p1[0] <= 0) { - resizeArrowFn = arrowResizeEnd; - } else { - resizeArrowFn = arrowResizeOrigin; - } - } - resizeArrowFn( - element, - p1, - deltaX, - deltaY, - x, - y, - e.shiftKey, - ); - } else { - element.width -= deltaX; - element.x += deltaX; - if (e.shiftKey) { - element.height = element.width; - } else { - element.height += deltaY; - } - } - break; - case "se": - if ( - element.type === "arrow" && - element.points.length === 2 - ) { - const [, p1] = element.points; - if (!resizeArrowFn) { - if (p1[0] > 0 || p1[1] > 0) { - resizeArrowFn = arrowResizeEnd; - } else { - resizeArrowFn = arrowResizeOrigin; - } - } - resizeArrowFn( - element, - p1, - deltaX, - deltaY, - x, - y, - e.shiftKey, - ); - } 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; - } - } else { - element.width += deltaX; - element.height += deltaY; - } - } - break; - case "n": { - element.height -= deltaY; - element.y += deltaY; - - if (element.points.length > 0) { - const len = element.points.length; - - const points = [...element.points].sort( - (a, b) => a[1] - b[1], - ); - - for (let i = 1; i < points.length; ++i) { - const pnt = points[i]; - pnt[1] -= deltaY / (len - i); - } - } - break; - } - case "w": { element.width -= deltaX; element.x += deltaX; - if (element.points.length > 0) { - const len = element.points.length; - const points = [...element.points].sort( - (a, b) => a[0] - b[0], - ); - - for (let i = 0; i < points.length; ++i) { - const pnt = points[i]; - pnt[0] -= deltaX / (len - i); + if (e.shiftKey) { + if (isLinear) { + resizePerfectLineForNWHandler(element, x, y); + } else { + element.y += element.height - element.width; + element.height = element.width; } + } else { + element.height -= deltaY; + element.y += deltaY; } break; - } - case "s": { - element.height += deltaY; - if (element.points.length > 0) { - const len = element.points.length; - const points = [...element.points].sort( - (a, b) => a[1] - b[1], - ); - - for (let i = 1; i < points.length; ++i) { - const pnt = points[i]; - pnt[1] += deltaY / (len - i); - } - } - break; - } - case "e": { + case "ne": element.width += deltaX; - if (element.points.length > 0) { - const len = element.points.length; - const points = [...element.points].sort( - (a, b) => a[0] - b[0], - ); - - for (let i = 1; i < points.length; ++i) { - const pnt = points[i]; - pnt[0] += deltaX / (len - i); - } + if (e.shiftKey) { + element.y += element.height - element.width; + element.height = element.width; + } else { + element.height -= deltaY; + element.y += deltaY; } break; - } + case "sw": + element.width -= deltaX; + element.x += deltaX; + if (e.shiftKey) { + element.height = element.width; + } else { + element.height += deltaY; + } + break; + case "se": + 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; + } + } else { + element.width += deltaX; + element.height += deltaY; + } + break; + case "n": + element.height -= deltaY; + element.y += deltaY; + break; + case "w": + element.width -= deltaX; + element.x += deltaX; + break; + case "s": + element.height += deltaY; + break; + case "e": + element.width += deltaX; + break; } if (resizeHandle) { @@ -1483,30 +1235,6 @@ export class App extends React.Component { draggingElement.width = width; draggingElement.height = height; - - if (this.state.elementType === "arrow") { - draggingOccurred = true; - const points = draggingElement.points; - let dx = x - draggingElement.x; - let dy = y - draggingElement.y; - - if (e.shiftKey && points.length === 2) { - ({ width: dx, height: dy } = getPerfectElementSize( - this.state.elementType, - dx, - dy, - )); - } - - if (points.length === 1) { - points.push([dx, dy]); - } else if (points.length > 1) { - const pnt = points[points.length - 1]; - pnt[0] = dx; - pnt[1] = dy; - } - } - draggingElement.shape = null; if (this.state.elementType === "selection") { @@ -1530,33 +1258,15 @@ export class App extends React.Component { const { draggingElement, resizingElement, - multiElement, elementType, elementLocked, } = this.state; - resizeArrowFn = null; lastMouseUp = null; isHoldingMouseButton = false; window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); - if (elementType === "arrow") { - if (draggingElement!.points.length > 1) { - history.resumeRecording(); - } - if (!draggingOccurred && !multiElement) { - this.setState({ multiElement: this.state.draggingElement }); - } else if (draggingOccurred && !multiElement) { - this.state.draggingElement!.isSelected = true; - this.setState({ - draggingElement: null, - elementType: "selection", - }); - } - return; - } - if ( elementType !== "selection" && draggingElement && @@ -1641,15 +1351,9 @@ export class App extends React.Component { window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); - if ( - !this.state.multiElement || - (this.state.multiElement && - this.state.multiElement.points.length < 2) - ) { - // We don't want to save history on mouseDown, only on mouseUp when it's fully configured - history.skipRecording(); - this.setState({}); - } + // We don't want to save history on mouseDown, only on mouseUp when it's fully configured + history.skipRecording(); + this.setState({}); }} onDoubleClick={e => { const { x, y } = viewportCoordsToSceneCoords(e, this.state); diff --git a/src/math.ts b/src/math.ts index fae4d950c..bbcbe17d8 100644 --- a/src/math.ts +++ b/src/math.ts @@ -1,5 +1,3 @@ -import { Point } from "roughjs/bin/geometry"; - // https://stackoverflow.com/a/6853926/232122 export function distanceBetweenPointAndSegment( x: number, @@ -54,66 +52,3 @@ export function rotate( (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2, ]; } - -export const getPointOnAPath = (point: Point, path: Point[]) => { - const [px, py] = point; - const [start, ...other] = path; - let [lastX, lastY] = start; - let kLine: number = 0; - let idx: number = 0; - - // if any item in the array is true, it means that a point is - // on some segment of a line based path - const retVal = other.some(([x2, y2], i) => { - // we always take a line when dealing with line segments - const x1 = lastX; - const y1 = lastY; - - lastX = x2; - lastY = y2; - - // if a point is not within the domain of the line segment - // it is not on the line segment - if (px < x1 || px > x2) { - return false; - } - - // check if all points lie on the same line - // y1 = kx1 + b, y2 = kx2 + b - // y2 - y1 = k(x2 - x2) -> k = (y2 - y1) / (x2 - x1) - - // coefficient for the line (p0, p1) - const kL = (y2 - y1) / (x2 - x1); - - // coefficient for the line segment (p0, point) - const kP1 = (py - y1) / (px - x1); - - // coefficient for the line segment (point, p1) - const kP2 = (py - y2) / (px - x2); - - // because we are basing both lines from the same starting point - // the only option for collinearity is having same coefficients - - // using it for floating point comparisons - const epsilon = 0.3; - - // if coefficient is more than an arbitrary epsilon, - // these lines are nor collinear - if (Math.abs(kP1 - kL) > epsilon && Math.abs(kP2 - kL) > epsilon) { - return false; - } - - // store the coefficient because we are goint to need it - kLine = kL; - idx = i; - - return true; - }); - - // Return a coordinate that is always on the line segment - if (retVal === true) { - return { x: point[0], y: kLine * point[0], segment: idx }; - } - - return null; -}; diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 0e9d175ab..cbe33110c 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -7,7 +7,6 @@ import { } from "../element/bounds"; import { RoughCanvas } from "roughjs/bin/canvas"; import { Drawable } from "roughjs/bin/core"; -import { Point } from "roughjs/bin/geometry"; import { RoughSVG } from "roughjs/bin/svg"; import { RoughGenerator } from "roughjs/bin/generator"; import { SVG_NS } from "../utils"; @@ -90,23 +89,18 @@ function generateElement( ); break; case "arrow": { - const [x2, y2, x3, y3, x4, y4] = getArrowPoints(element); + const [x1, y1, x2, y2, x3, y3, x4, y4] = getArrowPoints(element); const options = { stroke: element.strokeColor, strokeWidth: element.strokeWidth, roughness: element.roughness, seed: element.seed, }; - // points array can be empty in the beginning, so it is important to add - // initial position to it - 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(x1, y1, x2, y2, options), // / generator.line(x4, y4, x2, y2, options), ]; @@ -175,6 +169,7 @@ export function renderElement( context.fillStyle = fillStyle; context.font = font; context.globalAlpha = 1; + break; } else { throw new Error("Unimplemented type " + element.type); } diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 80b2daf00..59d0a36df 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -76,7 +76,10 @@ export function renderScene( element.y + sceneState.scrollY, ); renderElement(element, rc, context); - context.resetTransform(); + context.translate( + -element.x - sceneState.scrollX, + -element.y - sceneState.scrollY, + ); }); if (renderSelection) { @@ -104,11 +107,9 @@ export function renderScene( if (selectedElements.length === 1 && selectedElements[0].type !== "text") { const handlers = handlerRectangles(selectedElements[0], sceneState); - Object.values(handlers) - .filter(handler => handler !== undefined) - .forEach(handler => { - context.strokeRect(handler[0], handler[1], handler[2], handler[3]); - }); + Object.values(handlers).forEach(handler => { + context.strokeRect(handler[0], handler[1], handler[2], handler[3]); + }); } } @@ -148,20 +149,11 @@ function isVisibleElement( canvasHeight: number, ) { let [x1, y1, x2, y2] = getElementAbsoluteCoords(element); - if (element.type !== "arrow") { - x1 += scrollX; - y1 += scrollY; - x2 += scrollX; - y2 += scrollY; - return x2 >= 0 && x1 <= canvasWidth && y2 >= 0 && y1 <= canvasHeight; - } else { - return ( - x2 + scrollX >= 0 && - x1 + scrollX <= canvasWidth && - y2 + scrollY >= 0 && - y1 + scrollY <= canvasHeight - ); - } + x1 += scrollX; + y1 += scrollY; + x2 += scrollX; + y2 += scrollY; + return x2 >= 0 && x1 <= canvasWidth && y2 >= 0 && y1 <= canvasHeight; } // This should be only called for exporting purposes diff --git a/src/scene/data.ts b/src/scene/data.ts index 2c380c7c6..f9481b463 100644 --- a/src/scene/data.ts +++ b/src/scene/data.ts @@ -1,6 +1,6 @@ import { ExcalidrawElement } from "../element/types"; -import { getDefaultAppState, cleanAppStateForExport } from "../appState"; +import { getDefaultAppState } from "../appState"; import { AppState } from "../types"; import { ExportType, PreviousScene } from "./types"; @@ -24,7 +24,7 @@ const BACKEND_GET = "https://json.excalidraw.com/api/v1/"; interface DataState { elements: readonly ExcalidrawElement[]; - appState: AppState | null; + appState: AppState; selectedId?: number; } @@ -36,9 +36,10 @@ export function serializeAsJSON( { type: "excalidraw", version: 1, - source: window.location.origin, + appState: { + viewBackgroundColor: appState.viewBackgroundColor, + }, elements: elements.map(({ shape, isSelected, ...el }) => el), - appState: cleanAppStateForExport(appState), }, null, 2, @@ -254,7 +255,7 @@ export async function exportCanvas( function restore( savedElements: readonly ExcalidrawElement[], - savedState: AppState | null, + savedState: AppState, ): DataState { return { elements: savedElements.map(element => ({ @@ -290,7 +291,7 @@ export function restoreFromLocalStorage() { let appState = null; if (savedState) { try { - appState = JSON.parse(savedState) as AppState; + appState = JSON.parse(savedState); } catch (e) { // Do nothing because appState is already null } diff --git a/src/types.ts b/src/types.ts index f85d2ebde..284c1d876 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,6 @@ import { ExcalidrawElement } from "./element/types"; export type AppState = { draggingElement: ExcalidrawElement | null; resizingElement: ExcalidrawElement | null; - multiElement: ExcalidrawElement | null; // element being edited, but not necessarily added to elements array yet // (e.g. text element when typing into the input) editingElement: ExcalidrawElement | null; diff --git a/src/utils.ts b/src/utils.ts index 5e667e6a0..ff7ea588b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -103,9 +103,3 @@ export function removeSelection() { export function distance(x: number, y: number) { return Math.abs(x - y); } - -export function distance2d(x1: number, y1: number, x2: number, y2: number) { - const xd = x2 - x1; - const yd = y2 - y1; - return Math.sqrt(xd * xd + yd * yd); -}