skip to Main Content

It is necessary to make it so that blocks regardless of width and length do not overlap with each other and do not go beyond the borders of the canvas when moving and when resizing. Blocks can be either full width or full length, and with the side of the minimum cell.
If anyone has a solution or at least the direction in which to move I will be very happy

Here is my code. It does not work quite correctly, because in some cases it jumps to the wrong zones behind the canvas or inside another block.

const canvasSize = { width: 1923, height: 1083 }
const cellDimensions = { width: 120, height: 120 }
const borderStrokeWidth = 3
const objectTypes = {
    frame: 'frame',
    gridPoint: 'point',
    gridLine: 'line',
}

const initialFramesList = [
    {
        top: 120,
        left: 120,
        width: 240,
        height: 480,
    },
    {
        top: 120,
        left: 600,
        width: 480,
        height: 720,
    },
]

let drawingCanvas

initCanvas()
initFrames(initialFramesList)

function initCanvas(id = 'canvas') {
    let startX
    let startY
    let endX
    let endY
    let activeFrame

    drawingCanvas = new fabric.Canvas(id, {
        height: canvasSize.height + borderStrokeWidth,
        width: canvasSize.width + borderStrokeWidth,
        hoverCursor: 'default',
        backgroundColor: 'gray',
        selection: false,
    })

    const onClickPoint = (e) => {
        const isGridPoint = e.target?.type === objectTypes.gridPoint

        if (isGridPoint && !activeFrame) {
            const { top: y, left: x, width: d } = e.target
            const r = d / 2
            startX = x + r
            startY = y + r
            activeFrame = createFrame({ x: startX, y: startY })
            drawingCanvas.add(activeFrame)
        } else if (activeFrame) {
            startX = null
            startY = null
            endX = null
            endY = null
            activeFrame.setCoords()
            activeFrame = null
            drawingCanvas.renderAll()
        }
    }

    const widgetsLoop = (fn) => {
        const widgets = drawingCanvas.getObjects(objectTypes.frame)
        widgets.forEach((obj) => {
            fn(obj)
        })
    }

    const getAbsolutePosition = (el) => {
        return el.group
            ? { x: el.group.left, y: el.group.top }
            : { x: el.left, y: el.top }
    }

    const hasIntersection = (target, obj) => {
        const { x: targetLeft, y: targetTop } = getAbsolutePosition(target)
        const { x: objLeft, y: objTop } = getAbsolutePosition(obj)
        const {
            width: targetWidth,
            height: targetHeight,
            originX: targetOriginX,
            originY: targetOriginY,
        } = target
        const { width: objWidth, height: objHeight } = obj

        const rectLeftCorrect =
            targetOriginX === 'right'
                ? targetLeft - targetWidth - borderStrokeWidth
                : targetLeft
        const rectTopCorrect =
            targetOriginY === 'bottom'
                ? targetTop - targetHeight - borderStrokeWidth
                : targetTop

        const xIntersection =
            rectLeftCorrect + targetWidth > objLeft &&
            rectLeftCorrect < objLeft + objWidth
        const yIntersection =
            rectTopCorrect + targetHeight > objTop &&
            rectTopCorrect < objTop + objHeight

        return xIntersection && yIntersection
    }

    const limitFrameMoving = (target) => {
        const { width: screenW, height: screenH } = canvasSize
        const { x: rectLeft, y: rectTop } = getAbsolutePosition(target)
        const {
            originX: targetOriginX,
            originY: targetOriginY,
            width: targetW,
            height: targetH,
        } = target
        const rectLeftCorrect =
            targetOriginX === 'right'
                ? rectLeft - targetW - borderStrokeWidth
                : rectLeft
        const rectTopCorrect =
            targetOriginY === 'bottom'
                ? rectTop - targetH - borderStrokeWidth
                : rectTop

        if (rectLeftCorrect < 0) {
            target.set({ left: 0 })
        } else if (rectLeftCorrect + targetW > screenW) {
            target.set({ left: screenW - targetW })
        }

        if (rectTopCorrect < 0) {
            target.set({ top: 0 })
        } else if (rectTopCorrect + targetH > screenH) {
            target.set({ top: screenH - targetH })
        }
    }

    const setValidSize = (target) => {
        const { width: cellW, height: cellH } = cellDimensions

        widgetsLoop((obj) => {
            if (obj === target) {
                return
            }

            if (hasIntersection(target, obj)) {
                const { x: rectLeft, y: rectTop } = getAbsolutePosition(target)
                const { x: objLeft, y: objTop } = getAbsolutePosition(obj)
                const { originX: targetOriginX, originY: targetOriginY } =
                    target
                const { width: objWidth, height: objHeight } = obj

                const rectLeftCorrect =
                    targetOriginX === 'right'
                        ? rectLeft - objWidth - borderStrokeWidth
                        : rectLeft
                const rectTopCorrect =
                    targetOriginY === 'bottom'
                        ? rectTop - objHeight - borderStrokeWidth
                        : rectTop

                const dx = Math.abs(rectLeftCorrect - objLeft)
                const dy = Math.abs(rectTopCorrect - objTop)

                if (dx > dy && target.width > dx) {
                    target.set({ width: dx, cellY: dx / cellW })
                } else if (dx < dy && target.height > dy) {
                    target.set({ height: dy, cellY: dy / cellH })
                }
            }
        })
    }

    const setValidPosition = (target) => {
        widgetsLoop((obj) => {
            if (obj === target) {
                return
            }

            limitFrameMoving(target)

            if (hasIntersection(target, obj)) {
                const dx = Math.abs(target.left - obj.left)
                const dy = Math.abs(target.top - obj.top)
                if (dx > dy) {
                    target.set({ left: obj.left - target.width })
                    if (target.left < 0) {
                        target.set({ left: obj.left + obj.width })
                    }
                } else {
                    target.set({ top: obj.top - target.height })
                    if (target.top < 0) {
                        target.set({ top: obj.top + obj.height })
                    }
                }
            }
        })
        drawingCanvas.renderAll()
    }

    drawingCanvas.on('mouse:over', (e) => {
        if (e.target?.type === objectTypes.gridPoint) {
            drawingCanvas.bringToFront(e.target)
            e.target.set('opacity', 1)
            drawingCanvas.renderAll()
        }
    })

    drawingCanvas.on('mouse:out', (e) => {
        if (e.target?.type === objectTypes.gridPoint) {
            e.target.set('opacity', 0)
            drawingCanvas.renderAll()
        }
    })

    drawingCanvas.on('mouse:down', (e) => {
        onClickPoint(e)
    })

    drawingCanvas.on('mouse:move', (e) => {
        if (!activeFrame) {
            return
        }

        const { width: cellW, height: cellH } = cellDimensions
        const pointerX = Math.floor(e.pointer.x / cellW) * cellW
        const pointerY = Math.floor(e.pointer.y / cellH) * cellH

        const width = pointerX - startX + cellW
        const height = pointerY - startY + cellH

        if (pointerX >= startX) {
            activeFrame.set({ width, cellX: width / cellW })
        }

        if (pointerY >= startY) {
            activeFrame.set({ height, cellY: height / cellH })
        }

        setValidSize(activeFrame)

        drawingCanvas.renderAll()
    })

    drawingCanvas.on('object:scaling', (e) => {
        const target = e.target
        const { width: cellW, height: cellH } = cellDimensions
        const width = Math.round((target.width * target.scaleX) / cellW) * cellW
        const height =
            Math.round((target.height * target.scaleY) / cellH) * cellH
        const { originX, originY, corner } = e.transform
        const { y: top, x: left } = target.getPointByOrigin(originX, originY)

        target.set({
            originX,
            originY,
            left,
            top,
            scaleX: 1,
            scaleY: 1,
            width: width > cellW ? width : cellW,
            height: height > cellH ? height : cellH,
            cellX: width / cellW,
            cellY: height / cellH,
            lockScalingFlip: true,
        })

        setValidSize(target)
    })

    drawingCanvas.on('object:moving', (e) => {
        const target = e.target
        const { width: cellW, height: cellH } = cellDimensions
        const left = Math.floor(target.left / cellW) * cellW
        const top = Math.floor(target.top / cellH) * cellH
        target.set({
            left,
            top,
        })
        setValidPosition(target)
    })

    drawingCanvas.on('object:modified', (e) => {
        const obj = e.target
        const { y: top, x: left } = obj.getPointByOrigin('left', 'top')
        obj.set({ top, left, originX: 'left', originY: 'top' })
        drawingCanvas.renderAll()
    })

    drawGrid()
}

