Domanda

I'm drawing n rectangles on a canvas. The rectangles are draggable and scalable. I want to prevent them from overlapping or intersecting. The best case is, if they just snap to each other.

I figured out to check the intersection. In my example I set the opacity of touching objects to 0.1.

Coincidentally in my attempt to solve this problem, my objects cant be released when they touch another object. See http://jsfiddle.net/gcollect/jZw7P/ It's because of line 91, where the alert is not executed. alert(math.abs(distx));

Actually its a kind of solution, but definitely not an elegant one.

Any ideas?

È stato utile?

Soluzione 3

For those who are still interested in the solution: I solved it here: https://stackoverflow.com/a/22649022/3207478 See jsfiddle: http://jsfiddle.net/gcollect/FD53A/

Working with

.oCoords.tl .tr .bl. and .br solved it.

Altri suggerimenti

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

these solutions are pretty good, but non of them working with rotated objects. I came up with this. It's preventing intersecting of objects..Even rotated.

I made up this fiddle. Check it out.

[Fiddle](http://jsfiddle.net/m0jjc23v/9)

Explanation:

Workaround is that you have to save last non-intersectiong top & left position while moving object. When object is intersecting or isContained with another object, just use previously saved top & left position and reposition object. There's a lot of possible improvements. Feel free to use this code and make improvements.

Code:

//handle moving object
this.canvas.on('object:moving', function(event) {
        var obj = event.target;
        intersectingCheck(obj);
});

function intersectingCheck(activeObject) {
    activeObject.setCoords();
    if(typeof activeObject.refreshLast != 'boolean') {
        activeObject.refreshLast = true
    };

    //loop canvas objects
    activeObject.canvas.forEachObject(function (targ) {
        if (targ === activeObject) return; //bypass self

        //check intersections with every object in canvas
        if (activeObject.intersectsWithObject(targ) 
            || activeObject.isContainedWithinObject(targ) 
            || targ.isContainedWithinObject(activeObject)) {
                //objects are intersecting - deny saving last non-intersection position and break loop
                if(typeof activeObject.lastLeft == 'number') {
                    activeObject.left = activeObject.lastLeft;
                    activeObject.top = activeObject.lastTop;
                    activeObject.refreshLast = false;
                    return;
                }
       }
       else {
           activeObject.refreshLast = true;
       }
   });

   if(activeObject.refreshLast) {
       //save last non-intersecting position if possible
       activeObject.lastLeft = activeObject.left
       activeObject.lastTop = activeObject.top;
   }
}

I figured it out how to prevent the collision on the x-axis. By adding these lines:

canvas.forEachObject(function (obj) {
                if (obj === options.target) return;
                if (options.target.isContainedWithinObject(obj)||options.target.intersectsWithObject(obj)||obj.isContainedWithinObject(options.target)) {
                    var distx = ((obj.left + obj.width)/2) - ((options.target.left + options.target.width)/2);
                    var disty = ((obj.top + obj.height)/2) - ((options.target.top + options.target.height)/2);                  

                    if (distx > 0){
                        options.target.left = obj.left - options.target.width;
                    } else {
                        options.target.left = obj.left + obj.width;
                    }

See JSFiddle. Actually it's pretty cool and the objects snap to each other on the x-axis. Troublemaker is now the y-axis. An approach from top or below will move the object to the left or right edge... Will work on this issue.

I've been contemplating a similar problem and I + the last fiddle here from @kangax. One could even use this type of mechanism to do collision detection for paths themselves if one has a path collision detection algorithm like the polygon intersection code found here: http://www.kevlindev.com/geometry/2D/intersections/index.htm.

But what I don't like about this 'sticky' solution is that to me the object should actually slide along the surface for my application instead of forcing the user to 'unstick' the object. For this kind of effect, I realized that a 2D physics engine would probably do a great job of enabling this type of functionality, and some of the example uses of http://brm.io/matter-js/ demonstrate solving the problem and the positioning and rotation should surely be able to map into fabric.js. However, if fixed rotation is required, it looks like the probability of success would be reduced.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top