最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Algorithm - locating enough space to draw a rectangle given the x and y axis of all other rectangles - Stack Overfl

programmeradmin5浏览0评论

Every rectangle has x and y coordinates, width and height.

The total width of the screen is maxWidth and total height is maxHeight.

I have an array containing all the already drawn rectangles.

I am working on an web App where users will be drawing rectangles on the screen using their mouse. For that I am using Javascript to draw on the canvas element.

The challenge is that the rectangles must not intersect at any given point.

I am trying to avoid this kind of case:

or this:

This is how the output I am aiming for should look like:

What I basically need is an Algorithm (preferably in JavaScript) that can help locating enough space to draw a rectangle knowing its axis, height and width.

Every rectangle has x and y coordinates, width and height.

The total width of the screen is maxWidth and total height is maxHeight.

I have an array containing all the already drawn rectangles.

I am working on an web App where users will be drawing rectangles on the screen using their mouse. For that I am using Javascript to draw on the canvas element.

The challenge is that the rectangles must not intersect at any given point.

I am trying to avoid this kind of case:

or this:

This is how the output I am aiming for should look like:

What I basically need is an Algorithm (preferably in JavaScript) that can help locating enough space to draw a rectangle knowing its axis, height and width.

Share Improve this question edited Jun 4, 2020 at 9:40 Hamzaouiii asked Aug 14, 2017 at 19:00 HamzaouiiiHamzaouiii 4583 silver badges15 bronze badges 1
  • A 2D bin-packer is what you need (demo). – user1693593 Commented Aug 15, 2017 at 5:35
Add a ment  | 

3 Answers 3

Reset to default 14

BM67 Box packing.

This is a method I use to pack rectangles. I made it up myself to create sprite sheets.

How it works.

You maintain two arrays, one holds rectangles of available spaces (space array), and the other rectangles you have placed.

You start by adding to the space array a rectangle that covers the whole area to be filled. This rectangle represents available space.

When you add a rectangle to fit you search the available space rectangles for a rectangle that will fit the new rectangle. If you can not find a rectangle that is bigger or sane size as the one you want to add there is no room.

Once you have found a place to put the rectangle, check all the available space rectangles to see if any of them overlap the new added rectangle. If any overlap you slice it up along the top, bottom, left and right, resulting in up to 4 new space rectangles. There are some optimisation when you do this to keep the number of rectangles down but it will work without the optimisations.

It's not that plicated and reasonably efficient pared to some other methods. It is particularly good when the space starts to run low.

Example

Below is a demo of it filling the canvas with random rectangles. It's on a animation loop to show the process, so is very much slowed down.

Gray boxes are the ones to fit. Red show the current spacer boxes. Each box has a 2 pixel margin. See top of code for demo constants.

Click the canvas to restart.

const boxes = [];  // added boxes
const spaceBoxes = []; // free space boxes
const space = 2;  // space between boxes
const minW = 4;   // min width and height of boxes
const minH = 4;
const maxS = 50;   // max width and height
// Demo only
const addCount = 2;  // number to add per render cycle

const ctx = canvas.getContext("2d");
canvas.width = canvas.height = 1024;

// create a random integer
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
// itterates an array
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };



// resets boxes
function start(){
    boxes.length = 0;
    spaceBoxes.length = 0;
    spaceBoxes.push({
        x : space, y : space,
        w : canvas.width - space * 2,
        h : canvas.height - space * 2,
    });
}
// creates a random box without a position
function createBox(){
    return {  w : randI(minW,maxS),  h : randI(minH,maxS) }
}

