将对象的边缘彼此对齐并防止重叠

IT技术 javascript html canvas fabricjs
2021-03-09 00:21:46

我的目标是防止 FabricJS 画布内的两个或多个矩形重叠。

想象一下有两个关于位置和大小信息的矩形,您可以在画布内拖放任何矩形。

如果矩形 A 与矩形 B 足够接近,则矩形 A 的位置应该与矩形 B 的边缘对齐。这应该适用于矩形 B 的任何边缘。顶点不必匹配,因为矩形的大小是可变的.

我有一个用于在一维(x 轴)上进行捕捉的工作示例。

我最好的 jsfiddle 尝试

请参阅jsfiddle

但我需要它在两个维度上围绕矩形工作。我很确定,我的代码不足以管理这个。

可能有帮助的代码片段:

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

捕捉应该适用于无限数量的对象(不仅适用于两个矩形)。

4个回答

我自己解决了这个问题。见jsfiddle:http : //jsfiddle.net/gcollect/FD53A/

这是代码:

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;
    }
});

工作非常合法!干杯!

谁会在 2017 年读到这个,因为 Fabric 1.5 你应该使用.getWidth()and.getHeight()而不是.currentWidthand .currentHeight
2021-04-27 00:21:46
也适用于 FabricJS 版本 4.3.1
2021-05-02 00:21:46
@ryan 非常感谢。有一阵子了。在我的用例中,我提出了解决方案,即允许相交对象更好。因为否则用户必须绕过一个物体才能到达所需的目的地。也许它也适合您的用例。由于您可以检查对象何时相交,因此您可以提醒用户或禁用保存。
2021-05-17 00:21:46
我真的很喜欢你在这里所做的。这里的一些小错误是,一旦对象可拖动,它往往会被锁定到网格上。此外,您仍然可以在对象中移动。试图想出一些类似于你所做的只是在关闭时捕捉并防止重叠的东西。
2021-05-21 00:21:46

这是基于 gco 的答案,更新后可与 FabricJS 1.5.0 一起使用,并进行了以下改进:

  • 形状不重叠。
  • 捕捉更灵敏。
  • 形状包含在画布中。

JS小提琴: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);
            }
        }
    });
});
在某些情况下,形状可以重叠。imgur.com/a/42gWy 还不确定为什么,但很好,但可以重现。1) 调整中间形状的大小。2)将第一个形状放在右上角。3) 沿逆时针方向在该对周围拖动第三个形状。不过不错。
2021-04-27 00:21:46

我根据@Anna Phillips 和@gco 的例子制作了这个小提琴这包括:

  • 角落捕捉
  • 边缘捕捉
  • 对象可以重叠
  • 对象完全包含在画布中
  • 对象的大小不能大于画布区域

这是代码:

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;
        }
    });
});

我想知道的是是否可以扩展它以添加以下功能:

  • 动态捕捉在初始捕捉后继续拖动对象将暂时禁用捕捉,直到对象停止移动。例如,如果我将一个盒子拖到另一个盒子旁边,一旦它们在范围内,它们就会贴在一起。但是,如果我继续移动第一个框,我可以将它“放置”在它在捕捉范围内但不与另一个框对齐的位置。
  • 当所选对象在另一个对象的范围内时显示引导线目前我们在目标对象周围添加了一个边框,但最好显示向外延伸(可能到画布边缘)的指南,以便更容易地可视化目标对象的边界。
  • 平行捕捉移动已与目标对象对齐的对象时,所选对象应以两个对象的顶部、底部或侧面平行的方式与目标对象对齐。例如,假设所选方块对齐到目标方块的左侧,并且所选方块的顶部低于目标方块的顶部。向上移动所选方块应使其顶部在范围内与目标顶部对齐。向下移动时,或者所选对象位于目标上方/下方并水平移动时,应应用相同的逻辑。

我需要捕捉大小不等的区域。提琴手

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);