304 lines
10 KiB
Python
304 lines
10 KiB
Python
"""
|
|
This module plots many shapes a polygon objects containing a series of lines
|
|
The Polygon object can be found in reprap.toolpath
|
|
"""
|
|
|
|
# Python module properties
|
|
__author__ = "Stefan Blanke (greenarrow) (greenarrow@users.sourceforge.net)"
|
|
__license__ = "GPL 3.0"
|
|
__credits__ = "Author of potrace"
|
|
__licence__ = """
|
|
pyRepRap is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
pyRepRap is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with pyRepRap. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import math, os
|
|
import toolpath
|
|
|
|
# Fill modes
|
|
FILL_LOCUS = 1
|
|
FILL_LINES = 2
|
|
|
|
debug = False
|
|
|
|
def line(line):
|
|
"""Returns polygon for line (x1, y2, x2, y2) as a Polygon object"""
|
|
poly = toolpath.Polygon()
|
|
x1, y1, x2, y2 = line
|
|
poly.addPoint( toolpath.Point(x1, y1) )
|
|
poly.addPoint( toolpath.Point(x2, y2) )
|
|
return poly
|
|
|
|
def point(point):
|
|
"""Returns polygon for point (x, y) as a Polygon Object"""
|
|
poly = toolpath.Polygon()
|
|
x, y = point
|
|
poly.addPoint( toolpath.Point(x, y) )
|
|
return poly
|
|
|
|
def arc(x, y, radius, startAngle, endAngle, resolution):
|
|
"""Returns polygon an arc with x, y, radius, start angle (radians), end engle (radians) and resolution (lines per mm) a Polygon object"""
|
|
# This function works in degrees but takes parameters in radians
|
|
poly = toolpath.Polygon()
|
|
if debug: print "Plotting arc at", x, y, "from", startAngle, "(", math.degrees(startAngle), ") to", endAngle, "(", math.degrees(endAngle), ")"
|
|
startAngle, endAngle = math.degrees(startAngle), math.degrees(endAngle)
|
|
circumference = float(2) * math.pi * float(radius)
|
|
angleDiv = ( float(360) / float( circumference * resolution ) )# + ( 360 % int( circumference * resolution ) )
|
|
lastX, lastY = _calcCircle( startAngle, radius )
|
|
# compensate for arc going beyond 360 deg
|
|
if startAngle > endAngle:
|
|
endAngle += 360
|
|
# make detail proportional to radius to always give good resolution
|
|
for theta in _frange( startAngle, endAngle + angleDiv, angleDiv ):
|
|
cx, cy = _calcCircle(theta, radius)
|
|
poly.addPoint( toolpath.Point(x + cx, y + cy) )
|
|
return poly
|
|
|
|
def circle(x, y, radius, resolution, fillDensity = False):
|
|
"""Returns polygon for a filled circle with x, y, radius, resolution (lines per mm) and fill density (lines per mm) as a Polygon object"""
|
|
poly = toolpath.Polygon()
|
|
if fillDensity:
|
|
numFills = int( float(fillDensity) * float(radius) )
|
|
else:
|
|
numFills = 1
|
|
for d in range( 1, numFills + 1 ):
|
|
r = ( float(d) / float(numFills) ) * float(radius)
|
|
if debug: print "using r", r, "mm"
|
|
poly.addPolygon( arc( x, y, r, math.radians(0), math.radians(360), resolution ) )
|
|
return poly
|
|
|
|
def ellipse(x, y, a, b, resolution, fillDensity = False):
|
|
"""Returns polygon for a filled ellipse with x, y, a, b, resolution (lines per mm) and fill density (lines per mm) as a Polygon object"""
|
|
poly = toolpath.Polygon()
|
|
if not resolution:
|
|
resolution = self.circleResolution
|
|
if fillDensity:
|
|
if a > b:
|
|
largerDimension = a
|
|
else:
|
|
largerDimension = b
|
|
numFills = int( float(fillDensity) * float(largerDimension) )
|
|
else:
|
|
numFills = 1
|
|
startAngle, endAngle = 0, 360
|
|
# Not really cicumference but will do?
|
|
circumference = float(2) * math.pi * float( float( a + b ) / 2 )
|
|
angleDiv = ( float(360) / float( circumference * resolution ) ) # + ( 360 % int( circumference * resolution ) )
|
|
lastX, lastY = _calcEllipse( startAngle, a, b )
|
|
# Compensate for arc going beyond 360 deg
|
|
if startAngle > endAngle:
|
|
endAngle += 360
|
|
for d in range( 1, numFills + 1 ):
|
|
ra = ( float(d) / float(numFills) ) * float(a)
|
|
rb = ( float(d) / float(numFills) ) * float(b)
|
|
for theta in _frange( startAngle, endAngle + angleDiv, angleDiv ):
|
|
newX, newY = _calcEllipse( theta, ra, rb )
|
|
aLine = poly.addPoint( toolpath.Point(newX + x , newY + y) )
|
|
if debug: print "aLine", aLine
|
|
return poly
|
|
|
|
# Return polygon for a filled rectangle
|
|
def rectangle(x, y, width, height, fillDensity = False):
|
|
"""Returns polygon for line x, y, width, height and fill density (lines per mm) as a Polygon object"""
|
|
poly = toolpath.Polygon()
|
|
numFillsY = int( float(fillDensity) * float(height) )
|
|
cornerX, cornerY = x - ( width / 2 ), y - ( height / 2 )
|
|
invert = False
|
|
|
|
for dy in range( 0, numFillsY + 1 ):
|
|
ry = ( float(dy) / float(numFillsY) ) * float(height)
|
|
line1 = (cornerX, cornerY + ry, cornerX + width, cornerY + ry )
|
|
line2 = (cornerX, cornerY + ry, cornerX + width, cornerY - ry )
|
|
if invert:
|
|
line1 = _reverseLine(line1)
|
|
invert = not invert
|
|
x1, y1, x2, y2 = line1
|
|
poly.addPoint( toolpath.Point(x1, y1) )
|
|
poly.addPoint( toolpath.Point(x2, y2) )
|
|
|
|
poly.addPoint( toolpath.Point(cornerX, cornerY) )
|
|
poly.addPoint( toolpath.Point(cornerX, cornerY + height) )
|
|
poly.addPoint( toolpath.Point(cornerX + width, cornerY) )
|
|
poly.addPoint( toolpath.Point(cornerX + width, cornerY + height) )
|
|
return poly
|
|
|
|
def circleStroke(x1, y1, x2, y2, radius, resolution, fillDensity = False ):
|
|
"""Returns polygon for a photoplotter moving stroke using a circular aperture with x, y, radius, resolution (lines per mm) and fill density (lines per mm) as a Polygon object"""
|
|
poly = toolpath.Polygon()
|
|
deltaY = y2 - y1
|
|
deltaX = x2 - x1
|
|
if x1 == x2 and y1 == y2:
|
|
#print "this is not a move, this is why software like eagle that uses drm on your own files....is crap"
|
|
poly.addPolygon( circle( x1, y1, radius, resolution = resolution, fillDensity = fillDensity ) )
|
|
else:
|
|
if debug: print "PMWC, fill density", fillDensity
|
|
#Plot central line
|
|
poly.addPoint( toolpath.Point(x1, y1) )
|
|
poly.addPoint( toolpath.Point(x2, y2) )
|
|
# For each locus
|
|
numFills = int( float(fillDensity) * float(radius) )
|
|
for d in range( 1, numFills + 1 ):
|
|
r = ( float(d) / float(numFills) ) * float(radius)
|
|
if debug: print "using r", r, "mm"
|
|
theta = _angleFromDeltas( deltaX, deltaY )
|
|
rsintheta = r * math.sin( theta )
|
|
rcostheta = r * math.cos( theta )
|
|
# Makes sure angle is in correct quadrant
|
|
if deltaX > 0:
|
|
startOffset = math.radians(90)
|
|
endOffset = math.radians(-90)
|
|
else:
|
|
startOffset = math.radians(-90)
|
|
endOffset = math.radians(90)
|
|
if deltaY < 0:
|
|
startOffset = -startOffset
|
|
endOffset = -endOffset
|
|
# Plot side lines and end arcs (simi-circles) of locus
|
|
# reversing these point lets locus be drawn in one continual motion
|
|
poly.addPoint( toolpath.Point(x2 - rsintheta, y2 + rcostheta) )
|
|
poly.addPoint( toolpath.Point(x1 - rsintheta, y1 + rcostheta) )
|
|
poly.addPolygon( arc(x1, y1, r, theta + startOffset, theta + endOffset, resolution) )
|
|
poly.addPoint( toolpath.Point(x1 + rsintheta, y1 - rcostheta) )
|
|
poly.addPoint( toolpath.Point(x2 + rsintheta, y2 - rcostheta) )
|
|
poly.addPolygon( arc(x2, y2, r, theta - startOffset, theta - endOffset, resolution) )
|
|
return poly
|
|
|
|
def fill(polygon, fillDensity):
|
|
pass
|
|
|
|
def raster(fileName, originalWidth, originalHeight, svg = False):
|
|
"""Returns polygon for a vectorised raster as a Polygon object.
|
|
Uses external potrace program to convert raster file into polygon(s)
|
|
"""
|
|
poly = toolpath.Polygon()
|
|
if svg:
|
|
os.system("potrace --svg --output " + fileName[ :-3 ] + "svg " + fileName)
|
|
os.system("potrace --alphamax 0 --turdsize 5 --backend gimppath --output " + fileName[ :-3 ] + "gimppath " + fileName)
|
|
os.system("rm " + fileName)
|
|
f = open(fileName[ :-3 ] + "gimppath")
|
|
pathLines = f.readlines()
|
|
f.close()
|
|
os.system("rm " + fileName[ :-3 ] + "gimppath")
|
|
scale = 0.005 # temp - competely arbitary
|
|
# 1 / 200, i.e 1 / (resolution = 20 * 100 for some reason)
|
|
for l in pathLines:
|
|
parts = l.split(' ')
|
|
isPoint = False
|
|
for i, p in enumerate(parts):
|
|
if p == 'TYPE:':
|
|
ptype = int(parts[i + 1])
|
|
isPoint = True
|
|
elif p == 'X:':
|
|
x = float(parts[i + 1]) * scale
|
|
elif p == 'Y:':
|
|
y = float(parts[i + 1]) * scale
|
|
if isPoint:
|
|
poly.addPoint( toolpath.Point(x, y) )
|
|
#print "NEW POINT", x, y, ptype
|
|
# This should not be assumed?
|
|
poly.closed = True
|
|
|
|
"""
|
|
#this needs to be done on all paths at same time
|
|
maxX, maxY = 0, 0
|
|
for p in points:
|
|
x, y, t = p
|
|
maxX = max(maxX, x)
|
|
maxY = max(maxY, y)
|
|
print "max", maxX, maxY
|
|
#print "read", len(points), "points"
|
|
scaleX = originalWidth / maxX
|
|
scaleY = originalHeight / maxY
|
|
print "scales", scaleX, scaleY
|
|
for i in range(len(points)):
|
|
x, y, y = points[i]
|
|
x = x * scaleX
|
|
y = y * scaleY
|
|
points[i] = x, y, t
|
|
"""
|
|
#should make this return a list of all found polygons
|
|
return poly
|
|
|
|
|
|
############# General Maths Functions #############
|
|
|
|
# Return the coordinates of a point on a circle at theta (rad) with radius.
|
|
def _calcCircle(theta, radius):
|
|
x = math.cos( math.radians(theta) ) * radius
|
|
y = math.sin( math.radians(theta) ) * radius
|
|
return x, y
|
|
|
|
# Return the coordinates of a point on an ellipse at theta (rad) with a and b.
|
|
def _calcEllipse(theta, a, b):
|
|
x = math.cos( math.radians(theta) ) * a
|
|
y = math.sin( math.radians(theta) ) * b
|
|
return x, y
|
|
|
|
# Reverse line (swap x1, y1 and x2, y2)
|
|
def _reverseLine( line ):
|
|
x1, y1, x2, y2 = line
|
|
return x2, y2, x1, y1
|
|
|
|
# Return the angle between the line between two points (2D coordinates) and vetical?
|
|
def _angleFromDeltas( dx, dy ):
|
|
radius = math.sqrt( ( dx * dx ) + ( dy * dy ) )
|
|
#if radius != 0:
|
|
dx, dy = dx / radius, dy / radius
|
|
if dx > 0:
|
|
if dy > 0:
|
|
return math.asin(dx)
|
|
elif dy < 0:
|
|
return math.acos(dx) + math.radians(90)
|
|
else:
|
|
#print "moo1"
|
|
return 0
|
|
elif dx < 0:
|
|
if dy > 0:
|
|
return math.asin(dy) + math.radians(270)
|
|
elif dy < 0:
|
|
return math.radians(180) - math.asin(dx)
|
|
else:
|
|
#print "moo2"
|
|
return 0
|
|
else:
|
|
return math.radians(-90) # i think this should really be 90, it just makes thae program work wen its -90 :)
|
|
#else:
|
|
# print "Radius cannot be zero!, returning 0 (_angleFromDeltas, shapeplotter.py)"
|
|
|
|
# Range function accepting floats (by Dinu Gherman)
|
|
def _frange(start, end=None, inc=None):
|
|
if end == None:
|
|
end = start + 0.0
|
|
start = 0.0
|
|
if inc == None:
|
|
inc = 1.0
|
|
L = []
|
|
while 1:
|
|
next = start + len(L) * inc
|
|
if inc > 0 and next >= end:
|
|
break
|
|
elif inc < 0 and next <= end:
|
|
break
|
|
L.append(next)
|
|
return L
|
|
|
|
# Return length of vector
|
|
def _calcVectorLength(line):
|
|
x1, y1, x2, y2 = line
|
|
deltaX = max(x1, x2) - min(x1, x2)
|
|
deltaY = max(y1, y2) - min(y1, y2)
|
|
return math.sqrt( math.pow(deltaX, 2) + math.pow(deltaY, 2) )
|
|
|
|
|
|
|