// cuts box to make space for cutter (cutter is a box)
function cutBox(box,cutter){
    var b = [];
    // cut left
    if(cutter.x - box.x - space > minW){
        b.push({
            x : box.x,  y : box.y,
            w : cutter.x - box.x - space, 
            h : box.h,
        })
    }
    // cut top
    if(cutter.y - box.y - space > minH){
        b.push({
            x : box.x,  y : box.y,
            w : box.w, 
            h : cutter.y - box.y - space, 
        })
    }
    // cut right
    if((box.x + box.w) - (cutter.x + cutter.w + space) > space + minW){
        b.push({
            x : cutter.x + cutter.w + space, 
            y : box.y,
            w : (box.x + box.w) - (cutter.x + cutter.w + space), 
            h : box.h,
        })
    }
    // cut bottom
    if((box.y + box.h) - (cutter.y + cutter.h + space) > space + minH){
        b.push({
            x : box.x, 
            y : cutter.y + cutter.h + space, 
            w : box.w, 
            h : (box.y + box.h) - (cutter.y + cutter.h + space), 
        })    
    }
    return b;
}
// get the index of the spacer box that is closest in size to box
function findBestFitBox(box){
    var smallest = Infinity;
    var boxFound;
    eachOf(spaceBoxes,(sbox,index)=>{
        if(sbox.w >= box.w && sbox.h >= box.h){
            var area = sbox.w * sbox.h;
            if(area < smallest){
                smallest = area;
                boxFound = index;
            }            
        }
    })
    return boxFound;
}
// returns an array of boxes that are touching box
// removes the boxes from the spacer array
function getTouching(box){
    var b = [];
    for(var i = 0; i < spaceBoxes.length; i++){
        var sbox = spaceBoxes[i];
        if(!(sbox.x > box.x + box.w + space || sbox.x + sbox.w < box.x - space ||
           sbox.y > box.y + box.h + space || sbox.y + sbox.h < box.y - space )){
            b.push(spaceBoxes.splice(i--,1)[0])       
        }
    }
    return b;    
}

// Adds a space box to the spacer array.
// Check if it is insid, too small, or can be joined to another befor adding.
// will not add if not needed.
function addSpacerBox(box){
    var dontAdd = false;
    // is to small?
    if(box.w < minW || box.h < minH){ return }
    // is same or inside another
    eachOf(spaceBoxes,sbox=>{
        if(box.x >= sbox.x && box.x + box.w <= sbox.x + sbox.w &&
            box.y >= sbox.y && box.y + box.h <= sbox.y + sbox.h ){
            dontAdd = true;
            return true;
        }
    })
    if(!dontAdd){
        var join = false;
        // check if it can be joinded with another
        eachOf(spaceBoxes,sbox=>{
            if(box.x === sbox.x && box.w === sbox.w && 
                !(box.y > sbox.y + sbox.h || box.y + box.h < sbox.y)){
                join = true;
                var y = Math.min(sbox.y,box.y);
                var h = Math.max(sbox.y + sbox.h,box.y + box.h);
                sbox.y = y;
                sbox.h = h-y;
                return true;
            }
            if(box.y === sbox.y && box.h === sbox.h && 
                !(box.x > sbox.x + sbox.w || box.x + box.w < sbox.x)){
                join = true;
                var x = Math.min(sbox.x,box.x);
                var w = Math.max(sbox.x + sbox.w,box.x + box.w);
                sbox.x = x;
                sbox.w = w-x;
                return true;                
            }            
        })
        if(!join){  spaceBoxes.push(box) }// add to spacer array
    }
}
// Adds a box by finding a space to fit.
function locateSpace(box){
    if(boxes.length === 0){ // first box can go in top left
        box.x = space;
        box.y = space;
        boxes.push(box);
        var sb = spaceBoxes.pop();
        spaceBoxes.push(...cutBox(sb,box));        
    }else{
        var bf = findBestFitBox(box); // get the best fit space
        if(bf !== undefined){
            var sb = spaceBoxes.splice(bf,1)[0]; // remove the best fit spacer
            box.x = sb.x; // use it to position the box
            box.y = sb.y; 
            spaceBoxes.push(...cutBox(sb,box)); // slice the spacer box and add slices back to spacer array
            boxes.push(box); // add the box
            var tb = getTouching(box);  // find all touching spacer boxes
            while(tb.length > 0){ // and slice them if needed
                eachOf(cutBox(tb.pop(),box),b => addSpacerBox(b));
            }  
        }
    }
}

