簡體   English   中英

算法 - 給定所有其他矩形的 x 和 y 軸,找到足夠的空間來繪制矩形

[英]Algorithm - locating enough space to draw a rectangle given the x and y axis of all other rectangles

每個矩形都有 x 和 y 坐標、寬度和高度。

屏幕的總寬度為 maxWidth,總高度為 maxHeight。

我有一個包含所有已繪制矩形的數組。

我正在開發一個網絡應用程序,用戶將使用鼠標在屏幕上繪制矩形。 為此,我使用 Javascript 在畫布元素上繪制。

挑戰在於矩形不得在任何給定點相交。

我試圖避免這種情況:

在此處輸入圖片說明

或者這個:

在此處輸入圖片說明

這就是我的目標輸出應該是這樣的:

在此處輸入圖片說明

我基本上需要的是一種算法(最好使用 JavaScript),它可以幫助定位足夠的空間來繪制知道矩形的軸、高度和寬度的矩形。

BM67 盒裝。

這是我用來打包矩形的方法。 我自己編造了創建精靈表。

它是如何工作的。

您維護兩個數組,一個保存可用空間的矩形(空間數組),另一個保存您放置的矩形。

您首先向空間數組添加一個矩形,該矩形覆蓋要填充的整個區域。 這個矩形代表可用空間。

添加矩形以適應時,您可以在可用空間矩形中搜索適合新矩形的矩形。 如果您找不到一個更大或大小合理的矩形作為您要添加的矩形,則沒有空間。

找到放置矩形的位置后,檢查所有可用的空間矩形以查看它們是否與新添加的矩形重疊。 如果有任何重疊,則沿頂部、底部、左側和右側將其切片,從而產生最多 4 個新的空間矩形。 執行此操作時會進行一些優化以減少矩形的數量,但無需優化即可工作。

與其他一些方法相比,它並不那么復雜且相當有效。 當空間開始變低時特別好。

示例

下面是它用隨機矩形填充畫布的演示。 它在動畫循環中顯示該過程,因此速度非常慢。

灰色框是適合的。 紅色顯示當前間隔框。 每個框有 2 個像素的邊距。 有關演示常量,請參閱代碼頂部。

單擊畫布以重新啟動。

 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 = hy; 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 = wx; 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>

更新

改進上述算法。

  • 將算法轉化為對象
  • 通過在縱橫比上加權擬合來找到更合適的墊片來提高速度
  • 添加了placeBox(box)函數,該函數添加一個框而不檢查它是否合適。 它將被放置在它的box.x , box.y坐標

有關用法,請參閱下面的代碼示例。

示例

該示例與上面的示例相同,但在擬合框之前添加了隨機放置的框。

Demo 顯示了盒子和間隔盒,因為它展示了它是如何工作的。 單擊畫布以重新啟動。 按住 [shift] 鍵並單擊畫布重新啟動而不顯示中間結果。

預先放置的盒子是藍色的。 安裝的盒子是灰色的。 間距框是紅色的,會重疊。

當按住 shift 時,擬合過程在第一個不合適的盒子處停止。 紅色框將顯示可用但未使用的區域。

當顯示進度時,該功能將繼續添加框而忽略不合適的框直到超出空間。

 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 nm, 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 ax,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.com/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) => bw - aw)[0]; // const minHeightThatFits = spacerArray.sort((a,b) => ah - bh)[0]; // const minAreaThatFits = spacerArray.sort((a,b) => (aw * ah) - (bw * bh))[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 = hy; 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 = wx; 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>

請嘗試以下操作:

  • 根據每個現有矩形的頂部邊界,從上到下遍歷現有矩形
  • 在按從上到下的順序進行時,維護一個“活動矩形”列表:
    • 將基於其頂部邊界的每個后續矩形添加為活動矩形,以及
    • 根據底部邊界移除活動矩形
    • (您可以通過使用優先隊列有效地做到這一點)
  • 還要跟蹤活動矩形之間的間隙
    • 添加一個活動矩形將結束與它重疊的所有間隙,並且(假設它不與任何現有矩形重疊)在每一側開始一個新的間隙
    • 刪除活動矩形將添加一個新的間隙(不結束任何)
    • 請注意,多個活動間隙可能會相互重疊——您不能指望活動矩形之間只有一個間隙!

檢查您的新矩形(您要放置的矩形)是否與所有間隙相匹配。 每個間隙本身就是一個矩形; 如果新矩形完全適合某個間隙,則可以放置它。

這種方法稱為掃描線算法

您可能需要檢查當前點是否在任何當前矩形的區域內。 您可以使用以下代碼進行測試(從這里竊取)

在您擁有的數組中,按以下方式存儲矩形詳細信息

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

以下將是您測試任何給定點是否在任何給定矩形內的函數。

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

我不確定你將如何實現這一點 -

  • 在鼠標按下時
  • 總是在繪圖期間(這可能是一項工作量太大了)。
  • 鼠標向上(這將是我的偏好。如果測試未通過,您可以取消繪圖,並在畫布中的某處為用戶提供可能的解釋)

希望這會讓你開始。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM