|
|
|
|
@ -170,6 +170,39 @@ function hitTest(element: ExcalidrawElement, x: number, y: number): boolean {
@@ -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(
@@ -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(
@@ -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(
@@ -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() {
@@ -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> {
@@ -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> {
@@ -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 |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 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(); |
|
|
|
|
// 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; |
|
|
|
|
} |
|
|
|
|
// 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(); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
// If we don't click on anything, let's remove all the selected elements
|
|
|
|
|
clearSelection(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
isDraggingElements = someElementIsSelected(); |
|
|
|
|
isDraggingElements = someElementIsSelected(); |
|
|
|
|
|
|
|
|
|
if (isDraggingElements) { |
|
|
|
|
document.documentElement.style.cursor = "move"; |
|
|
|
|
if (isDraggingElements) { |
|
|
|
|
document.documentElement.style.cursor = "move"; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -1100,6 +1250,65 @@ class App extends React.Component<{}, AppState> {
@@ -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) { |
|
|
|
|
|