// draws a box array
function drawBoxes(list,col,col1){
    eachOf(list,box=>{
        if(col1){
            ctx.fillStyle = col1;
            ctx.fillRect(box.x+ 1,box.y+1,box.w-2,box.h - 2);
        }    
        ctx.fillStyle = col;        
        ctx.fillRect(box.x,box.y,box.w,1);
        ctx.fillRect(box.x,box.y,1,box.h);
        ctx.fillRect(box.x+box.w-1,box.y,1,box.h);
        ctx.fillRect(box.x,box.y+ box.h-1,box.w,1);
    })
}

// Show the process in action
ctx.clearRect(0,0,canvas.width,canvas.height);
var count = 0;
var handle = setTimeout(doIt,10);
start()
function doIt(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    for(var i = 0; i < addCount; i++){
        var box = createBox();
        locateSpace(box);
    }
    drawBoxes(boxes,"black","#CCC");
    drawBoxes(spaceBoxes,"red");
    if(count < 1214 && spaceBoxes.length > 0){
        count += 1;
        handle = setTimeout(doIt,10);
    }
}
canvas.onclick = function(){
  clearTimeout(handle);
  start();
  handle = setTimeout(doIt,10);
  count = 0;
}
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>

Update

Improving on the above algorithm.

  • Turned algorithm into an object
  • Improved speed by finding better fitting spacer via weighting the fit on the aspect ratio
  • Added placeBox(box) function that adds a box without checking if it fits. It will be placed at its box.x, box.y coordinates

See code example below on usage.

Example

The example is the same as the above example but have added randomly place boxes before fitting boxes.

Demo displays the boxes and spacer boxes as it goes to show how it works. Click the canvas to restart. Hold [shift] key and click canvas to restart without displaying intermediate results.

Pre placed boxes are blue. Fitted boxes are gray. Spacing boxes are red and will overlap.

When holding shift the fitting process is stopped at the first box tat does not fit. The red boxes will show area that are available but unused.

When showing progress the function will keep adding boxes ignoring non fitting boxes until out of room.

const minW = 4;   // min width and height of boxes
const minH = 4;
const maxS = 50;   // max width and height
const space = 2;
const numberBoxesToPlace = 20; // number of boxes to place befor fitting
const fixedBoxColor = "blue";

// Demo only
const addCount = 2;  // number to add per render cycle
const ctx = canvas.getContext("2d");
canvas.width = canvas.height = 1024;

// create a random integer randI(n) return random val 0-n randI(n,m) returns random int n-m, and iterator that can break
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };



// creates a random box. If place is true the box also gets a x,y position and is flaged as fixed
function createBox(place){
    if(place){
        const box = {
            w : randI(minW*4,maxS*4),
            h : randI(minH*4,maxS*4),
            fixed : true,
        }
        box.x = randI(space, canvas.width - box.w - space * 2);
        box.y = randI(space, canvas.height - box.h - space * 2);
        return box;
    }
    return {
        w : randI(minW,maxS),
        h : randI(minH,maxS),
    }
}

