Snap edges of objects to each other and prevent overlap

My goal is to prevent overlapping of two or more rectangles inside my FabricJS canvas.

Imagine two rectangles having info on position and size and you can drag and drop any rectangle inside the canvas.

If rectangle A gets close enough to rectangle B, the position of rectangle A should snap to the edge of rectangle B. This should work for any edge of rectangle B. The vertices do not have to match, cause the sizes of the rectangles are variable.

I have a working example for this snapping on one dimension (x-axes).

My best jsfiddle attempt

See jsfiddle.

But I need it to work around the rectangle on both dimensions. I am quite sure, that my code is not well enough to manage this.

Code-snippets which might help:

object.oCoords.tl.x //top-left corner x position. similar goes for top-right (tr), bottom-left (bl), bottom-right (br) and .y for y-position
mouse_pos = canvas.getPointer(e.e);
mouse_pos.x //pointer x.position
mouse_pos.y //pointer y.position
object.intersectsWithObject(targ) // object = dragged rectangle, targ = targeted rectangle

The snapping should work for an unlimited amount of objects (not only for two rectangles).

Answers:

Answer

I solved the problem on my own. See jsfiddle: http://jsfiddle.net/gcollect/FD53A/

This is the code:

this.canvas.on('object:moving', function (e) {
var obj = e.target;
obj.setCoords(); //Sets corner position coordinates based on current angle, width and height
canvas.forEachObject(function (targ) {
    var objects = this.canvas.getObjects(),
        i = objects.length;
    activeObject = canvas.getActiveObject();

    if (targ === activeObject) return;


    if (Math.abs(activeObject.oCoords.tr.x - targ.oCoords.tl.x) < edgedetection) {
        activeObject.left = targ.left - activeObject.currentWidth;
    }
    if (Math.abs(activeObject.oCoords.tl.x - targ.oCoords.tr.x) < edgedetection) {
        activeObject.left = targ.left + targ.currentWidth;
    }
    if (Math.abs(activeObject.oCoords.br.y - targ.oCoords.tr.y) < edgedetection) {
        activeObject.top = targ.top - activeObject.currentHeight;
    }
    if (Math.abs(targ.oCoords.br.y - activeObject.oCoords.tr.y) < edgedetection) {
        activeObject.top = targ.top + targ.currentHeight;
    }
    if (activeObject.intersectsWithObject(targ) && targ.intersectsWithObject(activeObject)) {
        targ.strokeWidth = 10;
        targ.stroke = 'red';
    } else {
        targ.strokeWidth = 0;
        targ.stroke = false;
    }
    if (!activeObject.intersectsWithObject(targ)) {
        activeObject.strokeWidth = 0;
        activeObject.stroke = false;
    }
});

Works pretty legit! Cheers!

Answer

This is based on gco's answer, updated to work with FabricJS 1.5.0, with the following improvements:

  • Shapes don't overlap.
  • Snapping is more responsive.
  • Shapes are contained within the canvas.

JS Fiddle: https://jsfiddle.net/aphillips8/31qbr0vn/1/

var canvas = new fabric.Canvas('canvas'),
canvasWidth = document.getElementById('canvas').width,
canvasHeight = document.getElementById('canvas').height,
counter = 0,
rectLeft = 0,
snap = 20; //Pixels to snap

canvas.selection = false;
plusrect();
plusrect();
plusrect();

function plusrect(top, left, width, height, fill) {
    var rect = new fabric.Rect({
        top: 300,
        name: 'rectangle ' + counter,
        left: 0 + rectLeft,
        width: 100,
        height: 100,
        fill: 'rgba(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ', 0.75)',
        lockRotation: true,
        originX: 'left',
        originY: 'top',
        cornerSize: 15,
        hasRotatingPoint: false,
        perPixelTargetFind: true,
        minScaleLimit: 1,
        maxWidth: canvasWidth,
        maxHeight: canvasHeight
    });

    rect.custom = {};
    rect.custom.counter = counter;

    canvas.add(rect);
    counter++;
    rectLeft += 200;
}

function findNewPos(distX, distY, target, obj) {
    // See whether to focus on X or Y axis
    if(Math.abs(distX) > Math.abs(distY)) {
        if (distX > 0) {
            target.setLeft(obj.getLeft() - target.getWidth());
        } else {
            target.setLeft(obj.getLeft() + obj.getWidth());
        }
    } else {
        if (distY > 0) {
            target.setTop(obj.getTop() - target.getHeight());
        } else {
            target.setTop(obj.getTop() + obj.getHeight());
        }
    }
}

canvas.on('object:moving', function (options) {
    // Sets corner position coordinates based on current angle, width and height
    options.target.setCoords();

    // Don't allow objects off the canvas
    if(options.target.getLeft() < snap) {
        options.target.setLeft(0);
    }

    if(options.target.getTop() < snap) {
        options.target.setTop(0);
    }

    if((options.target.getWidth() + options.target.getLeft()) > (canvasWidth - snap)) {
        options.target.setLeft(canvasWidth - options.target.getWidth());
    }

    if((options.target.getHeight() + options.target.getTop()) > (canvasHeight - snap)) {
        options.target.setTop(canvasHeight - options.target.getHeight());
    }

    // Loop through objects
    canvas.forEachObject(function (obj) {
        if (obj === options.target) return;

        // If objects intersect
        if (options.target.isContainedWithinObject(obj) || options.target.intersectsWithObject(obj) || obj.isContainedWithinObject(options.target)) {

            var distX = ((obj.getLeft() + obj.getWidth()) / 2) - ((options.target.getLeft() + options.target.getWidth()) / 2);
            var distY = ((obj.getTop() + obj.getHeight()) / 2) - ((options.target.getTop() + options.target.getHeight()) / 2);

            // Set new position
            findNewPos(distX, distY, options.target, obj);
        }

        // Snap objects to each other horizontally

        // If bottom points are on same Y axis
        if(Math.abs((options.target.getTop() + options.target.getHeight()) - (obj.getTop() + obj.getHeight())) < snap) {
            // Snap target BL to object BR
            if(Math.abs(options.target.getLeft() - (obj.getLeft() + obj.getWidth())) < snap) {
                options.target.setLeft(obj.getLeft() + obj.getWidth());
                options.target.setTop(obj.getTop() + obj.getHeight() - options.target.getHeight());
            }

            // Snap target BR to object BL
            if(Math.abs((options.target.getLeft() + options.target.getWidth()) - obj.getLeft()) < snap) {
                options.target.setLeft(obj.getLeft() - options.target.getWidth());
                options.target.setTop(obj.getTop() + obj.getHeight() - options.target.getHeight());
            }
        }

        // If top points are on same Y axis
        if(Math.abs(options.target.getTop() - obj.getTop()) < snap) {
            // Snap target TL to object TR
            if(Math.abs(options.target.getLeft() - (obj.getLeft() + obj.getWidth())) < snap) {
                options.target.setLeft(obj.getLeft() + obj.getWidth());
                options.target.setTop(obj.getTop());
            }

            // Snap target TR to object TL
            if(Math.abs((options.target.getLeft() + options.target.getWidth()) - obj.getLeft()) < snap) {
                options.target.setLeft(obj.getLeft() - options.target.getWidth());
                options.target.setTop(obj.getTop());
            }
        }

        // Snap objects to each other vertically

        // If right points are on same X axis
        if(Math.abs((options.target.getLeft() + options.target.getWidth()) - (obj.getLeft() + obj.getWidth())) < snap) {
            // Snap target TR to object BR
            if(Math.abs(options.target.getTop() - (obj.getTop() + obj.getHeight())) < snap) {
                options.target.setLeft(obj.getLeft() + obj.getWidth() - options.target.getWidth());
                options.target.setTop(obj.getTop() + obj.getHeight());
            }

            // Snap target BR to object TR
            if(Math.abs((options.target.getTop() + options.target.getHeight()) - obj.getTop()) < snap) {
                options.target.setLeft(obj.getLeft() + obj.getWidth() - options.target.getWidth());
                options.target.setTop(obj.getTop() - options.target.getHeight());
            }
        }

        // If left points are on same X axis
        if(Math.abs(options.target.getLeft() - obj.getLeft()) < snap) {
            // Snap target TL to object BL
            if(Math.abs(options.target.getTop() - (obj.getTop() + obj.getHeight())) < snap) {
                options.target.setLeft(obj.getLeft());
                options.target.setTop(obj.getTop() + obj.getHeight());
            }

            // Snap target BL to object TL
            if(Math.abs((options.target.getTop() + options.target.getHeight()) - obj.getTop()) < snap) {
                options.target.setLeft(obj.getLeft());
                options.target.setTop(obj.getTop() - options.target.getHeight());
            }
        }
    });

    options.target.setCoords();

    // If objects still overlap

    var outerAreaLeft = null,
    outerAreaTop = null,
    outerAreaRight = null,
    outerAreaBottom = null;

    canvas.forEachObject(function (obj) {
        if (obj === options.target) return;

        if (options.target.isContainedWithinObject(obj) || options.target.intersectsWithObject(obj) || obj.isContainedWithinObject(options.target)) {

            var intersectLeft = null,
            intersectTop = null,
            intersectWidth = null,
            intersectHeight = null,
            intersectSize = null,
            targetLeft = options.target.getLeft(),
            targetRight = targetLeft + options.target.getWidth(),
            targetTop = options.target.getTop(),
            targetBottom = targetTop + options.target.getHeight(),
            objectLeft = obj.getLeft(),
            objectRight = objectLeft + obj.getWidth(),
            objectTop = obj.getTop(),
            objectBottom = objectTop + obj.getHeight();

            // Find intersect information for X axis
            if(targetLeft >= objectLeft && targetLeft <= objectRight) {
                intersectLeft = targetLeft;
                intersectWidth = obj.getWidth() - (intersectLeft - objectLeft);

            } else if(objectLeft >= targetLeft && objectLeft <= targetRight) {
                intersectLeft = objectLeft;
                intersectWidth = options.target.getWidth() - (intersectLeft - targetLeft);
            }

            // Find intersect information for Y axis
            if(targetTop >= objectTop && targetTop <= objectBottom) {
                intersectTop = targetTop;
                intersectHeight = obj.getHeight() - (intersectTop - objectTop);

            } else if(objectTop >= targetTop && objectTop <= targetBottom) {
                intersectTop = objectTop;
                intersectHeight = options.target.getHeight() - (intersectTop - targetTop);
            }

            // Find intersect size (this will be 0 if objects are touching but not overlapping)
            if(intersectWidth > 0 && intersectHeight > 0) {
                intersectSize = intersectWidth * intersectHeight;
            }

            // Set outer snapping area
            if(obj.getLeft() < outerAreaLeft || outerAreaLeft == null) {
                outerAreaLeft = obj.getLeft();
            }

            if(obj.getTop() < outerAreaTop || outerAreaTop == null) {
                outerAreaTop = obj.getTop();
            }

            if((obj.getLeft() + obj.getWidth()) > outerAreaRight || outerAreaRight == null) {
                outerAreaRight = obj.getLeft() + obj.getWidth();
            }

            if((obj.getTop() + obj.getHeight()) > outerAreaBottom || outerAreaBottom == null) {
                outerAreaBottom = obj.getTop() + obj.getHeight();
            }

            // If objects are intersecting, reposition outside all shapes which touch
            if(intersectSize) {
                var distX = (outerAreaRight / 2) - ((options.target.getLeft() + options.target.getWidth()) / 2);
                var distY = (outerAreaBottom / 2) - ((options.target.getTop() + options.target.getHeight()) / 2);

                // Set new position
                findNewPos(distX, distY, options.target, obj);
            }
        }
    });
});
Answer

I based this fiddle off @Anna Phillips' and @gco's examples. It includes:

  • Corner snapping
  • Edge snapping
  • Objects can overlap
  • Objects are fully contained within the canvas
  • Objects cannot have a size larger than the canvas area

Here is the code:

window.canvas = new fabric.Canvas('fabriccanvas');
window.counter = 0;
var newleft = 0,
    edgedetection = 20, //pixels to snap
    canvasWidth = document.getElementById('fabriccanvas').width,
    canvasHeight = document.getElementById('fabriccanvas').height;

canvas.selection = false;
plusrect();
plusrect();
plusrect();

function plusrect(top, left, width, height, fill) {
    window.canvas.add(new fabric.Rect({
        top: 300,
        name: 'rectangle ' + window.counter,
        left: 0 + newleft,
        width: 100,
        height: 100,
        fill: 'rgba(' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ',' + (Math.floor(Math.random() * 256)) + ', 0.75)',
        lockRotation: true,
        originX: 'left',
        originY: 'top',
        cornerSize: 15,
        hasRotatingPoint: false,
        perPixelTargetFind: true,
        minScaleLimit: 1,
        maxHeight: document.getElementById("fabriccanvas").height,
        maxWidth: document.getElementById("fabriccanvas").width,
    }));
    window.counter++;
    newleft += 200;
}
this.canvas.on('object:moving', function (e) {
    var obj = e.target;
    obj.setCoords(); //Sets corner position coordinates based on current angle, width and height

    if(obj.getLeft() < edgedetection) {
        obj.setLeft(0);
    }

    if(obj.getTop() < edgedetection) {
        obj.setTop(0);
    }

    if((obj.getWidth() + obj.getLeft()) > (canvasWidth - edgedetection)) {
        obj.setLeft(canvasWidth - obj.getWidth());
    }

    if((obj.getHeight() + obj.getTop()) > (canvasHeight - edgedetection)) {
        obj.setTop(canvasHeight - obj.getHeight());
    }

    canvas.forEachObject(function (targ) {
        activeObject = canvas.getActiveObject();

        if (targ === activeObject) return;


        if (Math.abs(activeObject.oCoords.tr.x - targ.oCoords.tl.x) < edgedetection) {
            activeObject.left = targ.left - activeObject.currentWidth;
        }
        if (Math.abs(activeObject.oCoords.tl.x - targ.oCoords.tr.x) < edgedetection) {
            activeObject.left = targ.left + targ.currentWidth;
        }
        if (Math.abs(activeObject.oCoords.br.y - targ.oCoords.tr.y) < edgedetection) {
            activeObject.top = targ.top - activeObject.currentHeight;
        }
        if (Math.abs(targ.oCoords.br.y - activeObject.oCoords.tr.y) < edgedetection) {
            activeObject.top = targ.top + targ.currentHeight;
        }
        if (activeObject.intersectsWithObject(targ) && targ.intersectsWithObject(activeObject)) {
            targ.strokeWidth = 10;
            targ.stroke = 'red';
        } else {
            targ.strokeWidth = 0;
            targ.stroke = false;
        }
        if (!activeObject.intersectsWithObject(targ)) {
            activeObject.strokeWidth = 0;
            activeObject.stroke = false;
        }
    });
});

What I'd like to know is if it's possible to extend this to add the following features:

  • Dynamic snapping. Continuing to drag an object after the initial snap will temporarily disable snapping until the object stops moving. For example, if I drag one box next to another, they will snap together once they are within range. However if I continue moving the first box, I can "drop" it in a position where it is within the snapping range but not aligned to the other box.
  • Show guide lines when selected object is within range of another object. Currently we add a border around the target object, but it would be better to show guidelines that extend outwards (possibly to the edge of the canvas) to more easily visualize the bounds of the target object.
  • Parallel snapping. When moving an object that is already snapped to the target object, the selected object should snap to the target object in such a way that the tops, bottoms, or sides of both objects are parallel. For example, assume that the selected square is snapped to the left of the target square and that the top of the selected square is below the top of the target square. Moving the selected square up should cause its top to snap into alignment with the top of the target once in range. The same logic should apply when moving it down, or if the selected object is above/below the target and being moves horizontally.
Answer

I needed snapping of unequal sized areas. jsfiddle

var canvas = new fabric.Canvas('c');
canvas.setDimensions({width:window.innerWidth});

var edge_detection_external = 21;
var corner_detection = 5;

canvas.selection = false;

canvas.on('object:moving', function (e) {

    var obj = e.target;
    obj.setCoords();

    function update_position(obj){
        return function(targ){
            if(targ === obj) return;                   

            // Check overlap case https://www.geeksforgeeks.org/find-two-rectangles-overlap/ 
            if(!(function(targ,obj){                        
                if(obj.aCoords.tl.x > targ.aCoords.br.x || targ.aCoords.tl.x > obj.aCoords.br.x)
                    return false;
                if(targ.aCoords.tl.y > obj.aCoords.br.y || obj.aCoords.tl.y > targ.aCoords.br.y)
                    return false;
                return true;
            })(targ,obj)){
                // is on RIGHT or LEFT? 
                if((obj.top > targ.top && obj.top < targ.top + targ.height)
                    || (targ.top > obj.top && targ.top < obj.top + obj.height)){

                    // Object is to the RIGHT and Edge detection 
                    if(obj.aCoords.tl.x > targ.aCoords.br.x
                        && obj.aCoords.tl.x - targ.aCoords.br.x < edge_detection_external){
                            obj.set({left:targ.aCoords.br.x});

                            // Corner detection
                            obj.setCoords();
                            if(Math.abs(targ.aCoords.tr.y - obj.aCoords.tl.y) < corner_detection)
                                obj.set({top:targ.top});
                            else if(Math.abs(targ.aCoords.br.y - obj.aCoords.bl.y) < corner_detection)
                                obj.set({top:targ.top + targ.height - obj.height});                    
                    }

                    // LEFT
                    if(targ.aCoords.tl.x > obj.aCoords.br.x
                        && targ.aCoords.tl.x - obj.aCoords.br.x  < edge_detection_external){
                            obj.set({left:targ.aCoords.tl.x - obj.width});

                            obj.setCoords();
                            if(Math.abs(targ.aCoords.tl.y - obj.aCoords.tr.y) < corner_detection)
                                obj.set({top:targ.top});
                            else if(Math.abs(targ.aCoords.bl.y - obj.aCoords.br.y) < corner_detection)
                                obj.set({top:targ.top + targ.height - obj.height});  
                    }
                }       

                // is on TOP or BOTTOM?
                if((obj.left > targ.left && obj.left < targ.left + targ.width) 
                    || (targ.left > obj.left && targ.left < obj.left + obj.width)){

                    // TOP 
                    if(targ.aCoords.tl.y > obj.aCoords.br.y  
                        && targ.aCoords.tl.y - obj.aCoords.br.y < edge_detection_external){
                            obj.set({top:targ.aCoords.tl.y - obj.height});

                            obj.setCoords();
                            if(Math.abs(targ.aCoords.tl.x - obj.aCoords.bl.x) < corner_detection)
                                obj.set({left:targ.left});
                            else if(Math.abs(targ.aCoords.tr.x - obj.aCoords.br.x) < corner_detection)
                                obj.set({left:targ.left + targ.width - obj.width});
                    }

                    // BOTTOM
                    if(obj.aCoords.tl.y > targ.aCoords.br.y
                        && obj.aCoords.tl.y - targ.aCoords.br.y < edge_detection_external){
                            obj.set({top:targ.aCoords.br.y});

                            obj.setCoords();
                            if(Math.abs(targ.aCoords.bl.x - obj.aCoords.tl.x) < corner_detection)
                                obj.set({left:targ.left});
                            else if(Math.abs(targ.aCoords.br.x - obj.aCoords.tr.x) < corner_detection)
                                obj.set({left:targ.left + targ.width - obj.width});
                    }
                }

            }

        }
    }

    canvas.getObjects('group').some(update_position(obj));          
});

String.prototype.to_inches = function(){
    return this.split('-').map(function(value,index){
        value = Number(value);
        if(index == 0)
            return value * 12
        else
            return value
    }).reduce(function(total,current){
        return total + current;
    });
}

Array.prototype.to_object_list = function(){
    const preserved = [...this];
    var header = this.splice(0,1)[0];           

   for(var i = 0;i < this.length; i++){
       var obj = {};
       for(var j = 0;j < header.length; j++){
           obj[header[j].toLowerCase()] = this[i][j];
       }
       this[i] = obj;
   }

   return preserved;
}

function draw_areas(){
    var offset = 0;

    return function(area_params,index){
        if(area_params.area.indexOf('>') === -1){

            var area = new fabric.Rect({            
                fill: 'red',
                width:area_params.width.to_inches(),
                height:area_params.length.to_inches(),
            });

            var text = new fabric.Text(area_params.area + '\n' + area_params.width + ' x ' + area_params.length,{
                fontSize:12,
                fill:"white"
            });

            if(text.width - area.width > 0){
                text.set('width',area.width);                    
            }    

            if(text.height - area.height > 0){
                text.set('height',area.height);
            }

            var group_name = 'group_' + area_params.area.split(' ').join('-');
            var group = new fabric.Group([area,text],{
                name: group_name,
                left: 5,
                top: 5*(index++) + offset,                   
            });

            canvas.add(group);

            offset = area_params.length.to_inches() + offset;
            canvas.setDimensions({height:5*(index++) + offset});  
        }                       
    }
}

function handler_get_data(data){
    data = JSON.parse(data);
    data.to_object_list();                                           
    data.forEach(draw_areas());
}        

var d = '[["Area","Width","Length"],["Bedroom 1","19-5.5","14"],["Kitchen","14","16-3"],["Bedroom 2","13-6","12-9"]]';
handler_get_data(d);           


Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.