Snap edges of objects to each other and prevent overlap

前端 未结 4 1986
庸人自扰
庸人自扰 2020-12-01 08:48

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 d

相关标签:
4条回答
  • 2020-12-01 09:00

    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!

    0 讨论(0)
  • 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);
                }
            }
        });
    });
    
    0 讨论(0)
  • 2020-12-01 09:17

    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);           
    
    
    
    0 讨论(0)
  • 2020-12-01 09:18

    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.
    0 讨论(0)
提交回复
热议问题