//======================================================================
// BoxArea object using BM67 box packing algorithum
// https://stackoverflow./questions/45681299/algorithm-locating-enough-space-to-draw-a-rectangle-given-the-x-and-y-axis-of
// Please leave this and the above two lines with any copies of this code.
//======================================================================
//
// usage
//  var area = new BoxArea({
//      x: ?,  // x,y,width height of area
//      y: ?,
//      width: ?,
//      height : ?.
//      space : ?, // optional default = 1 sets the spacing between boxes
//      minW : ?, // optional default = 0 sets the in width of expected box. Note this is for optimisation you can add smaller but it may fail
//      minH : ?, // optional default = 0 sets the in height of expected box. Note this is for optimisation you can add smaller but it may fail
//  });
//
//  Add a box at a location. Not checked for fit or overlap
//  area.placeBox({x : 100, y : 100, w ; 100, h :100});
//
//  Tries to fit a box. If the box does not fit returns false
//  if(area.fitBox({x : 100, y : 100, w ; 100, h :100})){ // box added
//
//  Resets the BoxArea removing all boxes
//  area.reset()
//
//  To check if the area is full
//  area.isFull();  // returns true if there is no room of any more boxes.
//
//  You can check if a box can fit at a specific location with
//  area.isBoxTouching({x : 100, y : 100, w ; 100, h :100}, area.boxes)){ // box is touching another box
//
//  To get a list of spacer boxes. Note this is a copy of the array, changing it will not effect the functionality of BoxArea
//  const spacerArray = area.getSpacers();  
//  
//  Use it to get the max min box size that will fit
//
//  const maxWidthThatFits = spacerArray.sort((a,b) => b.w - a.w)[0];
//  const minHeightThatFits = spacerArray.sort((a,b) => a.h - b.h)[0];
//  const minAreaThatFits = spacerArray.sort((a,b) => (a.w * a.h) - (b.w * b.h))[0];
//
//  The following properties are available
//  area.boxes  // an array of boxes that have been added
//  x,y,width,height  // the area that boxes are fitted to  
const BoxArea = (()=>{
    const defaultSettings = {
        minW : 0, // min expected size of a box
        minH : 0,
        space : 1, // spacing between boxes
    };
    const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };

    function BoxArea(settings){
        settings = Object.assign({},defaultSettings,settings);
            
        this.width = settings.width;
        this.height = settings.height;
        this.x = settings.x;
        this.y = settings.y;
        
        const space = settings.space;
        const minW = settings.minW;
        const minH = settings.minH;
        
        const boxes = [];  // added boxes
        const spaceBoxes = [];
        this.boxes = boxes;
        
        
        
        // cuts box to make space for cutter (cutter is a box)
        function cutBox(box,cutter){
            var b = [];
            // cut left
            if(cutter.x - box.x - space >= minW){
                b.push({
                    x : box.x,  y : box.y, h : box.h,
                    w : cutter.x - box.x - space, 
                });
            }
            // cut top
            if(cutter.y - box.y - space >= minH){
                b.push({
                    x : box.x,  y : box.y, w : box.w, 
                    h : cutter.y - box.y - space, 
                });
            }
            // cut right
            if((box.x + box.w) - (cutter.x + cutter.w + space) >= space + minW){
                b.push({
                    y : box.y, h : box.h,
                    x : cutter.x + cutter.w + space, 
                    w : (box.x + box.w) - (cutter.x + cutter.w + space),                    
                });
            }
            // cut bottom
            if((box.y + box.h) - (cutter.y + cutter.h + space) >= space + minH){
                b.push({
                    w : box.w, x : box.x, 
                    y : cutter.y + cutter.h + space, 
                    h : (box.y + box.h) - (cutter.y + cutter.h + space), 
                });
            }
            return b;
        }
        // get the index of the spacer box that is closest in size and aspect to box
        function findBestFitBox(box, array = spaceBoxes){
            var smallest = Infinity;
            var boxFound;
            var aspect = box.w / box.h;
            eachOf(array, (sbox, index) => {
                if(sbox.w >= box.w && sbox.h >= box.h){
                    var area = ( sbox.w * sbox.h) * (1 + Math.abs(aspect - (sbox.w / sbox.h)));
                    if(area < smallest){
                        smallest = area;
                        boxFound = index;
                    }
                }
            })
            return boxFound;            
        }
        // Exposed helper function
        // returns true if box is touching any boxes in array
        // else return false
        this.isBoxTouching = function(box, array = []){
            for(var i = 0; i < array.length; i++){
                var sbox = array[i];
                if(!(sbox.x > box.x + box.w + space || sbox.x + sbox.w < box.x - space ||
                   sbox.y > box.y + box.h + space || sbox.y + sbox.h < box.y - space )){
                    return true; 
                }
            }
            return false;    
        }
        // returns an array of boxes that are touching box
        // removes the boxes from the array
        function getTouching(box, array = spaceBoxes){
            var boxes = [];
            for(var i = 0; i < array.length; i++){
                var sbox = array[i];
                if(!(sbox.x > box.x + box.w + space || sbox.x + sbox.w < box.x - space ||
                   sbox.y > box.y + box.h + space || sbox.y + sbox.h < box.y - space )){
                    boxes.push(array.splice(i--,1)[0])       
                }
            }
            return boxes;    
        }

        // Adds a space box to the spacer array.
        // Check if it is inside, too small, or can be joined to another befor adding.
        // will not add if not needed.
        function addSpacerBox(box, array = spaceBoxes){
            var dontAdd = false;
            // is box to0 small?
            if(box.w < minW || box.h < minH){ return }
            // is box same or inside another box
            eachOf(array, sbox => {
                if(box.x >= sbox.x && box.x + box.w <= sbox.x + sbox.w &&
                    box.y >= sbox.y && box.y + box.h <= sbox.y + sbox.h ){
                    dontAdd = true;
                    return true;   // exits eachOf (like a break statement);             
                }
            })
            if(!dontAdd){
                var join = false;
                // check if it can be joined with another
                eachOf(array, sbox => {
                    if(box.x === sbox.x && box.w === sbox.w && 
                        !(box.y > sbox.y + sbox.h || box.y + box.h < sbox.y)){
                        join = true;
                        var y = Math.min(sbox.y,box.y);
                        var h = Math.max(sbox.y + sbox.h,box.y + box.h);
                        sbox.y = y;
                        sbox.h = h-y;
                        return true;   // exits eachOf (like a break statement);             
                    }
                    if(box.y === sbox.y && box.h === sbox.h && 
                        !(box.x > sbox.x + sbox.w || box.x + box.w < sbox.x)){
                        join = true;
                        var x = Math.min(sbox.x,box.x);
                        var w = Math.max(sbox.x + sbox.w,box.x + box.w);
                        sbox.x = x;
                        sbox.w = w-x;
                        return true;   // exits eachOf (like a break statement);             
                    }            
                })
                if(!join){ array.push(box) }// add to spacer array
            }
        }

        // Adds a box by finding a space to fit.
        // returns true if the box has been added
        // returns false if there was no room.
        this.fitBox = function(box){
            if(boxes.length === 0){ // first box can go in top left
                box.x = space;
                box.y = space;
                boxes.push(box);
                var sb = spaceBoxes.pop();
                spaceBoxes.push(...cutBox(sb,box));        
            }else{
                var bf = findBestFitBox(box); // get the best fit space
                if(bf !== undefined){
                    var sb = spaceBoxes.splice(bf,1)[0]; // remove the best fit spacer
                    box.x = sb.x; // use it to position the box
                    box.y = sb.y; 
                    spaceBoxes.push(...cutBox(sb,box)); // slice the spacer box and add slices back to spacer array
                    boxes.push(box);            // add the box
                    var tb = getTouching(box);  // find all touching spacer boxes
                    while(tb.length > 0){       // and slice them if needed
                        eachOf(cutBox(tb.pop(),box),b => addSpacerBox(b));
                    }  
                } else {
                    return false;
                }
            }
            return true;
        }
        // Adds a box at location box.x, box.y
        // does not check if it can fit or for overlap.
        this.placeBox = function(box){
            boxes.push(box); // add the box
            var tb = getTouching(box);  // find all touching spacer boxes
            while(tb.length > 0){       // and slice them if needed
                eachOf(cutBox(tb.pop(),box),b => addSpacerBox(b));
            }  
        }
        // returns a copy of the spacer array
        this.getSpacers = function(){
            return [...spaceBoxes];
        }
        this.isFull = function(){
            return spaceBoxes.length === 0;
        }
        // resets boxes
        this.reset = function(){
            boxes.length = 0;
            spaceBoxes.length = 0;
            spaceBoxes.push({
                x : this.x + space, y : this.y + space,
                w : this.width - space * 2,
                h : this.height - space * 2,
            });
        } 
        this.reset();
    }           
    return BoxArea;
})();


