Привязать края объектов друг к другу и предотвратить совпадение

Моя цель - предотвратить перекрытие двух или более прямоугольников внутри моего холста 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

Привязка должна работать для неограниченного количества объектов (не только для двух прямоугольников).

Ответ 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;
    }
});

Работает довольно законно! Ура!

Ответ 2

Это основано на ответе gco, обновленном для работы с FabricJS 1.5.0 со следующими улучшениями:

  • Формы не перекрываются.
  • Привязка более отзывчива.
  • Формы содержатся внутри холста.

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

Ответ 3

Я основал эту скрипту от @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);