From dee8a73d3d62aa78ebb3f3023e660d6e3286b52b Mon Sep 17 00:00:00 2001 From: Paulo Menezes Date: Sat, 4 Jan 2020 15:56:11 -0300 Subject: [PATCH] Resize (#103) * Resize * Detect collision with squares * Disable resize for text, arrow and multiple selection * Hide middle handlers when small --- src/index.tsx | 263 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 236 insertions(+), 27 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index b1909f56..f95b49e5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -170,6 +170,39 @@ function hitTest(element: ExcalidrawElement, x: number, y: number): boolean { } } +function resizeTest( + element: ExcalidrawElement, + x: number, + y: number, + sceneState: SceneState +): string | false { + if (element.type === "text" || element.type === "arrow") return false; + + const x1 = getElementAbsoluteX1(element); + const x2 = getElementAbsoluteX2(element); + const y1 = getElementAbsoluteY1(element); + const y2 = getElementAbsoluteY2(element); + + const handlers = handlerRectangles(x1, x2, y1, y2, sceneState); + + const filter = Object.keys(handlers).filter(key => { + const handler = handlers[key]; + + return ( + x + sceneState.scrollX >= handler[0] && + x + sceneState.scrollX <= handler[0] + handler[2] && + y + sceneState.scrollY >= handler[1] && + y + sceneState.scrollY <= handler[1] + handler[3] + ); + }); + + if (filter.length > 0) { + return filter[0]; + } + + return false; +} + function newElement( type: string, x: number, @@ -243,6 +276,77 @@ function getScrollbars( }; } +function handlerRectangles( + elementX1: number, + elementX2: number, + elementY1: number, + elementY2: number, + sceneState: SceneState +) { + const margin = 4; + const minimumSize = 40; + const handlers: { [handler: string]: number[] } = {}; + + if (elementX2 - elementX1 > minimumSize) { + handlers["n"] = [ + elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4, + elementY1 - margin + sceneState.scrollY - 8, + 8, + 8 + ]; + + handlers["s"] = [ + elementX1 + (elementX2 - elementX1) / 2 + sceneState.scrollX - 4, + elementY2 - margin + sceneState.scrollY + 8, + 8, + 8 + ]; + } + + if (elementY2 - elementY1 > minimumSize) { + handlers["w"] = [ + elementX1 - margin + sceneState.scrollX - 8, + elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4, + 8, + 8 + ]; + + handlers["e"] = [ + elementX2 - margin + sceneState.scrollX + 8, + elementY1 + (elementY2 - elementY1) / 2 + sceneState.scrollY - 4, + 8, + 8 + ]; + } + + handlers["nw"] = [ + elementX1 - margin + sceneState.scrollX - 8, + elementY1 - margin + sceneState.scrollY - 8, + 8, + 8 + ]; // nw + handlers["ne"] = [ + elementX2 - margin + sceneState.scrollX + 8, + elementY1 - margin + sceneState.scrollY - 8, + 8, + 8 + ]; // ne + handlers["sw"] = [ + elementX1 - margin + sceneState.scrollX - 8, + elementY2 - margin + sceneState.scrollY + 8, + 8, + 8 + ]; // sw + handlers["se"] = [ + elementX2 - margin + sceneState.scrollX + 8, + elementY2 - margin + sceneState.scrollY + 8, + 8, + 8 + ]; // se + + return handlers; +} + function renderScene( rc: RoughCanvas, context: CanvasRenderingContext2D, @@ -259,6 +363,8 @@ function renderScene( } context.fillStyle = fillStyle; + const selectedIndices = getSelectedIndices(); + elements.forEach(element => { element.draw(rc, context, sceneState); if (element.isSelected) { @@ -277,6 +383,23 @@ function renderScene( elementY2 - elementY1 + margin * 2 ); context.setLineDash(lineDash); + + if ( + element.type !== "text" && + element.type !== "arrow" && + selectedIndices.length === 1 + ) { + const handlers = handlerRectangles( + elementX1, + elementX2, + elementY1, + elementY2, + sceneState + ); + Object.values(handlers).forEach(handler => { + context.strokeRect(handler[0], handler[1], handler[2], handler[3]); + }); + } } }); @@ -595,6 +718,7 @@ function restore() { type AppState = { draggingElement: ExcalidrawElement | null; + resizingElement: ExcalidrawElement | null; elementType: string; exportBackground: boolean; exportVisibleOnly: boolean; @@ -693,6 +817,7 @@ class App extends React.Component<{}, AppState> { public state: AppState = { draggingElement: null, + resizingElement: null, elementType: "selection", exportBackground: false, exportVisibleOnly: true, @@ -1018,40 +1143,65 @@ class App extends React.Component<{}, AppState> { this.state.currentItemStrokeColor, this.state.currentItemBackgroundColor ); + let resizeHandle: string | false = false; let isDraggingElements = false; + let isResizingElements = false; const cursorStyle = document.documentElement.style.cursor; if (this.state.elementType === "selection") { - let hitElement = null; - // We need to to hit testing from front (end of the array) to back (beginning of the array) - for (let i = elements.length - 1; i >= 0; --i) { - if (hitTest(elements[i], x, y)) { - hitElement = elements[i]; - break; - } - } + const resizeElement = elements.find(element => { + return resizeTest(element, x, y, { + scrollX: this.state.scrollX, + scrollY: this.state.scrollY, + viewBackgroundColor: this.state.viewBackgroundColor + }); + }); - // If we click on something - if (hitElement) { - if (hitElement.isSelected) { - // If that element is not already selected, do nothing, - // we're likely going to drag it - } else { - // We unselect every other elements unless shift is pressed - if (!e.shiftKey) { - clearSelection(); - } - // No matter what, we select it - hitElement.isSelected = true; - } + this.setState({ + resizingElement: resizeElement ? resizeElement : null + }); + + if (resizeElement) { + resizeHandle = resizeTest(resizeElement, x, y, { + scrollX: this.state.scrollX, + scrollY: this.state.scrollY, + viewBackgroundColor: this.state.viewBackgroundColor + }); + document.documentElement.style.cursor = `${resizeHandle}-resize`; + isResizingElements = true; } else { - // If we don't click on anything, let's remove all the selected elements - clearSelection(); - } + let hitElement = null; + + // We need to to hit testing from front (end of the array) to back (beginning of the array) + for (let i = elements.length - 1; i >= 0; --i) { + if (hitTest(elements[i], x, y)) { + hitElement = elements[i]; + break; + } + } - isDraggingElements = someElementIsSelected(); + // If we click on something + if (hitElement) { + if (hitElement.isSelected) { + // If that element is not already selected, do nothing, + // we're likely going to drag it + } else { + // We unselect every other elements unless shift is pressed + if (!e.shiftKey) { + clearSelection(); + } + // No matter what, we select it + hitElement.isSelected = true; + } + } else { + // If we don't click on anything, let's remove all the selected elements + clearSelection(); + } - if (isDraggingElements) { - document.documentElement.style.cursor = "move"; + isDraggingElements = someElementIsSelected(); + + if (isDraggingElements) { + document.documentElement.style.cursor = "move"; + } } } @@ -1100,6 +1250,65 @@ class App extends React.Component<{}, AppState> { return; } + if (isResizingElements && this.state.resizingElement) { + const el = this.state.resizingElement; + const selectedElements = elements.filter(el => el.isSelected); + if (selectedElements.length === 1) { + const x = e.clientX - target.offsetLeft - this.state.scrollX; + const y = e.clientY - target.offsetTop - this.state.scrollY; + selectedElements.forEach(element => { + switch (resizeHandle) { + case "nw": + element.width += element.x - lastX; + element.height += element.y - lastY; + element.x = lastX; + element.y = lastY; + break; + case "ne": + element.width = lastX - element.x; + element.height += element.y - lastY; + element.y = lastY; + break; + case "sw": + element.width += element.x - lastX; + element.x = lastX; + element.height = lastY - element.y; + break; + case "se": + element.width += x - lastX; + if (e.shiftKey) { + element.height = element.width; + } else { + element.height += y - lastY; + } + break; + case "n": + element.height += element.y - lastY; + element.y = lastY; + break; + case "w": + element.width += element.x - lastX; + element.x = lastX; + break; + case "s": + element.height = lastY - element.y; + break; + case "e": + element.width = lastX - element.x; + break; + } + + el.x = element.x; + el.y = element.y; + generateDraw(el); + }); + lastX = x; + lastY = y; + this.forceUpdate(); + return; + } + } + if (isDraggingElements) { const selectedElements = elements.filter(el => el.isSelected); if (selectedElements.length) {