gerbmerge/gerbmerge/tiling.py

378 lines
14 KiB
Python

#!/usr/bin/env python
"""A tiling is an arrangement of jobs, where each job may
be a copy of another and may be rotated. A tiling consists
of two things:
- a list of where each job is located (the lower-left of
each job is the origin)
- a list of points that begins at (0,Ymax) and ends at
(Xmax,0). These points describe the outside boundary
of the tiling.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import math
import config
import jobs
# Helper functions to determine if points are right-of, left-of, above, and
# below each other. These definitions assume that points are on a line that
# is vertical or horizontal.
def left_of(p1,p2):
return p1[0]<p2[0] and p1[1]==p2[1]
def right_of(p1,p2):
return p1[0]>p2[0] and p1[1]==p2[1]
def above(p1,p2):
return p1[1]>p2[1] and p1[0]==p2[0]
def below(p1,p2):
return p1[1]<p2[1] and p1[0]==p2[0]
class Tiling:
def __init__(self, Xmax, Ymax):
# Make maximum dimensions bigger by inter-job spacing so that
# we allow jobs (which are seated at the lower left of their cells)
# to just fit on the panel, and not disqualify them because their
# spacing area slightly exceeds the panel edge.
self.xmax = Xmax + config.Config['xspacing']
self.ymax = Ymax + config.Config['yspacing']
self.points = [(0,Ymax), (0,0), (Xmax,0)] # List of (X,Y) co-ordinates
self.jobs = [] # List of 3-tuples: ((Xbl,Ybl),(Xtr,Ytr),Job) where
# (Xbl,Ybl) is bottom left, (Xtr,Ytr) is top-right of the cell.
# The actual job has dimensions (Xtr-Xbl-Config['xspacing'],Ytr-Ybl-Config['yspacing'])
# and is located at the lower-left of the cell.
def canonicalize(self, OriginX, OriginY):
"""Return a list of JobLayout objects, after setting each job's (X,Y) origin"""
L = []
for job in self.jobs:
J = jobs.JobLayout(job[2])
J.setPosition(job[0][0]+OriginX, job[0][1]+OriginY)
L.append(J)
return L
def corners(self):
return len(self.points)-2
def clone(self):
T = Tiling(self.xmax-config.Config['xspacing'], self.ymax-config.Config['yspacing'])
T.points = self.points[:]
T.jobs = self.jobs[:]
return T
def dump(self, fid=sys.stdout):
fid.write("Points:\n ")
count = 0
for XY in self.points:
fid.write("%s " % str(XY))
count += 1
if count==8:
fid.write("\n ")
count=0
if count:
fid.write("\n")
fid.write("Jobs:\n")
for bl,tr,Job in self.jobs:
fid.write(" %s: %s\n" % (str(Job), str(bl)))
def joblist(self, fid=sys.stdout):
for bl,tr,Job in self.jobs:
fid.write("%s@(%.1f,%.1f) " % (Job.name,bl[0],bl[1]))
fid.write('\n')
def isOverlap(self, ix, X, Y, cfg=config.Config):
"""Determines if a new job with actual dimensions X-by-Y located at self.points[ix]
overlaps any existing job or exceeds the boundaries of the panel.
If it's an L-point, the new job will have position
p_bl=(self.points[ix][0],self.points[ix][1])
and top-right co-ordinate:
p_tr=(self.points[ix][0]+X,self.points[ix][1]+Y)
If it's a mirror-L point, the new job will have position
p_bl=(self.points[ix][0]-X,self.points[ix][1])
and top-right co-ordinate:
p_tr=(self.points[ix][0],self.points[ix][1]+Y)
For a test job defined by t_bl and t_tr, the given job overlaps
if:
p.left_edge<t.right_edge and p.right_edge>t.left_edge
and
p.bottom_edge<t.top_edge and p.top_edge>t.bottom_edge
"""
if self.isL(ix):
p_bl = self.points[ix]
p_tr = (p_bl[0]+X, p_bl[1]+Y)
if p_tr[0]>self.xmax or p_tr[1]>self.ymax:
return 1
else:
p_bl = (self.points[ix][0]-X,self.points[ix][1])
p_tr = (self.points[ix][0],self.points[ix][1]+Y)
if p_bl[0]<0 or p_tr[1]>self.ymax:
return 1
for t_bl,t_tr,Job in self.jobs:
if p_bl[0]<t_tr[0] and p_tr[0]>t_bl[0] \
and \
p_bl[1]<t_tr[1] and p_tr[1]>t_bl[1]:
return 1
return 0
def isL(self, ix):
"""True if self.points[ix] represents an L-shaped corner where there
is free space above and to the right, like this:
+------+ _____ this point is an L-shaped corner
| | /
| +______+
| |
| |
. .
. .
. .
"""
pts = self.points
# This is an L-point if:
# Previous point X co-ordinates are the same, and
# previous point Y co-ordinate is higher, and
# next point Y co-ordinate is the same, and
# next point X co-ordinate is to the right
return pts[ix-1][0]==pts[ix][0] \
and pts[ix-1][1]>pts[ix][1] \
and pts[ix+1][1]==pts[ix][1] \
and pts[ix+1][0]>pts[ix][0]
def isMirrorL(self, ix):
"""True if self.points[ix] represents a mirrored L-shaped corner where there
is free space above and to the left, like this:
+------+
mirrored-L corner __ | |
\ | |
+______+ |
| |
| |
. .
. .
. .
"""
pts = self.points
# This is a mirrored L-point if:
# Previous point Y co-ordinates are the same, and
# previous point X co-ordinate is lower, and
# next point X co-ordinate is the same, and
# next point X co-ordinate is higher
return pts[ix-1][1]==pts[ix][1] \
and pts[ix-1][0]<pts[ix][0] \
and pts[ix+1][0]==pts[ix][0] \
and pts[ix+1][1]>pts[ix][1]
def validAddPoints(self, X, Y):
"""Return a list of all valid indices into self.points at which we can add
the job with dimensions X-by-Y). Only points which are either L-points or
mirrored-L-points and which would support the given job with no overlaps
are returned.
"""
return [ix for ix in range(1,len(self.points)-1) if (self.isL(ix) or self.isMirrorL(ix)) and not self.isOverlap(ix,X,Y)]
def mergePoints(self, ix):
"""Inspect points self.points[ix] and self.points[ix+1] as well
as self.points[ix+3] and self.points[ix+4]. If they are the same, delete
both points, thus merging lines formed when the corners of two jobs coincide.
"""
# Do farther-on points first so we can delete things right from the list
if self.points[ix+3]==self.points[ix+4]:
del self.points[ix+3:ix+5]
if self.points[ix]==self.points[ix+1]:
del self.points[ix:ix+2]
# Experimental
def removeInlets(self, minSize):
"""Find sequences of 3 points that define an "inlet", either a left/right-going gap:
...---------------+ +--------.....
| |
| |
+------------+ +------------+
| |
+----------------------+ +--------------------+
| |
. .
. .
. .
or a down-going gap: +-------.....
|
...-----------+ ...-------------+ |
| | |
| | |
| | |
| | |
| +-----.... | |
| | | |
| | | |
+--+ +--+
that are too small for any job to fit in (as defined by minSize). These inlets
can be deleted to form corners where new jobs can be placed.
"""
pt = self.points
done = 0
while not done:
# Repeat this loop each time there is a change
for ix in range(0, len(pt)-3):
# Check for horizontal left-going inlet
if right_of(pt[ix],pt[ix+1]) and above(pt[ix+1],pt[ix+2]) and left_of(pt[ix+2],pt[ix+3]):
# Make sure minSize requirement is met
if pt[ix][1]-pt[ix+3][1] < minSize:
# Get rid of middle two points, extend Y-value of highest point down to lowest point
pt[ix] = (pt[ix][0],pt[ix+3][1])
del pt[ix+1:ix+3]
break
# Check for horizontal right-going inlet
if left_of(pt[ix],pt[ix+1]) and below(pt[ix+1],pt[ix+2]) and right_of(pt[ix+2],pt[ix+3]):
# Make sure minSize requirement is met
if pt[ix+3][1]-pt[ix][1] < minSize:
# Get rid of middle two points, exten Y-value of highest point down to lowest point
pt[ix+3] = (pt[ix+3][0], pt[ix][1])
del pt[ix+1:ix+3]
break
# Check for vertical inlets
if above(pt[ix],pt[ix+1]) and left_of(pt[ix+1],pt[ix+2]) and below(pt[ix+2],pt[ix+3]):
# Make sure minSize requirement is met
if pt[ix+3][0]-pt[ix][0] < minSize:
# Is right side lower or higher?
if pt[ix+3][1]>=pt[ix][1]: # higher?
pt[ix] = (pt[ix+3][0], pt[ix][1]) # Move first point to the right
else: # lower?
pt[ix+3] = (pt[ix][0], pt[ix+3][1]) # Move last point to the left
del pt[ix+1:ix+3]
break
else:
done = 1
def addLJob(self, ix, X, Y, Job, cfg=config.Config):
"""Add a job to the tiling at L-point self.points[ix] with actual dimensions X-by-Y.
The job is added with its lower-left corner at the point. The existing point
is removed from the tiling and new points are added at the top-left, top-right
and bottom-right of the new job, with extra space added for inter-job spacing.
"""
x,y = self.points[ix]
x_tr = x+X
y_tr = y+Y
self.points[ix:ix+1] = [(x,y_tr), (x_tr,y_tr), (x_tr,y)]
self.jobs.append( ((x,y),(x_tr,y_tr),Job) )
self.mergePoints(ix-1)
def addMirrorLJob(self, ix, X, Y, Job, cfg=config.Config):
"""Add a job to the tiling at mirror-L-point self.points[ix] with dimensions X-by-Y.
The job is added with its lower-right corner at the point. The existing point
is removed from the tiling and new points are added at the bottom-left, top-left
and top-right of the new job, with extra space added for inter-job spacing.
"""
x_tr,y = self.points[ix]
x = x_tr-X
y_tr = y+Y
self.points[ix:ix+1] = [(x,y), (x,y_tr), (x_tr,y_tr)]
self.jobs.append( ((x,y),(x_tr,y_tr),Job) )
self.mergePoints(ix-1)
def addJob(self, ix, X, Y, Job):
"""Add a job to the tiling at point self.points[ix] and with dimensions X-by-Y.
If the given point is an L-point, the job will be added with its lower-left
corner at the point. If the given point is a mirrored-L point, the job will
be added with its lower-right corner at the point.
"""
if self.isL(ix):
self.addLJob(ix, X, Y, Job)
else:
self.addMirrorLJob(ix, X, Y, Job)
def bounds(self):
"""Return 2-tuple ((minX, minY), (maxX, maxY)) of rectangular region defined by all jobs"""
minX = minY = float(sys.maxint)
maxX = maxY = 0.0
for bl,tr,job in self.jobs:
minX = min(minX,bl[0])
maxX = max(maxX,tr[0])
minY = min(minY,bl[1])
maxY = max(maxY,tr[1])
return ( (minX,minY), (maxX-config.Config['xspacing'], maxY-config.Config['yspacing']) )
def area(self):
"""Return area of rectangular region defined by all jobs."""
bl,tr = self.bounds()
DX = tr[0]-bl[0]
DY = tr[1]-bl[1]
return DX*DY
def usedArea(self):
"""Return total area of just jobs, not spaces in-between."""
area = 0.0
for job in self.jobs:
area += job[2].jobarea()
return area
# Function to estimate the maximum possible utilization given a list of jobs.
# Jobs list is 4-tuple (Xdim,Ydim,job,rjob).
def maxUtilization(Jobs):
xspacing = config.Config['xspacing']
yspacing = config.Config['yspacing']
usedArea = totalArea = 0.0
for Xdim,Ydim,job,rjob in Jobs:
usedArea += job.jobarea()
totalArea += job.jobarea()
totalArea += job.width_in()*xspacing + job.height_in()*yspacing + xspacing*yspacing
# Reduce total area by strip of unused spacing around top and side. Assume
# final result will be approximately square.
sq_side = math.sqrt(totalArea)
totalArea -= sq_side*xspacing + sq_side*yspacing + xspacing*yspacing
return usedArea/totalArea
# Utility function to compute the minimum dimension along any axis of all jobs.
# Used to remove inlets.
def minDimension(Jobs):
M = float(sys.maxint)
for Xdim,Ydim,job,rjob in Jobs:
M = min(M,Xdim)
M = min(M,Ydim)
return M
# vim: expandtab ts=2 sw=2