From 7947bd599ff562a3dc716d61884e698b90f4195c Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 10 Sep 2024 21:34:05 +0530 Subject: [PATCH] working on fixing canvas expansion --- src/canvas/canvas.js | 253 ++++++++++++++++++++++++++++--------- src/canvas/widgets/base.js | 160 +++++++++++++++++++++-- 2 files changed, 342 insertions(+), 71 deletions(-) diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index af11f16..656eb57 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -8,6 +8,13 @@ import Cursor from "./constants/cursor" import { UID } from "../utils/uid" +const CanvasModes = { + DEFAULT: 0, + PAN: 1, + MOVE_WIDGET: 2 // when the mode is move widget +} + + class Canvas extends React.Component { constructor(props) { @@ -16,15 +23,13 @@ class Canvas extends React.Component { this.canvasRef = React.createRef() this.canvasContainerRef = React.createRef() - this.widgetRefs = {} + this.widgetRefs = {} // stores the actual refs to the widgets inside the canvas + + + this.currentMode = CanvasModes.DEFAULT + + this.minCanvasSize = {width: 500, height: 500} - this.modes = { - DEFAULT: 0, - PAN: 1, - MOVE_WIDGET: 2 // when the mode is move widget - } - this.currentMode = this.modes.DEFAULT - this.mousePressed = false this.mousePos = { x: 0, @@ -36,6 +41,7 @@ class Canvas extends React.Component { zoom: 1, isPanning: false, currentTranslate: { x: 0, y: 0 }, + canvasSize: { width: 500, height: 500 }, } this.selectedWidgets = [] @@ -51,6 +57,14 @@ class Canvas extends React.Component { this.getActiveObjects = this.getActiveObjects.bind(this) this.getWidgetFromTarget = this.getWidgetFromTarget.bind(this) + this.getCanvasObjectsBoundingBox = this.getCanvasObjectsBoundingBox.bind(this) + this.fitCanvasToBoundingBox = this.fitCanvasToBoundingBox.bind(this) + + this.updateWidgetPosition = this.updateWidgetPosition.bind(this) + + this.checkAndExpandCanvas = this.checkAndExpandCanvas.bind(this) + this.expandCanvas = this.expandCanvas.bind(this) + this.clearSelections = this.clearSelections.bind(this) this.clearCanvas = this.clearCanvas.bind(this) @@ -98,7 +112,7 @@ class Canvas extends React.Component { // this.canvasRef.current.addEventListener("selection:created", () => { // console.log("selected") - // this.currentMode = this.modes.DEFAULT + // this.currentMode = Modes.DEFAULT // }) this.canvasContainerRef.current.addEventListener('wheel', (event) => { @@ -139,17 +153,17 @@ class Canvas extends React.Component { if (!selectedWidget._disableSelection){ selectedWidget.select() this.selectedWidgets.push(selectedWidget) - this.currentMode = this.modes.MOVE_WIDGET + this.currentMode = CanvasModes.MOVE_WIDGET } - this.currentMode = this.modes.PAN + this.currentMode = CanvasModes.PAN }else if (this.state?.widgets?.length > 0){ - + // get the canvas ready to pan, if there are widgets on the canvas this.clearSelections() - this.currentMode = this.modes.PAN + this.currentMode = CanvasModes.PAN this.setCursor(Cursor.GRAB) } @@ -158,18 +172,20 @@ class Canvas extends React.Component { mouseMoveEvent(event){ // console.log("mode: ", this.currentMode, this.getActiveObjects()) - if (this.mousePressed && [this.modes.PAN, this.modes.MOVE_WIDGET].includes(this.currentMode)) { + if (this.mousePressed && [CanvasModes.PAN, CanvasModes.MOVE_WIDGET].includes(this.currentMode)) { const deltaX = event.clientX - this.mousePos.x const deltaY = event.clientY - this.mousePos.y if (this.selectedWidgets.length === 0){ + // if there aren't any selected widgets, then pan the canvas this.setState(prevState => ({ currentTranslate: { x: prevState.currentTranslate.x + deltaX, y: prevState.currentTranslate.y + deltaY, } }), this.applyTransform) + }else{ // update the widgets position this.selectedWidgets.forEach(widget => { @@ -179,7 +195,10 @@ class Canvas extends React.Component { const newPosY = y + (deltaY/this.state.zoom) // account for the zoom, since the widget is relative to canvas widget.setPos(newPosX, newPosY) + this.checkAndExpandCanvas(newPosX, newPosY, widget.getSize().width, widget.getSize().height) }) + // this.fitCanvasToBoundingBox(10) + } @@ -191,8 +210,10 @@ class Canvas extends React.Component { mouseUpEvent(event){ this.mousePressed = false - this.currentMode = this.modes.DEFAULT + this.currentMode = CanvasModes.DEFAULT this.setCursor(Cursor.DEFAULT) + + } wheelZoom(event){ @@ -201,12 +222,141 @@ class Canvas extends React.Component { this.setZoom(zoom, {x: event.offsetX, y: event.offsetY}) } + checkAndExpandCanvas(widgetX, widgetY, widgetWidth, widgetHeight) { + const canvasWidth = this.canvasRef.current.offsetWidth + const canvasHeight = this.canvasRef.current.offsetHeight + + const canvasRect = this.canvasRef.current.getBoundingClientRect() + + // Get the zoom level + const zoom = this.state.zoom + + // Calculate effective canvas boundaries considering zoom + const effectiveCanvasRight = canvasWidth + const effectiveCanvasBottom = canvasHeight + + // Calculate widget boundaries + const widgetRight = widgetX + widgetWidth + const widgetBottom = widgetY + widgetHeight + + // Determine if expansion is needed + const expandRight = widgetRight > effectiveCanvasRight + const expandDown = widgetBottom > effectiveCanvasBottom + const expandLeft = widgetX < canvasRect.left * this.state.zoom + const expandUp = widgetY < canvasRect.top + + if (expandRight || expandLeft || expandDown || expandUp) { + this.expandCanvas(expandRight, expandLeft, expandDown, expandUp, widgetX, widgetY, widgetWidth, widgetHeight) + } + } + + // Expand the canvas method + /** + * + * @param {boolean} expandRight + * @param {boolean} expandLeft + * @param {boolean} expandDown + * @param {boolean} expandUp + * @param {number} widgetX + * @param {number} widgetY + * @param {number} widgetRight + * @param {number} widgetBottom + */ + expandCanvas(expandRight, expandLeft, expandDown, expandUp, widgetX, widgetY, widgetWidth, widgetHeight) { + const currentWidth = this.canvasRef.current.offsetWidth + const currentHeight = this.canvasRef.current.offsetHeight + + console.log("current: ", expandRight, expandDown, expandLeft, expandUp) + + let newWidth = currentWidth + let newHeight = currentHeight + let newTranslateX = this.state.currentTranslate.x + let newTranslateY = this.state.currentTranslate.y + + if (expandRight) { + // const requiredWidth = widgetRight - newTranslateX // Add padding + // newWidth = Math.max(requiredWidth, currentWidth) + newWidth = currentWidth + 50 + } + + if (expandLeft) { + // const leftOffset = widgetX + newTranslateX // Position of the widget relative to the left edge + // const requiredLeftExpansion = -leftOffset + 50 // Add padding + newWidth = currentWidth + widgetWidth + newTranslateX -= widgetWidth // Adjust translation to move the canvas to the left + } + + if (expandDown) { + newHeight = currentHeight + 50 + + // const requiredHeight = widgetBottom - newTranslateY // Add padding + // newHeight = Math.max(requiredHeight, currentHeight) + } + + if (expandUp) { + newHeight = currentHeight + widgetHeight + newTranslateY -= widgetHeight + // const topOffset = widgetY + newTranslateY // Position of the widget relative to the top edge + // const requiredTopExpansion = -topOffset + 50 // Add padding + // newHeight = currentHeight + requiredTopExpansion + // newTranslateY -= requiredTopExpansion // Adjust translation to move the canvas upwards + } + + // Apply new dimensions and translation + this.canvasRef.current.style.width = `${newWidth}px` + this.canvasRef.current.style.height = `${newHeight}px` + + console.log("translate: ", this.canvasRef.current.offsetWidth, ) + // Now, to keep the widget in the same relative position: + const updatedWidgetX = widgetX - newTranslateX / this.state.zoom; + const updatedWidgetY = widgetY - newTranslateY / this.state.zoom; + + this.setState({ + currentTranslate: { + x: newTranslateX, + y: newTranslateY + } + }, () => { + this.applyTransform() + this.updateWidgetPosition(updatedWidgetX, updatedWidgetY, widgetWidth, widgetHeight) + }) + + } + + // TODO: FIX this, to ensure that the widget position remains the same + // Function to update the widget's position based on new updated canvas coordinates, use it after expandCanvas + updateWidgetPosition(widgetX, widgetY, widgetWidth, widgetHeight) { + const widgetElement = this.selectedWidgets[0].current; // Assuming the widget is referenced via `widgetRef` + + console.log("widget element: ", this.selectedWidgets[0].current) + widgetElement.style.left = `${widgetX}px`; + widgetElement.style.top = `${widgetY}px`; + widgetElement.style.width = `${widgetWidth}px`; + widgetElement.style.height = `${widgetHeight}px`; + } + + /** * fits the canvas size to fit the widgets bounding box */ - fitCanvasToBoundingBox(){ - // this.canvasRef.current.style.width = this.canvasContainerRef.current.clientWidth - // this.canvasRef.current.style.height = this.canvasContainerRef.current.clientHeight + fitCanvasToBoundingBox(padding=0){ + const { top, left, right, bottom } = this.getCanvasObjectsBoundingBox() + + const width = right - left + const height = bottom - top + + const newWidth = Math.max(width + padding, this.minCanvasSize.width) + const newHeight = Math.max(height + padding, this.minCanvasSize.height) + + const canvasStyle = this.canvasRef.current.style + + // Adjust the canvas dimensions + canvasStyle.width = `${newWidth}px` + canvasStyle.height = `${newHeight}px` + + // Adjust the canvas position if needed + canvasStyle.left = `${left - padding}px` + canvasStyle.top = `${top - padding}px` } setCursor(cursor){ @@ -233,40 +383,6 @@ class Canvas extends React.Component { }, this.applyTransform) } - - getCanvasObjectsBoundingBox(padding = 0) { - const objects = this.fabricCanvas.getObjects() - if (objects.length === 0) { - return { left: 0, top: 0, width: this.fabricCanvas.width, height: this.fabricCanvas.height } - } - - const boundingBox = objects.reduce((acc, obj) => { - const objBoundingBox = obj.getBoundingRect(true) - acc.left = Math.min(acc.left, objBoundingBox.left) - acc.top = Math.min(acc.top, objBoundingBox.top) - acc.right = Math.max(acc.right, objBoundingBox.left + objBoundingBox.width) - acc.bottom = Math.max(acc.bottom, objBoundingBox.top + objBoundingBox.height) - return acc - }, { - left: Infinity, - top: Infinity, - right: -Infinity, - bottom: -Infinity - }) - - // Adding padding - boundingBox.left -= padding - boundingBox.top -= padding - boundingBox.right += padding - boundingBox.bottom += padding - - return { - left: boundingBox.left, - top: boundingBox.top, - width: boundingBox.right - boundingBox.left, - height: boundingBox.bottom - boundingBox.top - } - } resetTransforms() { this.setState({ @@ -282,6 +398,30 @@ class Canvas extends React.Component { this.selectedWidgets = [] } + /** + * returns tha combined bounding rect of all the widgets on the canvas + * + */ + getCanvasObjectsBoundingBox(){ + + // Initialize coordinates to opposite extremes + let top = Number.POSITIVE_INFINITY + let left = Number.POSITIVE_INFINITY + let right = Number.NEGATIVE_INFINITY + let bottom = Number.NEGATIVE_INFINITY + + for (let widget of Object.values(this.widgetRefs)) { + const rect = widget.current.getBoundingRect() + // Update the top, left, right, and bottom coordinates + if (rect.top < top) top = rect.top + if (rect.left < left) left = rect.left + if (rect.right > right) right = rect.right + if (rect.bottom > bottom) bottom = rect.bottom + } + + return { top, left, right, bottom } + } + /** * * @param {Widget} widgetComponentType - don't pass instead pass Widget object @@ -321,10 +461,9 @@ class Canvas extends React.Component { this.widgetRefs[widgetId]?.current.remove() delete this.widgetRefs[widgetId] - // TODO: remove from widgets - // this.setState(() => ({ - // widgets: [] - // })) + this.setState((prevState) => ({ + widgets: prevState.widgets.filter(widget => widget.id !== widgetId) + })) } renderWidget(widget){ @@ -346,7 +485,7 @@ class Canvas extends React.Component { -
diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index 8b03379..8e0e9f3 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -31,6 +31,9 @@ class Widget extends React.Component{ this._disableResize = false this._disableSelection = false + this.minSize = {width: 50, height: 50} // disable resizing below this number + this.maxSize = {width: 500, height: 500} // disable resizing above this number + this.cursor = Cursor.POINTER this.icon = "" // antd icon name representing this widget @@ -76,18 +79,26 @@ class Widget extends React.Component{ }, zIndex: 0, pos: {x: 0, y: 0}, + size: { width: 100, height: 100 }, selected: false, - widgetName: widgetName || 'unnamed widget' // this will later be converted to variable name + widgetName: widgetName || 'unnamed widget', // this will later be converted to variable name + resizing: false, + resizeCorner: "" } this.mousePress = this.mousePress.bind(this) this.getElement = this.getElement.bind(this) + this.getBoundingRect = this.getBoundingRect.bind(this) this.isSelected = this.isSelected.bind(this) this.getPos = this.getPos.bind(this) this.setPos = this.setPos.bind(this) + this.startResizing = this.startResizing.bind(this) + this.handleResize = this.handleResize.bind(this) + this.stopResizing = this.stopResizing.bind(this) + } @@ -101,10 +112,16 @@ class Widget extends React.Component{ componentDidMount(){ console.log("mounted: ") this.elementRef.current?.addEventListener("click", this.mousePress) + + this.canvas.addEventListener("mousemove", this.handleResize); + this.canvas.addEventListener("mouseup", this.stopResizing) } componentWillUnmount(){ this.elementRef.current?.removeEventListener("click", this.mousePress) + + this.canvas.addEventListener("mousemove", this.handleResize); + this.canvas.addEventListener("mouseup", this.stopResizing) } // TODO: add context menu items such as delete, add etc @@ -143,8 +160,6 @@ class Widget extends React.Component{ this.setState({ selected: false }) - console.log("DeSelected") - } isSelected(){ @@ -152,6 +167,12 @@ class Widget extends React.Component{ } setPos(x, y){ + + if (this.state.resizing){ + // don't change position when resizing the widget + return + } + this.setState({ pos: {x: x, y: y} }) @@ -165,6 +186,48 @@ class Widget extends React.Component{ return this.attrs } + getBoundingRect(){ + return this.elementRef.current?.getBoundingClientRect() + } + + getSize(){ + const boundingRect = this.getBoundingRect() + + return {width: boundingRect.width, height: boundingRect.height} + } + + getScaleAwareDimensions() { + // Get the bounding rectangle + const rect = this.elementRef.current.getBoundingClientRect() + + // Get the computed style of the element + const style = window.getComputedStyle(this.elementRef.current) + + // Get the transform matrix + const transform = style.transform + + // Extract scale factors from the matrix + let scaleX = 1 + let scaleY = 1 + + if (transform && transform !== 'none') { + // For 2D transforms (a, c, b, d) + const matrix = transform.match(/^matrix\(([^,]+),[^,]+,([^,]+),[^,]+,[^,]+,[^,]+\)$/); + + if (matrix) { + scaleX = parseFloat(matrix[1]) + scaleY = parseFloat(matrix[2]) + } + } + + // Return scaled width and height + return { + width: rect.width / scaleX, + height: rect.height / scaleY + } + } + + getWidgetFunctions(){ return this.functions } @@ -177,6 +240,59 @@ class Widget extends React.Component{ return this.elementRef.current } + startResizing(corner, event) { + event.stopPropagation() + this.setState({ resizing: true, resizeCorner: corner }) + } + + handleResize(event) { + if (!this.state.resizing) return + + const { resizeCorner, size, pos } = this.state + const deltaX = event.movementX + const deltaY = event.movementY + + let newSize = { ...size } + let newPos = { ...pos } + + const {width: minWidth, height: minHeight} = this.minSize + const {width: maxWidth, height: maxHeight} = this.maxSize + console.log("resizing: ", minHeight, minHeight) + + switch (resizeCorner) { + case "nw": + newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) + newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) + newPos.x += (newSize.width !== size.width) ? deltaX : 0 + newPos.y += (newSize.height !== size.height) ? deltaY : 0 + break + case "ne": + newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) + newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) + newPos.y += (newSize.height !== size.height) ? deltaY : 0 + break + case "sw": + newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) + newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) + newPos.x += (newSize.width !== size.width) ? deltaX : 0 + break + case "se": + newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) + newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) + break + default: + break + } + + this.setState({ size: newSize, pos: newPos }) + } + + stopResizing() { + if (this.state.resizing) { + this.setState({ resizing: false }) + } + } + renderContent(){ // throw new NotImplementedError("render method has to be implemented") return ( @@ -196,8 +312,8 @@ class Widget extends React.Component{ cursor: this.cursor, top: `${this.state.pos.y}px`, left: `${this.state.pos.x}px`, - width: this.boundingRect.width, - height: this.boundingRect.height + width: `${this.state.size.width}px`, + height: `${this.state.size.height}px`, } let selectionStyle = { @@ -224,20 +340,36 @@ class Widget extends React.Component{ {this.renderContent()}
+ ${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}>
- - {/*
e.preventDefault()} className="tw-text-sm tw-w-fit tw-min-w-[100px] tw-absolute tw--top-2"> - {this._widgetName} -
*/} - { this.state.selected && - - } + +
this.startResizing("nw", e)} + /> +
this.startResizing("ne", e)} + /> +
this.startResizing("sw", e)} + /> +
this.startResizing("se", e)} + /> +