在2d区域内为对象找到空间的算法

时间:2014-06-22 21:53:52

标签: javascript jquery algorithm

我正在构建一个使用jQuery的网站,允许用户将小部件添加到页面,拖动它们并调整它们的大小(页面是固定的宽度和无限高度。)我遇到的问题是,在添加时一个新的小部件到页面我必须为它找到一个空闲空间(小部件不能重叠,我想在页面顶部使用空格。)

我一直在研究各种打包算法,但它们似乎都不合适。原因是它们被设计用于将所有对象打包到容器中,这意味着所有先前的矩形都以统一的方式布局。它们通常排列矩形的边缘,以便它们形成行/列,这简化了适合下一行/列的位置。当用户可以随意移动/调整小部件时,这些算法效果不佳。

我认为我有一个部分解决方案,但在这里写了一些伪代码后,我意识到它不会起作用。基于暴力的方法可行,但如果可能的话,我更喜欢更高效的方法。有谁能建议合适的算法?它是我正在寻找的包装算法还是其他更好的工作?

由于

1 个答案:

答案 0 :(得分:1)

好的,我已经找到了解决方案。我不喜欢基于暴力的方法的想法,因为我认为它效率低下,我意识到如果你可以看看哪些现有的小部件在放置小部件的方式,那么你可以跳过大部分网格。

以下是一个示例:(在此示例中,放置的窗口小部件为20x20,页面宽度为100px。)

This diagram is 0.1 scale and got messed up so I've had to add an extra column