function drawGrid() {
    const { width: colSize, height: rowSize } = cellDimensions

    const makePoint = (x, y) => {
        const r = 8
        const point = new fabric.Rect({
            top: y - r,
            left: x - r,
            width: r * 2,
            height: r * 2,
            rx: 1,
            ry: 1,
            fill: 'black',
            hasControls: false,
            hasBorders: false,
            selectable: false,
            opacity: 0,
            type: objectTypes.gridPoint,
            hoverCursor: 'pointer',
        })

        drawingCanvas.add(point)
    }

    const drawLine = (params) => {
        const line = new fabric.Line(params, {
            stroke: 'yellow',
            borderStrokeWidth: borderStrokeWidth,
            selectable: false,
            evented: false,
            type: objectTypes.gridLine,
        })
        drawingCanvas.add(line)
    }

    const makeRows = () => {
        const rowsCount = canvasSize.height / rowSize
        for (let i = 0; i < rowsCount; i++) {
            const y = i * rowSize
            drawLine([0, y, canvasSize.width, y])
        }
    }

    const makeCols = () => {
        const colsCount = canvasSize.width / colSize
        for (let i = 0; i < colsCount; i++) {
            const x = i * colSize
            drawLine([x, 0, x, canvasSize.height])
        }
    }

    const makePoints = () => {
        const rowsCount = canvasSize.height / rowSize
        const colsCount = canvasSize.width / colSize
        for (let i = 0; i < colsCount; i++) {
            const x = i * colSize
            for (let j = 0; j < rowsCount; j++) {
                const y = j * rowSize
                makePoint(x, y)
            }
        }
    }

    makeRows()
    makeCols()
    makePoints()
}

