working on fixing canvas expansion

This commit is contained in:
paul
2024-09-10 21:34:05 +05:30
parent ac65b1d97b
commit 7947bd599f
2 changed files with 342 additions and 71 deletions

View File

@@ -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 <Widget /> 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 {
</Tooltip>
</div>
<div className="tw-w-full tw-relative tw-h-full tw-bg-red-300 tw-overflow-hidden"
<div className="tw-w-full tw-relative tw-h-full tw-overflow-hidden"
ref={this.canvasContainerRef}
style={{transition: " transform 0.3s ease-in-out"}}
>

View File

@@ -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()}
<div className={`tw-absolute tw-bg-transparent tw-scale-[1.1] tw-opacity-100
tw-w-full tw-h-full tw-top-0
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-border-none'}`}>
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}>
<div className="tw-relative tw-w-full tw-h-full">
{/* <div contentEditable="true" onClick={(e) => e.preventDefault()} className="tw-text-sm tw-w-fit tw-min-w-[100px] tw-absolute tw--top-2">
{this._widgetName}
</div> */}
{ this.state.selected &&
<EditableDiv value={this.state.widgetName} onChange={onWidgetNameChange}
<EditableDiv value={this.state.widgetName} onChange={onWidgetNameChange}
maxLength={40}
className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px]
tw-overflow-hidden tw-absolute tw--top-4 tw-h-6"
tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
/>
}
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.NW_RESIZE }}
onMouseDown={(e) => this.startResizing("nw", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => this.startResizing("ne", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => this.startResizing("sw", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SE_RESIZE }}
onMouseDown={(e) => this.startResizing("se", e)}
/>
</div>