*123456789A*
1+---+ +--+1
2|   | |  |2
3|   | +--+3
4|   |     4
5+---+     5
*123456789A*
  1. 我们尝试将小部件放在0x0但是它不适合,因为在该坐标处有一个50x50小部件。
  2. 然后我们将当前扫描的x坐标前进到51并再次检查。
  3. 然后我们在0x61找到一个40x30小部件。
  4. 然后我们将x坐标前进到90但是这并没有为放置小部件留出足够的空间,所以我们增加y坐标并将x重置为0.
  5. 我们从之前的尝试中知道前一行的小部件至少高30px,所以我们将y坐标增加到31。
  6. 我们在0x31遇到相同的50x50小部件。
  7. 所以我们将x增加到51并发现我们可以将小部件放在51x31
  8. 这是javascript:

    function findSpace(width, height) {
        var $ul = $('.snap-layout>ul');
        var widthOfContainer = $ul.width();
        var heightOfContainer = $ul.height();
        var $lis = $ul.children('.setup-widget'); // The li is on the page and we dont want it to collide with itself
    
        for (var y = 0; y < heightOfContainer - height + 1; y++) {
            var heightOfShortestInRow = 1;
            for (var x = 0; x < widthOfContainer - width + 1; x++) {
                console.log(x + '/' + y);
                var pos = { 'left': x, 'top': y };
                var $collider = $(isOverlapping($lis, pos, width, height));
                if ($collider.length == 0) {
                    // Found a space
                    return pos;
                }
    
                var colliderPos = $collider.position();
                // We have collided with something, there is no point testing the points within this widget so lets skip them
                var newX = colliderPos.left + $collider.width() - 1; // -1 to account for the ++ in the for loop
                x = newX > x ? newX : x; // Make sure that we are not some how going backwards and looping forever
    
                var colliderBottom = colliderPos.top + $collider.height();
                if (heightOfShortestInRow == 1 || colliderBottom - y < heightOfShortestInRow) {
                    heightOfShortestInRow = colliderBottom - y; // This isn't actually the height its just the distance from y to the bottom of the widget, y is normally at the top of the widget tho
                }
            }
            y += heightOfShortestInRow - 1;
        }
    
        //TODO: Add the widget to the bottom
    }
    

    这是一个更长,更不优雅的版本,也可以调整容器的高度(我现在只是将它们一起黑了,但稍后会清理并编辑)

    function findSpace(width, height,
            yStart, avoidIds // These are used if the function calls itself - see bellow
        ) {
        var $ul = $('.snap-layout>ul');
        var widthOfContainer = $ul.width();
        var heightOfContainer = $ul.height();
        var $lis = $ul.children('.setup-widget'); // The li is on the page and we dont want it to collide with itself
    
        var bottomOfShortestInRow;
        var idOfShortestInRow;
    
        for (var y = yStart ? yStart : 0; y <= heightOfContainer - height + 1; y++) {
            var heightOfShortestInRow = 1;
            for (var x = 0; x <= widthOfContainer - width + 1; x++) {
                console.log(x + '/' + y);
                var pos = { 'left': x, 'top': y };
                var $collider = $(isOverlapping($lis, pos, width, height));
                if ($collider.length == 0) {
                    // Found a space
                    return pos;
                }
    
                var colliderPos = $collider.position();
                // We have collided with something, there is no point testing the points within this widget so lets skip them
                var newX = colliderPos.left + $collider.width() - 1; // -1 to account for the ++ in the for loop
                x = newX > x ? newX : x; // Make sure that we are not some how going backwards and looping forever
    
                colliderBottom = colliderPos.top + $collider.height();
                if (heightOfShortestInRow == 1 || colliderBottom - y < heightOfShortestInRow) {
                    heightOfShortestInRow = colliderBottom - y; // This isn't actually the height its just the distance from y to the bottom of the widget, y is normally at the top of the widget tho
                    var widgetId = $collider.attr('data-widget-id');
                    if (!avoidIds || !$.inArray(widgetId, avoidIds)) { // If this is true then we are calling ourselves and we used this as the shortest widget before and it didnt work
                        bottomOfShortestInRow = colliderBottom;
                        idOfShortestInRow = widgetId;
                    }
                }
            }
            y += heightOfShortestInRow - 1;
        }
    
        if (!yStart) {
            // No space was found so create some
            var idsToAvoid = [];
    
            for (var attempts = 0; attempts < widthOfContainer; attempts++) { // As a worse case scenario we have lots of 1px wide colliders
                idsToAvoid.push(idOfShortestInRow);
    
                heightOfContainer = $ul.height();
                var maxAvailableRoom = heightOfContainer - bottomOfShortestInRow;
                var extraHeightRequired = height - maxAvailableRoom;
                if (extraHeightRequired < 0) { extraHeightRequired = 0;}
    
                $ul.height(heightOfContainer + extraHeightRequired);
    
                var result = findSpace(width, height, bottomOfShortestInRow, idsToAvoid);
                if (result.top) {
                    // Found a space
                    return result;
                }
    
                // Got a different collider so lets try that next time
                bottomOfShortestInRow = result.bottom;
                idOfShortestInRow = result.id;
    
                if (!bottomOfShortestInRow) {
                    // If this is undefined then its broken (because the widgets are bigger then their contianer which is hardcoded atm and resets on f5)
                    break;
                }
            }
    
            debugger;
            // Something has gone wrong so we just stick it on the bottom left
            $ul.height($ul.height() + height);
            return { 'left': 0, 'top': $ul.height() - height };
    
        } else {
            // The function is calling itself and we shouldnt recurse any further, just return the data required to continue searching
            return { 'bottom': bottomOfShortestInRow, 'id': idOfShortestInRow };
        }
    }
    
    
    function isOverlapping($obsticles, tAxis, width, height) {
        var t_x, t_y;
        if (typeof (width) == 'undefined') {
            // Existing element passed in
            var $target = $(tAxis);
            tAxis = $target.position();
            t_x = [tAxis.left, tAxis.left + $target.outerWidth()];
            t_y = [tAxis.top, tAxis.top + $target.outerHeight()];
        } else {
            // Coordinates and dimensions passed in
            t_x = [tAxis.left, tAxis.left + width];
            t_y = [tAxis.top, tAxis.top + height];
        }
    
        var overlap = false;
    
        $obsticles.each(function () {
            var $this = $(this);
            var thisPos = $this.position();
            var i_x = [thisPos.left, thisPos.left + $this.outerWidth()]
            var i_y = [thisPos.top, thisPos.top + $this.outerHeight()];
    
            if (t_x[0] < i_x[1] && t_x[1] > i_x[0] &&
                 t_y[0] < i_y[1] && t_y[1] > i_y[0]) {
                overlap = this;
                return false;
            }
        });
        return overlap;
    }