将对象的边缘相互对齐并防止重叠

Posted

技术标签:

【中文标题】将对象的边缘相互对齐并防止重叠【英文标题】:Snap edges of objects to each other and prevent overlap 【发布时间】:2014-04-30 18:40:36 【问题描述】:

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

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

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

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

我最好的 jsfiddle 尝试

见jsfiddle。

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

可能有帮助的代码-sn-ps:

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

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

【问题讨论】:

【参考方案1】:

我自己解决了这个问题。 见 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;
    
);

工作非常合法!干杯!

【讨论】:

我真的很喜欢你在这里所做的。这里的一些小错误是,一旦对象可拖动,它往往会被锁定在网格上。此外,您仍然可以在对象中移动。试图想出与您所做的类似的事情,只是在关闭时才捕捉并防止重叠。 @ryan 非常感谢。有一阵子了。在我的用例中,我提出了解决方案,最好允许相交对象。因为否则用户必须绕过一个对象才能到达所需的目的地。也许它也适合您的用例。由于您可以检查对象何时相交,因此您可以提醒用户或禁用保存。 谁会在 2017 年阅读这篇文章,从 Fabric 1.5 开始,你应该使用 .getWidth().getHeight() 而不是 .currentWidth.currentHeight 也适用于 FabricJS 版本 4.3.1【参考方案2】:

这是基于 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) 逆时针将第三个形状拖到该对周围和上方。不过不错。【参考方案3】:

我基于this fiddle @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;
        
    );
);

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

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

【讨论】:

【参考方案4】:

我需要捕捉大小不等的区域。 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);           


【讨论】:

以上是关于将对象的边缘相互对齐并防止重叠的主要内容,如果未能解决你的问题,请参考以下文章

在 JavaScript 中生成随机元素位置并防止重叠

以编程方式添加约束以防止按钮重叠 SWIFT 4

防止 UIView 重叠

防止视图重叠

防止力有向图中的节点重叠

visjs笔记-将edges(边)分开(防止重叠)