// draws a box array
function drawBoxes(list,col,col1){
    eachOf(list,box=>{
        if(col1){
            ctx.fillStyle = box.fixed ? fixedBoxColor : col1;
            ctx.fillRect(box.x+ 1,box.y+1,box.w-2,box.h - 2);
        }    
        ctx.fillStyle = col;        
        ctx.fillRect(box.x,box.y,box.w,1);
        ctx.fillRect(box.x,box.y,1,box.h);
        ctx.fillRect(box.x+box.w-1,box.y,1,box.h);
        ctx.fillRect(box.x,box.y+ box.h-1,box.w,1);
    })
}

// Show the process in action
ctx.clearRect(0,0,canvas.width,canvas.height);
var count = 0;
var failedCount = 0;
var timeoutHandle;
var addQuick = false;

// create a new box area
const area = new BoxArea({x : 0, y : 0, width : canvas.width, height : canvas.height, space : space, minW : minW, minH : minH});

// fit boxes until a box cant fit or count over count limit
function doIt(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    if(addQuick){
        while(area.fitBox(createBox()));
        count = 2000;
    
    }else{
        for(var i = 0; i < addCount; i++){
            if(!area.fitBox(createBox())){
                failedCount += 1;
                break;
            }
        }
    }
    drawBoxes(area.boxes,"black","#CCC");
    drawBoxes(area.getSpacers(),"red");
    if(count < 5214 && !area.isFull()){
        count += 1;
        timeoutHandle = setTimeout(doIt,10);
    }
}

