blender/release/scripts/bpymodules/boxpack2d.py
Campbell Barton d56de7edb7 Mostly fixed archimap issue where "fill holes" - island filling would hang ths script.
Some optimizations and Ctrl will escape the script properly if you want to skip merging the islands.

Thanks to Melchior FRANZ for the report and debug blend
2006-07-29 12:21:38 +00:00

493 lines
14 KiB
Python

'''
# 2D Box packing function used by archimap
# packs any list of 2d boxes into a square and returns a list of packed boxes.
# Example of usage.
import boxpack2d
# Build boxe list.
# the unique ID is not used.
# just the width and height.
boxes2Pack = []
anyUniqueID = 0; w = 2.2; h = 3.8
boxes2Pack.append([anyUniqueID, w,h])
anyUniqueID = 1; w = 4.1; h = 1.2
boxes2Pack.append([anyUniqueID, w,h])
anyUniqueID = 2; w = 5.2; h = 9.2
boxes2Pack.append([anyUniqueID, w,h])
anyUniqueID = 3; w = 8.3; h = 7.3
boxes2Pack.append([anyUniqueID, w,h])
anyUniqueID = 4; w = 1.1; h = 5.1
boxes2Pack.append([anyUniqueID, w,h])
anyUniqueID = 5; w = 2.9; h = 8.1
boxes2Pack.append([anyUniqueID, w,h])
anyUniqueID = 6; w = 4.2; h = 6.2
boxes2Pack.append([anyUniqueID, w,h])
# packedLs is a list of [(anyUniqueID, left, bottom, width, height)...]
packWidth, packHeight, packedLs = boxpack2d.boxPackIter(boxes2Pack)
'''
from Blender import NMesh, Window, Object, Scene
'''
def debug_(x,y,z):
ob = Object.New("Empty")
ob.loc= x,y,z
Scene.GetCurrent().link(ob)
'''
# a box packing vert
class vt:
def __init__(self, x,y):
self.x, self.y = x, y
self.free = 15
# Set flags so cant test bottom left of 0/0
#~ BLF = 1; TRF = 2; TLF = 4; BRF = 8
#self.users = [] # A list of boxes.
# Rather then users, store Quadrents
self.blb = self.tlb = self.brb = self.trb = None
# A hack to remember the box() that last intersectec this vert
self.intersectCache = ([], [], [], [])
class vertList:
def __init__(self, verts=[]):
self.verts = verts
def sortCorner(self,w,h):
'''
Sorts closest first. - uses the box w/h as a bias,
this makes it so its less likely to have lots of poking out bits
that use too much
Lambada based sort
'''
self.verts.sort(lambda A, B: cmp(max(A.x+w, A.y+h) , max(B.x+w, B.y+h))) # Reverse area sort
class box:
def __init__(self, width, height, id=None):
self.id= id
self.area = width * height # real area
self.farea = width + height # fake area
#self.farea = float(min(width, height)) / float(max(width, height)) # fake area
self.width = width
self.height = height
# Append 4 new verts
# (BL,TR,TL,BR) / 0,1,2,3
self.v=v= [vt(0,0), vt(width,height), vt(0,height), vt(width,0)]
# Set the interior quadrents as used.
v[0].free &= ~TRF
v[1].free &= ~BLF
v[2].free &= ~BRF
v[3].free &= ~TLF
#for v in self.v:
# v.users.append(self)
v[0].trb = self
v[1].blb = self
v[2].brb = self
v[3].tlb = self
def updateV34(self):
'''
Updates verts 3 & 4 from 1 and 2
since 3 and 4 are only there foill need is resizing/ rotating of patterns on the fly while I painr new box placement
but may be merged later with other verts
'''
self.v[TL].x = self.v[BL].x
self.v[TL].y = self.v[TR].y
self.v[BR].x = self.v[TR].x
self.v[BR].y = self.v[BL].y
def setLeft(self, lft):
self.v[TR].x = lft + self.v[TR].x - self.v[BL].x
self.v[BL].x = lft
# update othere verts
self.updateV34()
def setRight(self, rgt):
self.v[BL].x = rgt - (self.v[TR].x - self.v[BL].x)
self.v[TR].x = rgt
self.updateV34()
def setBottom(self, btm):
self.v[TR].y = btm + self.v[TR].y - self.v[BL].y
self.v[BL].y = btm
self.updateV34()
def setTop(self, tp):
self.v[BL].y = tp - (self.v[TR].y - self.v[BL].y)
self.v[TR].y = tp
self.updateV34()
def getLeft(self):
return self.v[BL].x
def getRight(self):
return self.v[TR].x
def getBottom(self):
return self.v[BL].y
def getTop(self):
return self.v[TR].y
def overlapAll(self, boxLs, intersectCache): # Flag index lets us know which quadere
''' Returns none, meaning it didnt overlap any new boxes '''
v= self.v
if v[BL].x < 0:
return True
elif v[BL].y < 0:
return True
else:
bIdx = len(intersectCache)
while bIdx:
bIdx-=1
b = intersectCache[bIdx]
if not ( v[TR].y <= b.v[BL].y or\
v[BL].y >= b.v[TR].y or\
v[BL].x >= b.v[TR].x or\
v[TR].x <= b.v[BL].x ):
return True # Intersection with existing box
#return 0 # Must keep looking
for b in boxLs.boxes:
if not (v[TR].y <= b.v[BL].y or\
v[BL].y >= b.v[TR].y or\
v[BL].x >= b.v[TR].x or\
v[TR].x <= b.v[BL].x ):
return b # Intersection with new box.
return False
def place(self, vert, quad):
'''
Place the box on the free quadrent of the vert
'''
if quad == BLF:
self.setRight(vert.x)
self.setTop(vert.y)
elif quad == TRF:
self.setLeft(vert.x)
self.setBottom(vert.y)
elif quad == TLF:
self.setRight(vert.x)
self.setBottom(vert.y)
elif quad == BRF:
self.setLeft(vert.x)
self.setTop(vert.y)
# Trys to lock a box onto another box's verts
# cleans up double verts after
def tryVert(self, boxes, baseVert):
for flagIndex, freeQuad in enumerate(quadFlagLs):
#print 'Testing ', self.width
if baseVert.free & freeQuad:
self.place(baseVert, freeQuad)
overlapBox = self.overlapAll(boxes, baseVert.intersectCache[flagIndex])
if overlapBox is False: # There is no overlap
baseVert.free &= ~freeQuad # Removes quad
# Appends all verts but the one that matches. this removes the need for remove doubles
for vIdx in (0,1,2,3): # (BL,TR,TL,BR) / 0,1,2,3
self_v= self.v[vIdx] # shortcut
if not (self_v.x == baseVert.x and self_v.y == baseVert.y):
boxList.packedVerts.verts.append(self_v)
else:
baseVert.free &= self_v.free # make sure the that any unfree areas are wiped.
# Inherit used boxes from old verts
if self_v.blb: baseVert.blb = self_v.blb
if self_v.brb: baseVert.brb = self_v.brb #print 'inherit2'
if self_v.tlb: baseVert.tlb = self_v.tlb #print 'inherit3'
if self_v.trb: baseVert.trb = self_v.trb #print 'inherit4'
self.v[vIdx] = baseVert
# Logical checking for used verts by compares box sized and works out verts that may be free.
# Verticle
if baseVert.tlb and baseVert.trb and\
(self == baseVert.tlb or self == baseVert.trb):
if baseVert.tlb.height > baseVert.trb.height:
baseVert.trb.v[TL].free &= ~(TLF|BLF)
elif baseVert.tlb.height < baseVert.trb.height:
baseVert.tlb.v[TR].free &= ~(TRF|BRF)
else: # same
baseVert.tlb.v[TR].free &= ~BLF
baseVert.trb.v[TL].free &= ~BRF
elif baseVert.blb and baseVert.brb and\
(self == baseVert.blb or self == baseVert.brb):
if baseVert.blb.height > baseVert.brb.height:
baseVert.brb.v[BL].free &= ~(TLF|BLF)
elif baseVert.blb.height < baseVert.brb.height:
baseVert.blb.v[BR].free &= ~(TRF|BRF)
else: # same
baseVert.blb.v[BR].free &= ~TRF
baseVert.brb.v[BL].free &= ~TLF
# Horizontal
if baseVert.tlb and baseVert.blb and\
(self == baseVert.tlb or self == baseVert.blb):
if baseVert.tlb.width > baseVert.blb.width:
baseVert.blb.v[TL].free &= ~(TLF|TRF)
elif baseVert.tlb.width < baseVert.blb.width:
baseVert.tlb.v[BL].free &= ~(BLF|BRF)
else: # same
baseVert.blb.v[TL].free &= ~TRF
baseVert.tlb.v[BL].free &= ~BRF
elif baseVert.trb and baseVert.brb and\
(self == baseVert.trb or self == baseVert.brb):
if baseVert.trb.width > baseVert.brb.width:
baseVert.brb.v[TR].free &= ~(TRF|TRF)
elif baseVert.trb.width < baseVert.brb.width:
baseVert.trb.v[BR].free &= ~(BLF|BRF)
else: # same
baseVert.brb.v[TR].free &= ~TLF
baseVert.trb.v[BR].free &= ~BLF
# END LOGICAL VREE SIZE REMOVAL
return 1 # Working
# We have a box that intersects that quadrent.
elif overlapBox is not False and overlapBox is not True: # True is used for a box thats alredt in the freq list or out of bounds error.
# There was an overlap, add this box to the verts list
#quadFlagLs = (BLF,BRF,TLF,TRF)
baseVert.intersectCache[flagIndex].append(overlapBox)
# Limit the cache size
if len(baseVert.intersectCache[flagIndex]) > 8:
del baseVert.intersectCache[flagIndex][0]
return 0
class boxList:
#Global vert pool, stores used lists
packedVerts = vertList() # will be vertList()
def __init__(self, boxes):
self.boxes = boxes
# keep a running update of the width and height so we know the area
# initialize with first box, fixes but where we whwere only packing 1 box
# At the moment we only start with 1 box so the code below will loop over 1. but thats ok.
width = height = 0.0
if boxes:
for b in boxes:
if width < b.width: width= b.width
if height < b.height: height= b.height
self.width= width
self.height= height
# boxArea is the total area of all boxes in the list,
# can be used with packArea() to determine waistage.
self.boxArea = 0 # incremented with addBox()
# Just like MyBoxLs.boxes.append(), but sets bounds
def addBoxPack(self, box):
'''Adds the box to the boxlist and resized the main bounds and adds area. '''
self.width = max(self.width, box.getRight())
self.height = max(self.height, box.getTop())
self.boxArea += box.area
# iterate through these
#~ quadFlagLs = (1,8,4,2)
#~ # Flags for vert idx used quads
#~ BLF = 1; TRF = 2; TLF = 4; BRF = 8
#~ quadFlagLs = (BLF,BRF,TLF,TRF)
# Look through all the free vert quads and see if there are some we can remove
#
for v in box.v:
# Is my bottom being used.
if v.free & BLF and v.free & BRF: # BLF and BRF
for b in self.boxes:
if b.v[TR].y == v.y:
if b.v[TR].x > v.x:
if b.v[BL].x < v.x:
v.free &= ~(BLF|BRF) # Removes quad
# Is my left being used.
if v.free & BLF and v.free & TLF:
for b in self.boxes:
if b.v[TR].x == v.x:
if b.v[TR].y > v.y:
if b.v[BL].y < v.y:
v.free &= ~(BLF|TLF) # Removes quad
if v.free & TRF and v.free & TLF:
# Is my top being used.
for b in self.boxes:
if b.v[BL].y == v.y:
if b.v[TR].x > v.x:
if b.v[BL].x < v.x:
v.free &= ~(TLF|TRF) # Removes quad
# Is my right being used.
if v.free & TRF and v.free & BRF:
for b in self.boxes:
if b.v[BL].x == v.x:
if b.v[TR].y > v.y:
if b.v[BL].y < v.y:
v.free &= ~(BRF|TRF) # Removes quad
self.boxes.append(box)
# Just like MyBoxLs.boxes.append(), but sets bounds
def addBox(self, box):
self.boxes.append(box)
self.boxArea += box.area
# The area of the backing bounds.
def packedArea(self):
return self.width * self.height
# Sort boxes by area
def sortArea(self):
self.boxes.sort(lambda A, B: cmp(A.area, B.area) ) # Reverse area sort
# BLENDER only
def draw(self):
m = NMesh.GetRaw()
for b in self.boxes:
z = min(b.width, b.height ) / max(b.width, b.height )
#z = b.farea
#z=0
f = NMesh.Face()
m.verts.append(NMesh.Vert(b.getLeft(), b.getBottom(), z))
f.v.append(m.verts[-1])
m.verts.append(NMesh.Vert(b.getRight(), b.getBottom(), z))
f.v.append(m.verts[-1])
m.verts.append(NMesh.Vert(b.getRight(), b.getTop(), z))
f.v.append(m.verts[-1])
m.verts.append(NMesh.Vert(b.getLeft(), b.getTop(), z))
f.v.append(m.verts[-1])
m.faces.append(f)
NMesh.PutRaw(m, 's')
Window.Redraw(1)
def pack(self):
self.sortArea()
if not self.boxes:
return
packedboxes = boxList([self.boxes[-1]])
# Remove verts we KNOW cant be added to
unpackedboxes = self.boxes[:-1]
# Start with this box, the biggest box
boxList.packedVerts.verts.extend(packedboxes.boxes[0].v)
while unpackedboxes: # != [] - while the list of unpacked boxes is not empty.
freeBoxIdx = len(unpackedboxes)
while freeBoxIdx:
freeBoxIdx-=1
freeBoxContext= unpackedboxes[freeBoxIdx]
# Sort the verts with this boxes dimensions as a bias, so less poky out bits are made.
boxList.packedVerts.sortCorner(freeBoxContext.width, freeBoxContext.height)
vertIdx = 0
for baseVert in boxList.packedVerts.verts:
if baseVert.free: # != 0
# This will lock the box if its possibel
if freeBoxContext.tryVert(packedboxes, baseVert):
packedboxes.addBoxPack( unpackedboxes.pop(freeBoxIdx) ) # same as freeBoxContext. but may as well pop at the same time.
freeBoxIdx = -1
break
freeBoxIdx +=1
boxList.packedVerts.verts = [] # Free the list, so it dosent use ram between runs.
self.width = packedboxes.width
self.height = packedboxes.height
#
def list(self):
''' Once packed, return a list of all boxes as a list of tuples - (X/Y/WIDTH/HEIGHT) '''
return [(b.id, b.getLeft(), b.getBottom(), b.width, b.height ) for b in self.boxes]
''' Define all globals here '''
# vert IDX's, make references easier to understand.
BL = 0; TR = 1; TL = 2; BR = 3
# iterate through these
# Flags for vert idx used quads
BLF = 1; TRF = 2; TLF = 4; BRF = 8
quadFlagLs = (BLF,BRF,TLF,TRF)
# Packs a list w/h's into box types and places then #Iter times
def boxPackIter(boxLs, iter=1, draw=0):
iterIdx = 0
bestArea = None
# Iterate over packing the boxes to get the best FIT!
while iterIdx < iter:
myBoxLs = boxList([])
for b in boxLs:
myBoxLs.addBox( box(b[1], b[2], b[0]) ) # w/h/id
myBoxLs.pack()
# myBoxLs.draw() # Draw as we go?
newArea = myBoxLs.packedArea()
#print 'pack test %s of %s, area:%.2f' % (iterIdx, iter, newArea)
# First time?
if bestArea == None:
bestArea = newArea
bestBoxLs = myBoxLs
elif newArea < bestArea:
bestArea = newArea
bestBoxLs = myBoxLs
iterIdx+=1
if draw:
bestBoxLs.draw()
#print 'best area: %.4f, %.2f%% efficient' % (bestArea, (float(bestBoxLs.boxArea) / (bestArea+0.000001))*100)
return bestBoxLs.width, bestBoxLs.height, bestBoxLs.list()