function initFrames(frames) {
    const { width: cellW, height: cellH } = cellDimensions
    frames.forEach(({ top, left, width, height }) => {
        const frame = createFrame(
            {
                x: left,
                y: top,
            },
            {
                width: width,
                height: height,
            }
        )
        frame.set({
            cellX: width / cellW,
            cellY: height / cellH,
            lockScalingFlip: true,
        })
        drawingCanvas.add(frame)
    })
}

function createFrame({ x, y }, { width, height } = { width: 0, height: 0 }) {
    const rect = new fabric.Rect({
        top: y,
        left: x,
        width,
        height,
        stroke: 'gray',
        borderStrokeWidth: borderStrokeWidth,
        fill: 'white',
        type: objectTypes.frame,
        strokeUniform: true,
        noScaleCache: false,
        objectCaching: false,
    })

    return rect
}
<canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.4.0/fabric.min.js"></script>

Everything I’ve tried is listed in my code. The element rotation will not be used, I will exclude it from the library later.

2

Answers


  1. Chosen as BEST ANSWER

    I made a new arrangement of blocks. If there are more than two blocks, they start overlapping. I used your example as a basis. Thanks for your help.
    GIF example

    const canvasSize = { width: 520, height: 400 }
    const cellDimensions = { width: 40, height: 40 }
    const borderStrokeWidth = 3
    const objectTypes = {frame: 'frame', gridPoint: 'point', gridLine: 'line'}
    
    const initialFramesList = [
      {
          top: 40,
          left: 80,
          width: 200,
          height: 40,
      },
      {
          top: 120,
          left: 200,
          width: 80,
          height: 120,
      },
      {
          top: 280,
          left: 0,
          width: 360,
          height: 80,
      },
      {
          top: 0,
          left: 360,
          width: 120,
          height: 400,
      },
    ]
    
    let drawingCanvas
    
    initCanvas()
    initFrames(initialFramesList)
    
    function initCanvas(id = 'canvas') {
        let startX
        let startY
        let endX
        let endY
        let activeFrame
    
        drawingCanvas = new fabric.Canvas(id, {
            height: canvasSize.height + borderStrokeWidth,
            width: canvasSize.width + borderStrokeWidth,
            hoverCursor: 'default',
            backgroundColor: 'gray',
            selection: false,
        })
        drawingCanvas.stateful = true;
    
        const widgetsLoop = (fn) => {
            const widgets = drawingCanvas.getObjects(objectTypes.frame)
            widgets.forEach((obj) => { fn(obj) })
        }
    
        const getAbsolutePosition = (el) => {
            return el.group
                ? { x: el.group.left, y: el.group.top }
                : { x: el.left, y: el.top }
        }
    
        const limitFrameMoving = (target) => {
            const { width: screenW, height: screenH } = canvasSize
            const { x: rectLeft, y: rectTop } = getAbsolutePosition(target)
            const {
                originX: targetOriginX,
                originY: targetOriginY,
                width: targetW,
                height: targetH,
            } = target
            const rectLeftCorrect =
                targetOriginX === 'right'
                    ? rectLeft - targetW - borderStrokeWidth
                    : rectLeft
            const rectTopCorrect =
                targetOriginY === 'bottom'
                    ? rectTop - targetH - borderStrokeWidth
                    : rectTop
    
            if (rectLeftCorrect < 0) {
                target.set({ left: 0 })
            } else if (rectLeftCorrect + targetW > screenW) {
                target.set({ left: screenW - targetW })
            }
    
            if (rectTopCorrect < 0) {
                target.set({ top: 0 })
            } else if (rectTopCorrect + targetH > screenH) {
                target.set({ top: screenH - targetH })
            }
        }
    
        const hasIntersection = (target, obj) => {
            const { x: targetLeft, y: targetTop } = getAbsolutePosition(target)
            const { x: objLeft, y: objTop } = getAbsolutePosition(obj)
            const {
                width: targetWidth,
                height: targetHeight,
                originX: targetOriginX,
                originY: targetOriginY,
            } = target
            const { width: objWidth, height: objHeight } = obj
    
            const rectLeftCorrect =
                targetOriginX === 'right'
                    ? targetLeft - targetWidth - borderStrokeWidth
                    : targetLeft
            const rectTopCorrect =
                targetOriginY === 'bottom'
                    ? targetTop - targetHeight - borderStrokeWidth
                    : targetTop
    
            const xIntersection =
                rectLeftCorrect + targetWidth > objLeft &&
                rectLeftCorrect < objLeft + objWidth
            const yIntersection =
                rectTopCorrect + targetHeight > objTop &&
                rectTopCorrect < objTop + objHeight
            
            return xIntersection && yIntersection
        }
    
        const setValidPosition = (target) => {
            widgetsLoop((obj) => {
                if (obj === target) {
                    return
                }
                limitFrameMoving(target)
    
                if (hasIntersection(target, obj)) {
                    target.set({ 
                      left: target._stateProperties.left, 
                      top: target._stateProperties.top, })
                } else {
                    target.saveState();
                }
            })
            drawingCanvas.renderAll()
        }
    
        drawingCanvas.on('mouse:over', (e) => {
            if (e.target?.type === objectTypes.gridPoint) {
                drawingCanvas.bringToFront(e.target)
                e.target.set('opacity', 1)
                drawingCanvas.renderAll()
            }
        })
    
        drawingCanvas.on('mouse:out', (e) => {
            if (e.target?.type === objectTypes.gridPoint) {
                e.target.set('opacity', 0)
                drawingCanvas.renderAll()
            }
        })
    
        drawingCanvas.on('object:moving', (e) => {
            const target = e.target
            const { width: cellW, height: cellH } = cellDimensions
            const left = Math.floor(target.left / cellW) * cellW
            const top = Math.floor(target.top / cellH) * cellH
            target.set({ left, top })
            setValidPosition(target)
        })
    
        drawingCanvas.on('object:modified', (e) => {
            const obj = e.target
            const { y: top, x: left } = obj.getPointByOrigin('left', 'top')
            obj.set({ top, left, originX: 'left', originY: 'top' })
            drawingCanvas.renderAll()
        })
    
        drawGrid()
    }
    
    function drawGrid() {
        const { width: colSize, height: rowSize } = cellDimensions
    
        const drawLine = (params) => {
            const line = new fabric.Line(params, {
                stroke: 'yellow',
                borderStrokeWidth: borderStrokeWidth,
                selectable: false,
                evented: false,
                type: objectTypes.gridLine,
            })
            drawingCanvas.add(line)
        }
    
        const makeRows = () => {
            const rowsCount = canvasSize.height / rowSize
            for (let i = 0; i < rowsCount; i++) {
                const y = i * rowSize
                drawLine([0, y, canvasSize.width, y])
            }
        }
    
        const makeCols = () => {
            const colsCount = canvasSize.width / colSize
            for (let i = 0; i < colsCount; i++) {
                const x = i * colSize
                drawLine([x, 0, x, canvasSize.height])
            }
        }
    
        makeRows()
        makeCols()
    }
    
    function initFrames(frames) {
        const { width: cellW, height: cellH } = cellDimensions
        frames.forEach(({ top, left, width, height }) => {
            const frame = createFrame(
                { x: left, y: top },
                { width: width, height: height }
            )
            frame.set({
                cellX: width / cellW,
                cellY: height / cellH,
                lockScalingFlip: true,
            })
            drawingCanvas.add(frame)
        })
    }
    
    function createFrame({ x, y }, { width, height } = { width: 0, height: 0 }) {
        const rect = new fabric.Rect({
            top: y,
            left: x,
            width,
            height,
            stroke: 'gray',
            borderStrokeWidth: borderStrokeWidth,
            fill: 'white',
            type: objectTypes.frame,
            strokeUniform: true,
            noScaleCache: false,
            objectCaching: false,
        })
        drawingCanvas.setActiveObject(rect)
        return rect
    }
    <canvas id="canvas"></canvas>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.4.0/fabric.min.js"></script>


  2. Here is something that I think might be what you call a jump to the wrong zone.

    We can see I’m moving the left block horizontally but it jumps down to the bottom…

    So I’m just going to focus on that horizontal movement problem, and that alone…
    Looking at your code you have setValidPosition and inside that there is a condition:
    if (hasIntersection(target, obj)) {

    I really could not understand your logic there, I think all that is needed there is set the position back to a known good state, there should not be any moving when there is an intersection.

    Two things I will do:
    First enable stateful on the canvas

    drawingCanvas.stateful = true;
    

    then change the setValidPosition to

    const setValidPosition = (target) => {
      widgetsLoop((obj) => {
        if (obj === target) {
          return
        }
        if (hasIntersection(target, obj)) {
          target.set({ left: target._stateProperties.left, top: target._stateProperties.top })
        } else {
          target.saveState();
        }
      })
      drawingCanvas.renderAll()
    }
    

    Here is the result, I’m still moving the left block horizontally but we can see that while the mouse is over the second block there is not jumping down like there was before


    I removed some of your code to just focus on the movement and keep the code as small as possible, see working snippet below

    const canvasSize = { width: 520, height: 400 }
    const cellDimensions = { width: 40, height: 40 }
    const borderStrokeWidth = 3
    const objectTypes = {frame: 'frame', gridPoint: 'point', gridLine: 'line'}
    
    const initialFramesList = [
        {top: 40, left: 200, width: 80, height: 200},
        {top: 40, left: 40, width: 80, height: 120}
    ]
    
    let drawingCanvas
    
    initCanvas()
    initFrames(initialFramesList)
    
    function initCanvas(id = 'canvas') {
        let startX
        let startY
        let endX
        let endY
        let activeFrame
    
        drawingCanvas = new fabric.Canvas(id, {
            height: canvasSize.height + borderStrokeWidth,
            width: canvasSize.width + borderStrokeWidth,
            hoverCursor: 'default',
            backgroundColor: 'gray',
            selection: false,
        })
        drawingCanvas.stateful = true;
    
        const widgetsLoop = (fn) => {
            const widgets = drawingCanvas.getObjects(objectTypes.frame)
            widgets.forEach((obj) => { fn(obj) })
        }
    
        const getAbsolutePosition = (el) => {
            return el.group
                ? { x: el.group.left, y: el.group.top }
                : { x: el.left, y: el.top }
        }
    
        const hasIntersection = (target, obj) => {
            const { x: targetLeft, y: targetTop } = getAbsolutePosition(target)
            const { x: objLeft, y: objTop } = getAbsolutePosition(obj)
            const {
                width: targetWidth,
                height: targetHeight,
                originX: targetOriginX,
                originY: targetOriginY,
            } = target
            const { width: objWidth, height: objHeight } = obj
    
            const rectLeftCorrect =
                targetOriginX === 'right'
                    ? targetLeft - targetWidth - borderStrokeWidth
                    : targetLeft
            const rectTopCorrect =
                targetOriginY === 'bottom'
                    ? targetTop - targetHeight - borderStrokeWidth
                    : targetTop
    
            const xIntersection =
                rectLeftCorrect + targetWidth > objLeft &&
                rectLeftCorrect < objLeft + objWidth
            const yIntersection =
                rectTopCorrect + targetHeight > objTop &&
                rectTopCorrect < objTop + objHeight
            
            return xIntersection && yIntersection
        }
    
        const setValidPosition = (target) => {
            widgetsLoop((obj) => {
                if (obj === target) {
                    return
                }
                if (hasIntersection(target, obj)) {
                    target.set({ 
                      left: target._stateProperties.left, 
                      top: target._stateProperties.top, })
                } else {
                    target.saveState();
                }
            })
            drawingCanvas.renderAll()
        }
    
        drawingCanvas.on('mouse:over', (e) => {
            if (e.target?.type === objectTypes.gridPoint) {
                drawingCanvas.bringToFront(e.target)
                e.target.set('opacity', 1)
                drawingCanvas.renderAll()
            }
        })
    
        drawingCanvas.on('mouse:out', (e) => {
            if (e.target?.type === objectTypes.gridPoint) {
                e.target.set('opacity', 0)
                drawingCanvas.renderAll()
            }
        })
    
        drawingCanvas.on('object:moving', (e) => {
            const target = e.target
            const { width: cellW, height: cellH } = cellDimensions
            const left = Math.floor(target.left / cellW) * cellW
            const top = Math.floor(target.top / cellH) * cellH
            target.set({ left, top })
            setValidPosition(target)
        })
    
        drawingCanvas.on('object:modified', (e) => {
            const obj = e.target
            const { y: top, x: left } = obj.getPointByOrigin('left', 'top')
            obj.set({ top, left, originX: 'left', originY: 'top' })
            drawingCanvas.renderAll()
        })
    
        drawGrid()
    }
    
    function drawGrid() {
        const { width: colSize, height: rowSize } = cellDimensions
    
        const drawLine = (params) => {
            const line = new fabric.Line(params, {
                stroke: 'yellow',
                borderStrokeWidth: borderStrokeWidth,
                selectable: false,
                evented: false,
                type: objectTypes.gridLine,
            })
            drawingCanvas.add(line)
        }
    
        const makeRows = () => {
            const rowsCount = canvasSize.height / rowSize
            for (let i = 0; i < rowsCount; i++) {
                const y = i * rowSize
                drawLine([0, y, canvasSize.width, y])
            }
        }
    
        const makeCols = () => {
            const colsCount = canvasSize.width / colSize
            for (let i = 0; i < colsCount; i++) {
                const x = i * colSize
                drawLine([x, 0, x, canvasSize.height])
            }
        }
    
        makeRows()
        makeCols()
    }
    
    function initFrames(frames) {
        const { width: cellW, height: cellH } = cellDimensions
        frames.forEach(({ top, left, width, height }) => {
            const frame = createFrame(
                { x: left, y: top },
                { width: width, height: height }
            )
            frame.set({
                cellX: width / cellW,
                cellY: height / cellH,
                lockScalingFlip: true,
            })
            drawingCanvas.add(frame)
        })
    }
    
    function createFrame({ x, y }, { width, height } = { width: 0, height: 0 }) {
        const rect = new fabric.Rect({
            top: y,
            left: x,
            width,
            height,
            stroke: 'gray',
            borderStrokeWidth: borderStrokeWidth,
            fill: 'white',
            type: objectTypes.frame,
            strokeUniform: true,
            noScaleCache: false,
            objectCaching: false,
        })
        drawingCanvas.setActiveObject(rect)
        return rect
    }
    <canvas id="canvas"></canvas>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.4.0/fabric.min.js"></script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search