// resets the area places some fixed boxes and starts the fitting cycle.
function start(event){
    clearTimeout(timeoutHandle);
    area.reset();
    failedCount = 0;
    for(var i = 0; i < numberBoxesToPlace; i++){
        var box = createBox(true); // create a fixed box
        if(!area.isBoxTouching(box,area.boxes)){
            area.placeBox(box);
        }
    }
    if(event && event.shiftKey){
        addQuick = true;
    }else{
        addQuick = false;
    }
    timeoutHandle = setTimeout(doIt,10);
    count = 0;
}
canvas.onclick = start;
start();
body {font-family : arial;}
canvas { border : 2px solid black; }
.info {position: absolute; z-index : 200; top : 16px; left : 16px; background : rgba(255,255,255,0.75);}
<div class="info">Click canvas to reset. Shift click to add without showing progress.</div>
<canvas id="canvas"></canvas>

Try the following:

  • iterate through the existing rectangles from top to bottom, based on the top boundary of each existing rectangle
  • while proceeding in top-to-bottom order, maintain a list of "active rectangles":
    • adding each succeeding rectangle based on its top boundary as an active rectangle, and
    • removing active rectangles based on their bottom boundary
    • (you can do this efficiently by using a priority queue)
  • also keep track of the gaps between active rectangles:
    • adding an active rectangle will end all gaps that overlap it, and (assuming it doesn't overlap any existing rectangles) start a new gap on each side
    • removing an active rectangle will add a new gap (without ending any)
    • note that multiple active gaps may overlap each other -- you can't count on having exactly one gap between active rectangles!

Check your new rectangle (the one you want to place) against all gaps. Each gap is itself a rectangle; you can place your new rectangle if it fits entirely inside some gap.

This kind of method is called a sweep-line algorithm.

You may have to check whether your current point is inside the area of any of the current rectangles. You can use the following code to test that (stolen from here)

In the array you are having, store the rectangle details in the following way

var point = {x: 1, y: 2};
var rectangle = {x1: 0, x2: 10, y1: 1, y2: 7};

Following will be your function to test whether any given point is inside any given rectangle.

function isPointInsideRectangle(p, r) {
    return 
        p.x > r.x1 && 
        p.x < r.x2 && 
        p.y > r.y1 && 
        p.y < r.y2;
}

I am not sure how you are going to implement this -

  • On mouse down
  • Always during drawing (This may be too much of a work).
  • On mouse up (this will be my preference. You can cancel the drawing if the test did not pass, with possible explanation for the user somewhere in the canvas)

Hope this will get you starting.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论