- grouped all parsers files in flatcamParsers folder
This commit is contained in:
453
flatcamParsers/ParseDXF.py
Normal file
453
flatcamParsers/ParseDXF.py
Normal file
@@ -0,0 +1,453 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 3/10/2019 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
from shapely.geometry import LineString
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('base2')
|
||||
|
||||
from flatcamParsers.ParseFont import *
|
||||
from flatcamParsers.ParseDXF_Spline import *
|
||||
|
||||
|
||||
def distance(pt1, pt2):
|
||||
return math.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
|
||||
|
||||
|
||||
def dxfpoint2shapely(point):
|
||||
|
||||
geo = Point(point.dxf.location).buffer(0.01)
|
||||
return geo
|
||||
|
||||
def dxfline2shapely(line):
|
||||
|
||||
try:
|
||||
start = (line.dxf.start[0], line.dxf.start[1])
|
||||
stop = (line.dxf.end[0], line.dxf.end[1])
|
||||
|
||||
except Exception as e:
|
||||
log.debug(str(e))
|
||||
return None
|
||||
|
||||
geo = LineString([start, stop])
|
||||
|
||||
return geo
|
||||
|
||||
def dxfcircle2shapely(circle, n_points=100):
|
||||
|
||||
ocs = circle.ocs()
|
||||
# if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
|
||||
if circle.dxf.extrusion != (0, 0, 1):
|
||||
center_pt = ocs.to_wcs(circle.dxf.center)
|
||||
else:
|
||||
center_pt = circle.dxf.center
|
||||
|
||||
radius = circle.dxf.radius
|
||||
geo = Point(center_pt).buffer(radius, int(n_points / 4))
|
||||
|
||||
return geo
|
||||
|
||||
|
||||
def dxfarc2shapely(arc, n_points=100):
|
||||
# ocs = arc.ocs()
|
||||
# # if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
|
||||
# if arc.dxf.extrusion != (0, 0, 1):
|
||||
# arc_center = ocs.to_wcs(arc.dxf.center)
|
||||
# start_angle = math.radians(arc.dxf.start_angle) + math.pi
|
||||
# end_angle = math.radians(arc.dxf.end_angle) + math.pi
|
||||
# dir = 'CW'
|
||||
# else:
|
||||
# arc_center = arc.dxf.center
|
||||
# start_angle = math.radians(arc.dxf.start_angle)
|
||||
# end_angle = math.radians(arc.dxf.end_angle)
|
||||
# dir = 'CCW'
|
||||
#
|
||||
# center_x = arc_center[0]
|
||||
# center_y = arc_center[1]
|
||||
# radius = arc.dxf.radius
|
||||
#
|
||||
# point_list = []
|
||||
#
|
||||
# if start_angle > end_angle:
|
||||
# start_angle += 2 * math.pi
|
||||
#
|
||||
# line_seg = int((n_points * (end_angle - start_angle)) / math.pi)
|
||||
# step_angle = (end_angle - start_angle) / float(line_seg)
|
||||
#
|
||||
# angle = start_angle
|
||||
# for step in range(line_seg + 1):
|
||||
# if dir == 'CCW':
|
||||
# x = center_x + radius * math.cos(angle)
|
||||
# y = center_y + radius * math.sin(angle)
|
||||
# else:
|
||||
# x = center_x + radius * math.cos(-angle)
|
||||
# y = center_y + radius * math.sin(-angle)
|
||||
# point_list.append((x, y))
|
||||
# angle += step_angle
|
||||
#
|
||||
#
|
||||
# log.debug("X = %.3f, Y = %.3f, Radius = %.3f, start_angle = %.1f, stop_angle = %.1f, step_angle = %.3f, dir=%s" %
|
||||
# (center_x, center_y, radius, start_angle, end_angle, step_angle, dir))
|
||||
#
|
||||
# geo = LineString(point_list)
|
||||
# return geo
|
||||
|
||||
ocs = arc.ocs()
|
||||
# if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
|
||||
if arc.dxf.extrusion != (0, 0, 1):
|
||||
arc_center = ocs.to_wcs(arc.dxf.center)
|
||||
start_angle = arc.dxf.start_angle + 180
|
||||
end_angle = arc.dxf.end_angle + 180
|
||||
dir = 'CW'
|
||||
else:
|
||||
arc_center = arc.dxf.center
|
||||
start_angle = arc.dxf.start_angle
|
||||
end_angle = arc.dxf.end_angle
|
||||
dir = 'CCW'
|
||||
|
||||
center_x = arc_center[0]
|
||||
center_y = arc_center[1]
|
||||
radius = arc.dxf.radius
|
||||
|
||||
point_list = []
|
||||
|
||||
if start_angle > end_angle:
|
||||
start_angle = start_angle - 360
|
||||
angle = start_angle
|
||||
|
||||
step_angle = float(abs(end_angle - start_angle) / n_points)
|
||||
|
||||
while angle <= end_angle:
|
||||
if dir == 'CCW':
|
||||
x = center_x + radius * math.cos(math.radians(angle))
|
||||
y = center_y + radius * math.sin(math.radians(angle))
|
||||
else:
|
||||
x = center_x + radius * math.cos(math.radians(-angle))
|
||||
y = center_y + radius * math.sin(math.radians(-angle))
|
||||
point_list.append((x, y))
|
||||
angle += abs(step_angle)
|
||||
|
||||
# in case the number of segments do not cover everything until the end of the arc
|
||||
if angle != end_angle:
|
||||
if dir == 'CCW':
|
||||
x = center_x + radius * math.cos(math.radians(end_angle))
|
||||
y = center_y + radius * math.sin(math.radians(end_angle))
|
||||
else:
|
||||
x = center_x + radius * math.cos(math.radians(- end_angle))
|
||||
y = center_y + radius * math.sin(math.radians(- end_angle))
|
||||
point_list.append((x, y))
|
||||
|
||||
# log.debug("X = %.3f, Y = %.3f, Radius = %.3f, start_angle = %.1f, stop_angle = %.1f, step_angle = %.3f" %
|
||||
# (center_x, center_y, radius, start_angle, end_angle, step_angle))
|
||||
|
||||
geo = LineString(point_list)
|
||||
return geo
|
||||
|
||||
|
||||
def dxfellipse2shapely(ellipse, ellipse_segments=100):
|
||||
# center = ellipse.dxf.center
|
||||
# start_angle = ellipse.dxf.start_param
|
||||
# end_angle = ellipse.dxf.end_param
|
||||
|
||||
ocs = ellipse.ocs()
|
||||
# if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
|
||||
if ellipse.dxf.extrusion != (0, 0, 1):
|
||||
center = ocs.to_wcs(ellipse.dxf.center)
|
||||
start_angle = ocs.to_wcs(ellipse.dxf.start_param)
|
||||
end_angle = ocs.to_wcs(ellipse.dxf.end_param)
|
||||
dir = 'CW'
|
||||
else:
|
||||
center = ellipse.dxf.center
|
||||
start_angle = ellipse.dxf.start_param
|
||||
end_angle = ellipse.dxf.end_param
|
||||
dir = 'CCW'
|
||||
|
||||
# print("Dir = %s" % dir)
|
||||
major_axis = ellipse.dxf.major_axis
|
||||
ratio = ellipse.dxf.ratio
|
||||
|
||||
points_list = []
|
||||
|
||||
major_axis = Vector(major_axis)
|
||||
|
||||
major_x = major_axis[0]
|
||||
major_y = major_axis[1]
|
||||
|
||||
if start_angle >= end_angle:
|
||||
end_angle += 2.0 * math.pi
|
||||
|
||||
line_seg = int((ellipse_segments * (end_angle - start_angle)) / math.pi)
|
||||
step_angle = abs(end_angle - start_angle) / float(line_seg)
|
||||
|
||||
angle = start_angle
|
||||
for step in range(line_seg + 1):
|
||||
if dir == 'CW':
|
||||
major_dim = normalize_2(major_axis)
|
||||
minor_dim = normalize_2(Vector([ratio * k for k in major_axis]))
|
||||
vx = (major_dim[0] + major_dim[1]) * math.cos(angle)
|
||||
vy = (minor_dim[0] - minor_dim[1]) * math.sin(angle)
|
||||
x = center[0] + major_x * vx - major_y * vy
|
||||
y = center[1] + major_y * vx + major_x * vy
|
||||
angle += step_angle
|
||||
else:
|
||||
major_dim = normalize_2(major_axis)
|
||||
minor_dim = (Vector([ratio * k for k in major_dim]))
|
||||
vx = (major_dim[0] + major_dim[1]) * math.cos(angle)
|
||||
vy = (minor_dim[0] + minor_dim[1]) * math.sin(angle)
|
||||
x = center[0] + major_x * vx + major_y * vy
|
||||
y = center[1] + major_y * vx + major_x * vy
|
||||
angle += step_angle
|
||||
|
||||
points_list.append((x, y))
|
||||
|
||||
geo = LineString(points_list)
|
||||
return geo
|
||||
|
||||
|
||||
def dxfpolyline2shapely(polyline):
|
||||
final_pts = []
|
||||
pts = polyline.points()
|
||||
for i in pts:
|
||||
final_pts.append((i[0], i[1]))
|
||||
if polyline.is_closed:
|
||||
final_pts.append(final_pts[0])
|
||||
|
||||
geo = LineString(final_pts)
|
||||
return geo
|
||||
|
||||
|
||||
def dxflwpolyline2shapely(lwpolyline):
|
||||
final_pts = []
|
||||
|
||||
for point in lwpolyline:
|
||||
x, y, _, _, _ = point
|
||||
final_pts.append((x, y))
|
||||
if lwpolyline.closed:
|
||||
final_pts.append(final_pts[0])
|
||||
|
||||
geo = LineString(final_pts)
|
||||
return geo
|
||||
|
||||
|
||||
def dxfsolid2shapely(solid):
|
||||
iterator = 0
|
||||
corner_list = []
|
||||
try:
|
||||
corner_list.append(solid[iterator])
|
||||
iterator += 1
|
||||
except:
|
||||
return Polygon(corner_list)
|
||||
|
||||
|
||||
def dxfspline2shapely(spline):
|
||||
with spline.edit_data() as spline_data:
|
||||
ctrl_points = spline_data.control_points
|
||||
knot_values = spline_data.knot_values
|
||||
is_closed = spline.closed
|
||||
degree = spline.dxf.degree
|
||||
|
||||
x_list, y_list, _ = spline2Polyline(ctrl_points, degree=degree, closed=is_closed, segments=20, knots=knot_values)
|
||||
points_list = zip(x_list, y_list)
|
||||
|
||||
geo = LineString(points_list)
|
||||
return geo
|
||||
|
||||
|
||||
def dxftrace2shapely(trace):
|
||||
iterator = 0
|
||||
corner_list = []
|
||||
try:
|
||||
corner_list.append(trace[iterator])
|
||||
iterator += 1
|
||||
except:
|
||||
return Polygon(corner_list)
|
||||
|
||||
|
||||
def getdxfgeo(dxf_object):
|
||||
|
||||
msp = dxf_object.modelspace()
|
||||
geos = get_geo(dxf_object, msp)
|
||||
|
||||
# geo_block = get_geo_from_block(dxf_object)
|
||||
|
||||
return geos
|
||||
|
||||
|
||||
def get_geo_from_insert(dxf_object, insert):
|
||||
geo_block_transformed = []
|
||||
|
||||
phi = insert.dxf.rotation
|
||||
tr = insert.dxf.insert
|
||||
sx = insert.dxf.xscale
|
||||
sy = insert.dxf.yscale
|
||||
r_count = insert.dxf.row_count
|
||||
r_spacing = insert.dxf.row_spacing
|
||||
c_count = insert.dxf.column_count
|
||||
c_spacing = insert.dxf.column_spacing
|
||||
|
||||
# print(phi, tr)
|
||||
|
||||
# identify the block given the 'INSERT' type entity name
|
||||
block = dxf_object.blocks[insert.dxf.name]
|
||||
block_coords = (block.block.dxf.base_point[0], block.block.dxf.base_point[1])
|
||||
|
||||
# get a list of geometries found in the block
|
||||
geo_block = get_geo(dxf_object, block)
|
||||
|
||||
# iterate over the geometries found and apply any transformation found in the 'INSERT' entity attributes
|
||||
for geo in geo_block:
|
||||
|
||||
# get the bounds of the geometry
|
||||
# minx, miny, maxx, maxy = geo.bounds
|
||||
|
||||
if tr[0] != 0 or tr[1] != 0:
|
||||
geo = translate(geo, (tr[0] - block_coords[0]), (tr[1] - block_coords[1]))
|
||||
|
||||
# support for array block insertions
|
||||
if r_count > 1:
|
||||
for r in range(r_count):
|
||||
geo_block_transformed.append(translate(geo, (tr[0] + (r * r_spacing) - block_coords[0]), 0))
|
||||
if c_count > 1:
|
||||
for c in range(c_count):
|
||||
geo_block_transformed.append(translate(geo, 0, (tr[1] + (c * c_spacing) - block_coords[1])))
|
||||
|
||||
if sx != 1 or sy != 1:
|
||||
geo = scale(geo, sx, sy)
|
||||
if phi != 0:
|
||||
geo = rotate(geo, phi, origin=tr)
|
||||
|
||||
geo_block_transformed.append(geo)
|
||||
return geo_block_transformed
|
||||
|
||||
|
||||
def get_geo(dxf_object, container):
|
||||
# store shapely geometry here
|
||||
geo = []
|
||||
|
||||
for dxf_entity in container:
|
||||
g = []
|
||||
# print("Entity", dxf_entity.dxftype())
|
||||
if dxf_entity.dxftype() == 'POINT':
|
||||
g = dxfpoint2shapely(dxf_entity,)
|
||||
elif dxf_entity.dxftype() == 'LINE':
|
||||
g = dxfline2shapely(dxf_entity,)
|
||||
elif dxf_entity.dxftype() == 'CIRCLE':
|
||||
g = dxfcircle2shapely(dxf_entity)
|
||||
elif dxf_entity.dxftype() == 'ARC':
|
||||
g = dxfarc2shapely(dxf_entity)
|
||||
elif dxf_entity.dxftype() == 'ELLIPSE':
|
||||
g = dxfellipse2shapely(dxf_entity)
|
||||
elif dxf_entity.dxftype() == 'LWPOLYLINE':
|
||||
g = dxflwpolyline2shapely(dxf_entity)
|
||||
elif dxf_entity.dxftype() == 'POLYLINE':
|
||||
g = dxfpolyline2shapely(dxf_entity)
|
||||
elif dxf_entity.dxftype() == 'SOLID':
|
||||
g = dxfsolid2shapely(dxf_entity)
|
||||
elif dxf_entity.dxftype() == 'TRACE':
|
||||
g = dxftrace2shapely(dxf_entity)
|
||||
elif dxf_entity.dxftype() == 'SPLINE':
|
||||
g = dxfspline2shapely(dxf_entity)
|
||||
elif dxf_entity.dxftype() == 'INSERT':
|
||||
g = get_geo_from_insert(dxf_object, dxf_entity)
|
||||
else:
|
||||
log.debug(" %s is not supported yet." % dxf_entity.dxftype())
|
||||
|
||||
if g is not None:
|
||||
if type(g) == list:
|
||||
for subg in g:
|
||||
geo.append(subg)
|
||||
else:
|
||||
geo.append(g)
|
||||
|
||||
return geo
|
||||
|
||||
def getdxftext(exf_object, object_type, units=None):
|
||||
pass
|
||||
|
||||
# def get_geo_from_block(dxf_object):
|
||||
# geo_block_transformed = []
|
||||
#
|
||||
# msp = dxf_object.modelspace()
|
||||
# # iterate through all 'INSERT' entities found in modelspace msp
|
||||
# for insert in msp.query('INSERT'):
|
||||
# phi = insert.dxf.rotation
|
||||
# tr = insert.dxf.insert
|
||||
# sx = insert.dxf.xscale
|
||||
# sy = insert.dxf.yscale
|
||||
# r_count = insert.dxf.row_count
|
||||
# r_spacing = insert.dxf.row_spacing
|
||||
# c_count = insert.dxf.column_count
|
||||
# c_spacing = insert.dxf.column_spacing
|
||||
#
|
||||
# # print(phi, tr)
|
||||
#
|
||||
# # identify the block given the 'INSERT' type entity name
|
||||
# print(insert.dxf.name)
|
||||
# block = dxf_object.blocks[insert.dxf.name]
|
||||
# block_coords = (block.block.dxf.base_point[0], block.block.dxf.base_point[1])
|
||||
#
|
||||
# # get a list of geometries found in the block
|
||||
# # store shapely geometry here
|
||||
# geo_block = []
|
||||
#
|
||||
# for dxf_entity in block:
|
||||
# g = []
|
||||
# # print("Entity", dxf_entity.dxftype())
|
||||
# if dxf_entity.dxftype() == 'POINT':
|
||||
# g = dxfpoint2shapely(dxf_entity, )
|
||||
# elif dxf_entity.dxftype() == 'LINE':
|
||||
# g = dxfline2shapely(dxf_entity, )
|
||||
# elif dxf_entity.dxftype() == 'CIRCLE':
|
||||
# g = dxfcircle2shapely(dxf_entity)
|
||||
# elif dxf_entity.dxftype() == 'ARC':
|
||||
# g = dxfarc2shapely(dxf_entity)
|
||||
# elif dxf_entity.dxftype() == 'ELLIPSE':
|
||||
# g = dxfellipse2shapely(dxf_entity)
|
||||
# elif dxf_entity.dxftype() == 'LWPOLYLINE':
|
||||
# g = dxflwpolyline2shapely(dxf_entity)
|
||||
# elif dxf_entity.dxftype() == 'POLYLINE':
|
||||
# g = dxfpolyline2shapely(dxf_entity)
|
||||
# elif dxf_entity.dxftype() == 'SOLID':
|
||||
# g = dxfsolid2shapely(dxf_entity)
|
||||
# elif dxf_entity.dxftype() == 'TRACE':
|
||||
# g = dxftrace2shapely(dxf_entity)
|
||||
# elif dxf_entity.dxftype() == 'SPLINE':
|
||||
# g = dxfspline2shapely(dxf_entity)
|
||||
# elif dxf_entity.dxftype() == 'INSERT':
|
||||
# log.debug("Not supported yet.")
|
||||
# else:
|
||||
# log.debug("Not supported yet.")
|
||||
#
|
||||
# if g is not None:
|
||||
# if type(g) == list:
|
||||
# for subg in g:
|
||||
# geo_block.append(subg)
|
||||
# else:
|
||||
# geo_block.append(g)
|
||||
#
|
||||
# # iterate over the geometries found and apply any transformation found in the 'INSERT' entity attributes
|
||||
# for geo in geo_block:
|
||||
# if tr[0] != 0 or tr[1] != 0:
|
||||
# geo = translate(geo, (tr[0] - block_coords[0]), (tr[1] - block_coords[1]))
|
||||
#
|
||||
# # support for array block insertions
|
||||
# if r_count > 1:
|
||||
# for r in range(r_count):
|
||||
# geo_block_transformed.append(translate(geo, (tr[0] + (r * r_spacing) - block_coords[0]), 0))
|
||||
#
|
||||
# if c_count > 1:
|
||||
# for c in range(c_count):
|
||||
# geo_block_transformed.append(translate(geo, 0, (tr[1] + (c * c_spacing) - block_coords[1])))
|
||||
#
|
||||
# if sx != 1 or sy != 1:
|
||||
# geo = scale(geo, sx, sy)
|
||||
# if phi != 0:
|
||||
# geo = rotate(geo, phi, origin=tr)
|
||||
#
|
||||
# geo_block_transformed.append(geo)
|
||||
# return geo_block_transformed
|
||||
816
flatcamParsers/ParseDXF_Spline.py
Normal file
816
flatcamParsers/ParseDXF_Spline.py
Normal file
@@ -0,0 +1,816 @@
|
||||
# Author: vvlachoudis@gmail.com
|
||||
# Vasilis Vlachoudis
|
||||
# Date: 20-Oct-2015
|
||||
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# File modified: Marius Adrian Stanciu #
|
||||
# Date: 3/10/2019 #
|
||||
############################################################
|
||||
|
||||
import math
|
||||
import sys
|
||||
|
||||
def norm(v):
|
||||
return math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
|
||||
|
||||
def normalize_2(v):
|
||||
m = norm(v)
|
||||
return [v[0]/m, v[1]/m, v[2]/m]
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Convert a B-spline to polyline with a fixed number of segments
|
||||
#
|
||||
# FIXME to become adaptive
|
||||
# ------------------------------------------------------------------------------
|
||||
def spline2Polyline(xyz, degree, closed, segments, knots):
|
||||
'''
|
||||
:param xyz: DXF spline control points
|
||||
:param degree: degree of the Spline curve
|
||||
:param closed: closed Spline
|
||||
:type closed: bool
|
||||
:param segments: how many lines to use for Spline approximation
|
||||
:param knots: DXF spline knots
|
||||
:return: x,y,z coordinates (each is a list)
|
||||
'''
|
||||
|
||||
# Check if last point coincide with the first one
|
||||
if (Vector(xyz[0]) - Vector(xyz[-1])).length2() < 1e-10:
|
||||
# it is already closed, treat it as open
|
||||
closed = False
|
||||
# FIXME we should verify if it is periodic,.... but...
|
||||
# I am not sure :)
|
||||
|
||||
if closed:
|
||||
xyz.extend(xyz[:degree])
|
||||
knots = None
|
||||
else:
|
||||
# make base-1
|
||||
knots.insert(0, 0)
|
||||
|
||||
npts = len(xyz)
|
||||
|
||||
if degree<1 or degree>3:
|
||||
#print "invalid degree"
|
||||
return None,None,None
|
||||
|
||||
# order:
|
||||
k = degree+1
|
||||
|
||||
if npts < k:
|
||||
#print "not enough control points"
|
||||
return None,None,None
|
||||
|
||||
# resolution:
|
||||
nseg = segments * npts
|
||||
|
||||
# WARNING: base 1
|
||||
b = [0.0]*(npts*3+1) # polygon points
|
||||
h = [1.0]*(npts+1) # set all homogeneous weighting factors to 1.0
|
||||
p = [0.0]*(nseg*3+1) # returned curved points
|
||||
|
||||
i = 1
|
||||
for pt in xyz:
|
||||
b[i] = pt[0]
|
||||
b[i+1] = pt[1]
|
||||
b[i+2] = pt[2]
|
||||
i +=3
|
||||
|
||||
#if periodic:
|
||||
if closed:
|
||||
_rbsplinu(npts, k, nseg, b, h, p, knots)
|
||||
else:
|
||||
_rbspline(npts, k, nseg, b, h, p, knots)
|
||||
|
||||
x = []
|
||||
y = []
|
||||
z = []
|
||||
for i in range(1,3*nseg+1,3):
|
||||
x.append(p[i])
|
||||
y.append(p[i+1])
|
||||
z.append(p[i+2])
|
||||
|
||||
# for i,xyz in enumerate(zip(x,y,z)):
|
||||
# print i,xyz
|
||||
|
||||
return x,y,z
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Subroutine to generate a B-spline open knot vector with multiplicity
|
||||
# equal to the order at the ends.
|
||||
# c = order of the basis function
|
||||
# n = the number of defining polygon vertices
|
||||
# n+2 = index of x[] for the first occurence of the maximum knot vector value
|
||||
# n+order = maximum value of the knot vector -- $n + c$
|
||||
# x[] = array containing the knot vector
|
||||
# ------------------------------------------------------------------------------
|
||||
def _knot(n, order):
|
||||
x = [0.0]*(n+order+1)
|
||||
for i in range(2, n+order+1):
|
||||
if i>order and i<n+2:
|
||||
x[i] = x[i-1] + 1.0
|
||||
else:
|
||||
x[i] = x[i-1]
|
||||
return x
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Subroutine to generate a B-spline uniform (periodic) knot vector.
|
||||
#
|
||||
# order = order of the basis function
|
||||
# n = the number of defining polygon vertices
|
||||
# n+order = maximum value of the knot vector -- $n + order$
|
||||
# x[] = array containing the knot vector
|
||||
# ------------------------------------------------------------------------------
|
||||
def _knotu(n, order):
|
||||
x = [0]*(n+order+1)
|
||||
for i in range(2, n+order+1):
|
||||
x[i] = float(i-1)
|
||||
return x
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Subroutine to generate rational B-spline basis functions--open knot vector
|
||||
|
||||
# C code for An Introduction to NURBS
|
||||
# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
|
||||
# All rights reserved.
|
||||
|
||||
# Name: rbasis
|
||||
# Subroutines called: none
|
||||
# Book reference: Chapter 4, Sec. 4. , p 296
|
||||
|
||||
# c = order of the B-spline basis function
|
||||
# d = first term of the basis function recursion relation
|
||||
# e = second term of the basis function recursion relation
|
||||
# h[] = array containing the homogeneous weights
|
||||
# npts = number of defining polygon vertices
|
||||
# nplusc = constant -- npts + c -- maximum number of knot values
|
||||
# r[] = array containing the rational basis functions
|
||||
# r[1] contains the basis function associated with B1 etc.
|
||||
# t = parameter value
|
||||
# temp[] = temporary array
|
||||
# x[] = knot vector
|
||||
# ------------------------------------------------------------------------------
|
||||
def _rbasis(c, t, npts, x, h, r):
|
||||
nplusc = npts + c
|
||||
temp = [0.0]*(nplusc+1)
|
||||
|
||||
# calculate the first order non-rational basis functions n[i]
|
||||
for i in range(1, nplusc):
|
||||
if x[i] <= t < x[i+1]:
|
||||
temp[i] = 1.0
|
||||
else:
|
||||
temp[i] = 0.0
|
||||
|
||||
# calculate the higher order non-rational basis functions
|
||||
for k in range(2,c+1):
|
||||
for i in range(1,nplusc-k+1):
|
||||
# if the lower order basis function is zero skip the calculation
|
||||
if temp[i] != 0.0:
|
||||
d = ((t-x[i])*temp[i])/(x[i+k-1]-x[i])
|
||||
else:
|
||||
d = 0.0
|
||||
|
||||
# if the lower order basis function is zero skip the calculation
|
||||
if temp[i+1] != 0.0:
|
||||
e = ((x[i+k]-t)*temp[i+1])/(x[i+k]-x[i+1])
|
||||
else:
|
||||
e = 0.0
|
||||
temp[i] = d + e
|
||||
|
||||
# pick up last point
|
||||
if t >= x[nplusc]:
|
||||
temp[npts] = 1.0
|
||||
|
||||
# calculate sum for denominator of rational basis functions
|
||||
s = 0.0
|
||||
for i in range(1,npts+1):
|
||||
s += temp[i]*h[i]
|
||||
|
||||
# form rational basis functions and put in r vector
|
||||
for i in range(1, npts+1):
|
||||
if s != 0.0:
|
||||
r[i] = (temp[i]*h[i])/s
|
||||
else:
|
||||
r[i] = 0
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Generates a rational B-spline curve using a uniform open knot vector.
|
||||
#
|
||||
# C code for An Introduction to NURBS
|
||||
# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
|
||||
# All rights reserved.
|
||||
#
|
||||
# Name: rbspline.c
|
||||
# Subroutines called: _knot, rbasis
|
||||
# Book reference: Chapter 4, Alg. p. 297
|
||||
#
|
||||
# b = array containing the defining polygon vertices
|
||||
# b[1] contains the x-component of the vertex
|
||||
# b[2] contains the y-component of the vertex
|
||||
# b[3] contains the z-component of the vertex
|
||||
# h = array containing the homogeneous weighting factors
|
||||
# k = order of the B-spline basis function
|
||||
# nbasis = array containing the basis functions for a single value of t
|
||||
# nplusc = number of knot values
|
||||
# npts = number of defining polygon vertices
|
||||
# p[,] = array containing the curve points
|
||||
# p[1] contains the x-component of the point
|
||||
# p[2] contains the y-component of the point
|
||||
# p[3] contains the z-component of the point
|
||||
# p1 = number of points to be calculated on the curve
|
||||
# t = parameter value 0 <= t <= npts - k + 1
|
||||
# x[] = array containing the knot vector
|
||||
# ------------------------------------------------------------------------------
|
||||
def _rbspline(npts, k, p1, b, h, p, x):
|
||||
nplusc = npts + k
|
||||
nbasis = [0.0]*(npts+1) # zero and re-dimension the basis array
|
||||
|
||||
# generate the uniform open knot vector
|
||||
if x is None or len(x) != nplusc+1:
|
||||
x = _knot(npts, k)
|
||||
icount = 0
|
||||
# calculate the points on the rational B-spline curve
|
||||
t = 0
|
||||
step = float(x[nplusc])/float(p1-1)
|
||||
for i1 in range(1, p1+1):
|
||||
if x[nplusc] - t < 5e-6:
|
||||
t = x[nplusc]
|
||||
# generate the basis function for this value of t
|
||||
nbasis = [0.0]*(npts+1) # zero and re-dimension the knot vector and the basis array
|
||||
_rbasis(k, t, npts, x, h, nbasis)
|
||||
# generate a point on the curve
|
||||
for j in range(1, 4):
|
||||
jcount = j
|
||||
p[icount+j] = 0.0
|
||||
# Do local matrix multiplication
|
||||
for i in range(1, npts+1):
|
||||
p[icount+j] += nbasis[i]*b[jcount]
|
||||
jcount += 3
|
||||
icount += 3
|
||||
t += step
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Subroutine to generate a rational B-spline curve using an uniform periodic knot vector
|
||||
#
|
||||
# C code for An Introduction to NURBS
|
||||
# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
|
||||
# All rights reserved.
|
||||
#
|
||||
# Name: rbsplinu.c
|
||||
# Subroutines called: _knotu, _rbasis
|
||||
# Book reference: Chapter 4, Alg. p. 298
|
||||
#
|
||||
# b[] = array containing the defining polygon vertices
|
||||
# b[1] contains the x-component of the vertex
|
||||
# b[2] contains the y-component of the vertex
|
||||
# b[3] contains the z-component of the vertex
|
||||
# h[] = array containing the homogeneous weighting factors
|
||||
# k = order of the B-spline basis function
|
||||
# nbasis = array containing the basis functions for a single value of t
|
||||
# nplusc = number of knot values
|
||||
# npts = number of defining polygon vertices
|
||||
# p[,] = array containing the curve points
|
||||
# p[1] contains the x-component of the point
|
||||
# p[2] contains the y-component of the point
|
||||
# p[3] contains the z-component of the point
|
||||
# p1 = number of points to be calculated on the curve
|
||||
# t = parameter value 0 <= t <= npts - k + 1
|
||||
# x[] = array containing the knot vector
|
||||
# ------------------------------------------------------------------------------
|
||||
def _rbsplinu(npts, k, p1, b, h, p, x=None):
|
||||
nplusc = npts + k
|
||||
nbasis = [0.0]*(npts+1) # zero and re-dimension the basis array
|
||||
# generate the uniform periodic knot vector
|
||||
if x is None or len(x) != nplusc+1:
|
||||
# zero and re dimension the knot vector and the basis array
|
||||
x = _knotu(npts, k)
|
||||
icount = 0
|
||||
# calculate the points on the rational B-spline curve
|
||||
t = k-1
|
||||
step = (float(npts)-(k-1))/float(p1-1)
|
||||
for i1 in range(1, p1+1):
|
||||
if x[nplusc] - t < 5e-6:
|
||||
t = x[nplusc]
|
||||
# generate the basis function for this value of t
|
||||
nbasis = [0.0]*(npts+1)
|
||||
_rbasis(k, t, npts, x, h, nbasis)
|
||||
# generate a point on the curve
|
||||
for j in range(1,4):
|
||||
jcount = j
|
||||
p[icount+j] = 0.0
|
||||
# Do local matrix multiplication
|
||||
for i in range(1,npts+1):
|
||||
p[icount+j] += nbasis[i]*b[jcount]
|
||||
jcount += 3
|
||||
icount += 3
|
||||
t += step
|
||||
|
||||
# Accuracy for comparison operators
|
||||
_accuracy = 1E-15
|
||||
|
||||
|
||||
def Cmp0(x):
|
||||
"""Compare against zero within _accuracy"""
|
||||
return abs(x)<_accuracy
|
||||
|
||||
|
||||
def gauss(A, B):
|
||||
"""Solve A*X = B using the Gauss elimination method"""
|
||||
|
||||
n = len(A)
|
||||
s = [0.0] * n
|
||||
X = [0.0] * n
|
||||
|
||||
p = [i for i in range(n)]
|
||||
for i in range(n):
|
||||
s[i] = max([abs(x) for x in A[i]])
|
||||
|
||||
for k in range(n - 1):
|
||||
# select j>=k so that
|
||||
# |A[p[j]][k]| / s[p[i]] >= |A[p[i]][k]| / s[p[i]] for i = k,k+1,...,n
|
||||
j = k
|
||||
ap = abs(A[p[j]][k]) / s[p[j]]
|
||||
for i in range(k + 1, n):
|
||||
api = abs(A[p[i]][k]) / s[p[i]]
|
||||
if api > ap:
|
||||
j = i
|
||||
ap = api
|
||||
|
||||
if j != k: p[k], p[j] = p[j], p[k] # Swap values
|
||||
|
||||
for i in range(k + 1, n):
|
||||
z = A[p[i]][k] / A[p[k]][k]
|
||||
A[p[i]][k] = z
|
||||
for j in range(k + 1, n):
|
||||
A[p[i]][j] -= z * A[p[k]][j]
|
||||
|
||||
for k in range(n - 1):
|
||||
for i in range(k + 1, n):
|
||||
B[p[i]] -= A[p[i]][k] * B[p[k]]
|
||||
|
||||
for i in range(n - 1, -1, -1):
|
||||
X[i] = B[p[i]]
|
||||
for j in range(i + 1, n):
|
||||
X[i] -= A[p[i]][j] * X[j]
|
||||
X[i] /= A[p[i]][i]
|
||||
|
||||
return X
|
||||
|
||||
|
||||
# Vector class
|
||||
# Inherits from List
|
||||
class Vector(list):
|
||||
"""Vector class"""
|
||||
|
||||
def __init__(self, x=3, *args):
|
||||
"""Create a new vector,
|
||||
Vector(size), Vector(list), Vector(x,y,z,...)"""
|
||||
list.__init__(self)
|
||||
|
||||
if isinstance(x, int) and not args:
|
||||
for i in range(x):
|
||||
self.append(0.0)
|
||||
elif isinstance(x, (list, tuple)):
|
||||
for i in x:
|
||||
self.append(float(i))
|
||||
else:
|
||||
self.append(float(x))
|
||||
for i in args:
|
||||
self.append(float(i))
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def set(self, x, y, z=None):
|
||||
"""Set vector"""
|
||||
self[0] = x
|
||||
self[1] = y
|
||||
if z: self[2] = z
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def __repr__(self):
|
||||
return "[%s]" % (", ".join([repr(x) for x in self]))
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def __str__(self):
|
||||
return "[%s]" % (", ".join([("%15g" % (x)).strip() for x in self]))
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def eq(self, v, acc=_accuracy):
|
||||
"""Test for equality with vector v within accuracy"""
|
||||
if len(self) != len(v): return False
|
||||
s2 = 0.0
|
||||
for a, b in zip(self, v):
|
||||
s2 += (a - b) ** 2
|
||||
return s2 <= acc ** 2
|
||||
|
||||
def __eq__(self, v):
|
||||
return self.eq(v)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def __neg__(self):
|
||||
"""Negate vector"""
|
||||
new = Vector(len(self))
|
||||
for i, s in enumerate(self):
|
||||
new[i] = -s
|
||||
return new
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def __add__(self, v):
|
||||
"""Add 2 vectors"""
|
||||
size = min(len(self), len(v))
|
||||
new = Vector(size)
|
||||
for i in range(size):
|
||||
new[i] = self[i] + v[i]
|
||||
return new
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def __iadd__(self, v):
|
||||
"""Add vector v to self"""
|
||||
for i in range(min(len(self), len(v))):
|
||||
self[i] += v[i]
|
||||
return self
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def __sub__(self, v):
|
||||
"""Subtract 2 vectors"""
|
||||
size = min(len(self), len(v))
|
||||
new = Vector(size)
|
||||
for i in range(size):
|
||||
new[i] = self[i] - v[i]
|
||||
return new
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def __isub__(self, v):
|
||||
"""Subtract vector v from self"""
|
||||
for i in range(min(len(self), len(v))):
|
||||
self[i] -= v[i]
|
||||
return self
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Scale or Dot product
|
||||
# ----------------------------------------------------------------------
|
||||
def __mul__(self, v):
|
||||
"""scale*Vector() or Vector()*Vector() - Scale vector or dot product"""
|
||||
if isinstance(v, list):
|
||||
return self.dot(v)
|
||||
else:
|
||||
return Vector([x * v for x in self])
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Scale or Dot product
|
||||
# ----------------------------------------------------------------------
|
||||
def __rmul__(self, v):
|
||||
"""scale*Vector() or Vector()*Vector() - Scale vector or dot product"""
|
||||
if isinstance(v, Vector):
|
||||
return self.dot(v)
|
||||
else:
|
||||
return Vector([x * v for x in self])
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Divide by floating point
|
||||
# ----------------------------------------------------------------------
|
||||
def __div__(self, b):
|
||||
return Vector([x / b for x in self])
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def __xor__(self, v):
|
||||
"""Cross product"""
|
||||
return self.cross(v)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def dot(self, v):
|
||||
"""Dot product of 2 vectors"""
|
||||
s = 0.0
|
||||
for a, b in zip(self, v):
|
||||
s += a * b
|
||||
return s
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def cross(self, v):
|
||||
"""Cross product of 2 vectors"""
|
||||
if len(self) == 3:
|
||||
return Vector(self[1] * v[2] - self[2] * v[1],
|
||||
self[2] * v[0] - self[0] * v[2],
|
||||
self[0] * v[1] - self[1] * v[0])
|
||||
elif len(self) == 2:
|
||||
return self[0] * v[1] - self[1] * v[0]
|
||||
else:
|
||||
raise Exception("Cross product needs 2d or 3d vectors")
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def length2(self):
|
||||
"""Return length squared of vector"""
|
||||
s2 = 0.0
|
||||
for s in self:
|
||||
s2 += s ** 2
|
||||
return s2
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def length(self):
|
||||
"""Return length of vector"""
|
||||
s2 = 0.0
|
||||
for s in self:
|
||||
s2 += s ** 2
|
||||
return math.sqrt(s2)
|
||||
|
||||
__abs__ = length
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def arg(self):
|
||||
"""return vector angle"""
|
||||
return math.atan2(self[1], self[0])
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def norm(self):
|
||||
"""Normalize vector and return length"""
|
||||
l = self.length()
|
||||
if l > 0.0:
|
||||
invlen = 1.0 / l
|
||||
for i in range(len(self)):
|
||||
self[i] *= invlen
|
||||
return l
|
||||
|
||||
normalize = norm
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def unit(self):
|
||||
"""return a unit vector"""
|
||||
v = self.clone()
|
||||
v.norm()
|
||||
return v
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def clone(self):
|
||||
"""Clone vector"""
|
||||
return Vector(self)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def x(self):
|
||||
return self[0]
|
||||
|
||||
def y(self):
|
||||
return self[1]
|
||||
|
||||
def z(self):
|
||||
return self[2]
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def orthogonal(self):
|
||||
"""return a vector orthogonal to self"""
|
||||
xx = abs(self.x())
|
||||
yy = abs(self.y())
|
||||
|
||||
if len(self) >= 3:
|
||||
zz = abs(self.z())
|
||||
if xx < yy:
|
||||
if xx < zz:
|
||||
return Vector(0.0, self.z(), -self.y())
|
||||
else:
|
||||
return Vector(self.y(), -self.x(), 0.0)
|
||||
else:
|
||||
if yy < zz:
|
||||
return Vector(-self.z(), 0.0, self.x())
|
||||
else:
|
||||
return Vector(self.y(), -self.x(), 0.0)
|
||||
else:
|
||||
return Vector(-self.y(), self.x())
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def direction(self, zero=_accuracy):
|
||||
"""return containing the direction if normalized with any of the axis"""
|
||||
|
||||
v = self.clone()
|
||||
l = v.norm()
|
||||
if abs(l) <= zero: return "O"
|
||||
|
||||
if abs(v[0] - 1.0) < zero:
|
||||
return "X"
|
||||
elif abs(v[0] + 1.0) < zero:
|
||||
return "-X"
|
||||
elif abs(v[1] - 1.0) < zero:
|
||||
return "Y"
|
||||
elif abs(v[1] + 1.0) < zero:
|
||||
return "-Y"
|
||||
elif abs(v[2] - 1.0) < zero:
|
||||
return "Z"
|
||||
elif abs(v[2] + 1.0) < zero:
|
||||
return "-Z"
|
||||
else:
|
||||
# nothing special about the direction, return N
|
||||
return "N"
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Set the vector directly in polar coordinates
|
||||
# @param ma magnitude of vector
|
||||
# @param ph azimuthal angle in radians
|
||||
# @param th polar angle in radians
|
||||
# ----------------------------------------------------------------------
|
||||
def setPolar(self, ma, ph, th):
|
||||
"""Set the vector directly in polar coordinates"""
|
||||
sf = math.sin(ph)
|
||||
cf = math.cos(ph)
|
||||
st = math.sin(th)
|
||||
ct = math.cos(th)
|
||||
self[0] = ma * st * cf
|
||||
self[1] = ma * st * sf
|
||||
self[2] = ma * ct
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def phi(self):
|
||||
"""return the azimuth angle."""
|
||||
if Cmp0(self.x()) and Cmp0(self.y()):
|
||||
return 0.0
|
||||
return math.atan2(self.y(), self.x())
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def theta(self):
|
||||
"""return the polar angle."""
|
||||
if Cmp0(self.x()) and Cmp0(self.y()) and Cmp0(self.z()):
|
||||
return 0.0
|
||||
return math.atan2(self.perp(), self.z())
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def cosTheta(self):
|
||||
"""return cosine of the polar angle."""
|
||||
ptot = self.length()
|
||||
if Cmp0(ptot):
|
||||
return 1.0
|
||||
else:
|
||||
return self.z() / ptot
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def perp2(self):
|
||||
"""return the transverse component squared
|
||||
(R^2 in cylindrical coordinate system)."""
|
||||
return self.x() * self.x() + self.y() * self.y()
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def perp(self):
|
||||
"""@return the transverse component
|
||||
(R in cylindrical coordinate system)."""
|
||||
return math.sqrt(self.perp2())
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Return a random 3D vector
|
||||
# ----------------------------------------------------------------------
|
||||
# @staticmethod
|
||||
# def random():
|
||||
# cosTheta = 2.0 * random.random() - 1.0
|
||||
# sinTheta = math.sqrt(1.0 - cosTheta ** 2)
|
||||
# phi = 2.0 * math.pi * random.random()
|
||||
# return Vector(math.cos(phi) * sinTheta, math.sin(phi) * sinTheta, cosTheta)
|
||||
|
||||
# #===============================================================================
|
||||
# # Cardinal cubic spline class
|
||||
# #===============================================================================
|
||||
# class CardinalSpline:
|
||||
# def __init__(self, A=0.5):
|
||||
# # The default matrix is the Catmull-Rom spline
|
||||
# # which is equal to Cardinal matrix
|
||||
# # for A = 0.5
|
||||
# #
|
||||
# # Note: Vasilis
|
||||
# # The A parameter should be the fraction in t where
|
||||
# # the second derivative is zero
|
||||
# self.setMatrix(A)
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# # Set the matrix according to Cardinal
|
||||
# #-----------------------------------------------------------------------
|
||||
# def setMatrix(self, A=0.5):
|
||||
# self.M = []
|
||||
# self.M.append([ -A, 2.-A, A-2., A ])
|
||||
# self.M.append([2.*A, A-3., 3.-2.*A, -A ])
|
||||
# self.M.append([ -A, 0., A, 0.])
|
||||
# self.M.append([ 0., 1., 0, 0.])
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# # Evaluate Cardinal spline at position t
|
||||
# # @param P list or tuple with 4 points y positions
|
||||
# # @param t [0..1] fraction of interval from points 1..2
|
||||
# # @param k index of starting 4 elements in P
|
||||
# # @return spline evaluation
|
||||
# #-----------------------------------------------------------------------
|
||||
# def __call__(self, P, t, k=1):
|
||||
# T = [t*t*t, t*t, t, 1.0]
|
||||
# R = [0.0]*4
|
||||
# for i in range(4):
|
||||
# for j in range(4):
|
||||
# R[i] += T[j] * self.M[j][i]
|
||||
# y = 0.0
|
||||
# for i in range(4):
|
||||
# y += R[i]*P[k+i-1]
|
||||
#
|
||||
# return y
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# # Return the coefficients of a 3rd degree polynomial
|
||||
# # f(x) = a t^3 + b t^2 + c t + d
|
||||
# # @return [a, b, c, d]
|
||||
# #-----------------------------------------------------------------------
|
||||
# def coefficients(self, P, k=1):
|
||||
# C = [0.0]*4
|
||||
# for i in range(4):
|
||||
# for j in range(4):
|
||||
# C[i] += self.M[i][j] * P[k+j-1]
|
||||
# return C
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# # Evaluate the value of the spline using the coefficients
|
||||
# #-----------------------------------------------------------------------
|
||||
# def evaluate(self, C, t):
|
||||
# return ((C[0]*t + C[1])*t + C[2])*t + C[3]
|
||||
#
|
||||
# #===============================================================================
|
||||
# # Cubic spline ensuring that the first and second derivative are continuous
|
||||
# # adapted from Penelope Manual Appending B.1
|
||||
# # It requires all the points (xi,yi) and the assumption on how to deal
|
||||
# # with the second derivative on the extremities
|
||||
# # Option 1: assume zero as second derivative on both ends
|
||||
# # Option 2: assume the same as the next or previous one
|
||||
# #===============================================================================
|
||||
# class CubicSpline:
|
||||
# def __init__(self, X, Y):
|
||||
# self.X = X
|
||||
# self.Y = Y
|
||||
# self.n = len(X)
|
||||
#
|
||||
# # Option #1
|
||||
# s1 = 0.0 # zero based = s0
|
||||
# sN = 0.0 # zero based = sN-1
|
||||
#
|
||||
# # Construct the tri-diagonal matrix
|
||||
# A = []
|
||||
# B = [0.0] * (self.n-2)
|
||||
# for i in range(self.n-2):
|
||||
# A.append([0.0] * (self.n-2))
|
||||
#
|
||||
# for i in range(1,self.n-1):
|
||||
# hi = self.h(i)
|
||||
# Hi = 2.0*(self.h(i-1) + hi)
|
||||
# j = i-1
|
||||
# A[j][j] = Hi
|
||||
# if i+1<self.n-1:
|
||||
# A[j][j+1] = A[j+1][j] = hi
|
||||
#
|
||||
# if i==1:
|
||||
# B[j] = 6.*(self.d(i) - self.d(j)) - hi*s1
|
||||
# elif i<self.n-2:
|
||||
# B[j] = 6.*(self.d(i) - self.d(j))
|
||||
# else:
|
||||
# B[j] = 6.*(self.d(i) - self.d(j)) - hi*sN
|
||||
#
|
||||
#
|
||||
# self.s = gauss(A,B)
|
||||
# self.s.insert(0,s1)
|
||||
# self.s.append(sN)
|
||||
# # print ">> s <<"
|
||||
# # pprint(self.s)
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# def h(self, i):
|
||||
# return self.X[i+1] - self.X[i]
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# def d(self, i):
|
||||
# return (self.Y[i+1] - self.Y[i]) / (self.X[i+1] - self.X[i])
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# def coefficients(self, i):
|
||||
# """return coefficients of cubic spline for interval i a*x**3+b*x**2+c*x+d"""
|
||||
# hi = self.h(i)
|
||||
# si = self.s[i]
|
||||
# si1 = self.s[i+1]
|
||||
# xi = self.X[i]
|
||||
# xi1 = self.X[i+1]
|
||||
# fi = self.Y[i]
|
||||
# fi1 = self.Y[i+1]
|
||||
#
|
||||
# a = 1./(6.*hi)*(si*xi1**3 - si1*xi**3 + 6.*(fi*xi1 - fi1*xi)) + hi/6.*(si1*xi - si*xi1)
|
||||
# b = 1./(2.*hi)*(si1*xi**2 - si*xi1**2 + 2*(fi1 - fi)) + hi/6.*(si - si1)
|
||||
# c = 1./(2.*hi)*(si*xi1 - si1*xi)
|
||||
# d = 1./(6.*hi)*(si1-si)
|
||||
#
|
||||
# return [d,c,b,a]
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# def __call__(self, i, x):
|
||||
# # FIXME should interpolate to find the interval
|
||||
# C = self.coefficients(i)
|
||||
# return ((C[0]*x + C[1])*x + C[2])*x + C[3]
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# # @return evaluation of cubic spline at x using coefficients C
|
||||
# #-----------------------------------------------------------------------
|
||||
# def evaluate(self, C, x):
|
||||
# return ((C[0]*x + C[1])*x + C[2])*x + C[3]
|
||||
#
|
||||
# #-----------------------------------------------------------------------
|
||||
# # Return evaluated derivative at x using coefficients C
|
||||
# #-----------------------------------------------------------------------
|
||||
# def derivative(self, C, x):
|
||||
# a = 3.0*C[0] # derivative coefficients
|
||||
# b = 2.0*C[1] # ... for sampling with rejection
|
||||
# c = C[2]
|
||||
# return (3.0*C[0]*x + 2.0*C[1])*x + C[2]
|
||||
#
|
||||
347
flatcamParsers/ParseFont.py
Normal file
347
flatcamParsers/ParseFont.py
Normal file
@@ -0,0 +1,347 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Date: 3/10/2019 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
#########################################################################
|
||||
### Borrowed code from 'https://github.com/gddc/ttfquery/blob/master/ ###
|
||||
### and made it work with Python 3 #############
|
||||
#########################################################################
|
||||
|
||||
import re, os, sys, glob
|
||||
import itertools
|
||||
|
||||
from shapely.geometry import Point, Polygon
|
||||
from shapely.affinity import translate, scale, rotate
|
||||
from shapely.geometry import MultiPolygon
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
|
||||
import freetype as ft
|
||||
from fontTools import ttLib
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('base2')
|
||||
|
||||
|
||||
class ParseFont():
|
||||
|
||||
FONT_SPECIFIER_NAME_ID = 4
|
||||
FONT_SPECIFIER_FAMILY_ID = 1
|
||||
|
||||
@staticmethod
|
||||
def get_win32_font_path():
|
||||
"""Get User-specific font directory on Win32"""
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
return os.path.join(os.environ['WINDIR'], 'Fonts')
|
||||
else:
|
||||
k = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
|
||||
try:
|
||||
# should check that k is valid? How?
|
||||
return winreg.QueryValueEx(k, "Fonts")[0]
|
||||
finally:
|
||||
winreg.CloseKey(k)
|
||||
|
||||
@staticmethod
|
||||
def get_linux_font_paths():
|
||||
"""Get system font directories on Linux/Unix
|
||||
|
||||
Uses /usr/sbin/chkfontpath to get the list
|
||||
of system-font directories, note that many
|
||||
of these will *not* be truetype font directories.
|
||||
|
||||
If /usr/sbin/chkfontpath isn't available, uses
|
||||
returns a set of common Linux/Unix paths
|
||||
"""
|
||||
executable = '/usr/sbin/chkfontpath'
|
||||
if os.path.isfile(executable):
|
||||
data = os.popen(executable).readlines()
|
||||
match = re.compile('\d+: (.+)')
|
||||
set = []
|
||||
for line in data:
|
||||
result = match.match(line)
|
||||
if result:
|
||||
set.append(result.group(1))
|
||||
return set
|
||||
else:
|
||||
directories = [
|
||||
# what seems to be the standard installation point
|
||||
"/usr/X11R6/lib/X11/fonts/TTF/",
|
||||
# common application, not really useful
|
||||
"/usr/lib/openoffice/share/fonts/truetype/",
|
||||
# documented as a good place to install new fonts...
|
||||
"/usr/share/fonts",
|
||||
"/usr/local/share/fonts",
|
||||
# seems to be where fonts are installed for an individual user?
|
||||
"~/.fonts",
|
||||
]
|
||||
|
||||
dir_set = []
|
||||
|
||||
for directory in directories:
|
||||
directory = directory = os.path.expanduser(os.path.expandvars(directory))
|
||||
try:
|
||||
if os.path.isdir(directory):
|
||||
for path, children, files in os.walk(directory):
|
||||
dir_set.append(path)
|
||||
except (IOError, OSError, TypeError, ValueError):
|
||||
pass
|
||||
return dir_set
|
||||
|
||||
@staticmethod
|
||||
def get_mac_font_paths():
|
||||
"""Get system font directories on MacOS
|
||||
"""
|
||||
directories = [
|
||||
# okay, now the OS X variants...
|
||||
"~/Library/Fonts/",
|
||||
"/Library/Fonts/",
|
||||
"/Network/Library/Fonts/",
|
||||
"/System/Library/Fonts/",
|
||||
"System Folder:Fonts:",
|
||||
]
|
||||
|
||||
dir_set = []
|
||||
|
||||
for directory in directories:
|
||||
directory = directory = os.path.expanduser(os.path.expandvars(directory))
|
||||
try:
|
||||
if os.path.isdir(directory):
|
||||
for path, children, files in os.walk(directory):
|
||||
dir_set.append(path)
|
||||
except (IOError, OSError, TypeError, ValueError):
|
||||
pass
|
||||
return dir_set
|
||||
|
||||
@staticmethod
|
||||
def get_win32_fonts(font_directory=None):
|
||||
"""Get list of explicitly *installed* font names"""
|
||||
|
||||
import winreg
|
||||
if font_directory is None:
|
||||
font_directory = ParseFont.get_win32_font_path()
|
||||
k = None
|
||||
|
||||
items = {}
|
||||
for keyName in (
|
||||
r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
|
||||
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts",
|
||||
):
|
||||
try:
|
||||
k = winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE,
|
||||
keyName
|
||||
)
|
||||
except OSError as err:
|
||||
pass
|
||||
|
||||
if not k:
|
||||
# couldn't open either WinNT or Win98 key???
|
||||
return glob.glob(os.path.join(font_directory, '*.ttf'))
|
||||
|
||||
try:
|
||||
# should check that k is valid? How?
|
||||
for index in range(winreg.QueryInfoKey(k)[1]):
|
||||
key, value, _ = winreg.EnumValue(k, index)
|
||||
if not os.path.dirname(value):
|
||||
value = os.path.join(font_directory, value)
|
||||
value = os.path.abspath(value).lower()
|
||||
if value[-4:] == '.ttf':
|
||||
items[value] = 1
|
||||
return list(items.keys())
|
||||
finally:
|
||||
winreg.CloseKey(k)
|
||||
|
||||
@staticmethod
|
||||
def get_font_name(font_path):
|
||||
"""
|
||||
Get the short name from the font's names table
|
||||
From 'https://github.com/gddc/ttfquery/blob/master/ttfquery/describe.py'
|
||||
and
|
||||
http://www.starrhorne.com/2012/01/18/
|
||||
how-to-extract-font-names-from-ttf-files-using-python-and-our-old-friend-the-command-line.html
|
||||
ported to Python 3 here: https://gist.github.com/pklaus/dce37521579513c574d0
|
||||
"""
|
||||
name = ""
|
||||
family = ""
|
||||
|
||||
font = ttLib.TTFont(font_path)
|
||||
|
||||
for record in font['name'].names:
|
||||
if b'\x00' in record.string:
|
||||
name_str = record.string.decode('utf-16-be')
|
||||
else:
|
||||
# name_str = record.string.decode('utf-8')
|
||||
name_str = record.string.decode('latin-1')
|
||||
|
||||
if record.nameID == ParseFont.FONT_SPECIFIER_NAME_ID and not name:
|
||||
name = name_str
|
||||
elif record.nameID == ParseFont.FONT_SPECIFIER_FAMILY_ID and not family:
|
||||
family = name_str
|
||||
|
||||
if name and family:
|
||||
break
|
||||
return name, family
|
||||
|
||||
def __init__(self, app, parent=None):
|
||||
super(ParseFont, self).__init__()
|
||||
|
||||
self.app = app
|
||||
|
||||
# regular fonts
|
||||
self.regular_f = {}
|
||||
# bold fonts
|
||||
self.bold_f = {}
|
||||
# italic fonts
|
||||
self.italic_f = {}
|
||||
# bold and italic fonts
|
||||
self.bold_italic_f = {}
|
||||
|
||||
def get_fonts(self, paths=None):
|
||||
"""
|
||||
Find fonts in paths, or the system paths if not given
|
||||
"""
|
||||
files = {}
|
||||
if paths is None:
|
||||
if sys.platform == 'win32':
|
||||
font_directory = ParseFont.get_win32_font_path()
|
||||
paths = [font_directory,]
|
||||
|
||||
# now get all installed fonts directly...
|
||||
for f in self.get_win32_fonts(font_directory):
|
||||
files[f] = 1
|
||||
elif sys.platform == 'linux':
|
||||
paths = ParseFont.get_linux_font_paths()
|
||||
else:
|
||||
paths = ParseFont.get_mac_font_paths()
|
||||
elif isinstance(paths, str):
|
||||
paths = [paths]
|
||||
|
||||
for path in paths:
|
||||
for file in glob.glob(os.path.join(path, '*.ttf')):
|
||||
files[os.path.abspath(file)] = 1
|
||||
|
||||
return list(files.keys())
|
||||
|
||||
def get_fonts_by_types(self):
|
||||
|
||||
system_fonts = self.get_fonts()
|
||||
|
||||
# split the installed fonts by type: regular, bold, italic (oblique), bold-italic and
|
||||
# store them in separate dictionaries {name: file_path/filename.ttf}
|
||||
for font in system_fonts:
|
||||
try:
|
||||
name, family = ParseFont.get_font_name(font)
|
||||
except Exception as e:
|
||||
log.debug("ParseFont.get_fonts_by_types() --> Could not get the font name. %s" % str(e))
|
||||
continue
|
||||
|
||||
if 'Bold' in name and 'Italic' in name:
|
||||
name = name.replace(" Bold Italic", '')
|
||||
self.bold_italic_f.update({name: font})
|
||||
elif 'Bold' in name and 'Oblique' in name:
|
||||
name = name.replace(" Bold Oblique", '')
|
||||
self.bold_italic_f.update({name: font})
|
||||
elif 'Bold' in name:
|
||||
name = name.replace(" Bold", '')
|
||||
self.bold_f.update({name: font})
|
||||
elif 'SemiBold' in name:
|
||||
name = name.replace(" SemiBold", '')
|
||||
self.bold_f.update({name: font})
|
||||
elif 'DemiBold' in name:
|
||||
name = name.replace(" DemiBold", '')
|
||||
self.bold_f.update({name: font})
|
||||
elif 'Demi' in name:
|
||||
name = name.replace(" Demi", '')
|
||||
self.bold_f.update({name: font})
|
||||
elif 'Italic' in name:
|
||||
name = name.replace(" Italic", '')
|
||||
self.italic_f.update({name: font})
|
||||
elif 'Oblique' in name:
|
||||
name = name.replace(" Italic", '')
|
||||
self.italic_f.update({name: font})
|
||||
else:
|
||||
try:
|
||||
name = name.replace(" Regular", '')
|
||||
except:
|
||||
pass
|
||||
self.regular_f.update({name: font})
|
||||
log.debug("Font parsing is finished.")
|
||||
|
||||
def font_to_geometry(self, char_string, font_name, font_type, font_size, units='MM', coordx=0, coordy=0):
|
||||
path = []
|
||||
scaled_path = []
|
||||
path_filename = ""
|
||||
|
||||
regular_dict = self.regular_f
|
||||
bold_dict = self.bold_f
|
||||
italic_dict = self.italic_f
|
||||
bold_italic_dict = self.bold_italic_f
|
||||
|
||||
try:
|
||||
if font_type == 'bi':
|
||||
path_filename = bold_italic_dict[font_name]
|
||||
elif font_type == 'bold':
|
||||
path_filename = bold_dict[font_name]
|
||||
elif font_type == 'italic':
|
||||
path_filename = italic_dict[font_name]
|
||||
elif font_type == 'regular':
|
||||
path_filename = regular_dict[font_name]
|
||||
except Exception as e:
|
||||
self.app.inform.emit("[ERROR_NOTCL] Font not supported, try another one.")
|
||||
log.debug("[ERROR_NOTCL] Font Loading: %s" % str(e))
|
||||
return "flatcam font parse failed"
|
||||
|
||||
face = ft.Face(path_filename)
|
||||
face.set_char_size(int(font_size) * 64)
|
||||
|
||||
pen_x = coordx
|
||||
previous = 0
|
||||
|
||||
# done as here: https://www.freetype.org/freetype2/docs/tutorial/step2.html
|
||||
for char in char_string:
|
||||
glyph_index = face.get_char_index(char)
|
||||
|
||||
try:
|
||||
if previous > 0 and glyph_index > 0:
|
||||
delta = face.get_kerning(previous, glyph_index)
|
||||
pen_x += delta.x
|
||||
except:
|
||||
pass
|
||||
|
||||
face.load_glyph(glyph_index)
|
||||
# face.load_char(char, flags=8)
|
||||
|
||||
slot = face.glyph
|
||||
outline = slot.outline
|
||||
|
||||
start, end = 0, 0
|
||||
for i in range(len(outline.contours)):
|
||||
end = outline.contours[i]
|
||||
points = outline.points[start:end + 1]
|
||||
points.append(points[0])
|
||||
|
||||
char_geo = Polygon(points)
|
||||
char_geo = translate(char_geo, xoff=pen_x, yoff=coordy)
|
||||
|
||||
path.append(char_geo)
|
||||
|
||||
start = end + 1
|
||||
|
||||
pen_x += slot.advance.x
|
||||
previous = glyph_index
|
||||
|
||||
for item in path:
|
||||
if units == 'MM':
|
||||
scaled_path.append(scale(item, 0.0080187969924812, 0.0080187969924812, origin=(coordx, coordy)))
|
||||
else:
|
||||
scaled_path.append(scale(item, 0.00031570066, 0.00031570066, origin=(coordx, coordy)))
|
||||
|
||||
return MultiPolygon(scaled_path)
|
||||
648
flatcamParsers/ParseSVG.py
Normal file
648
flatcamParsers/ParseSVG.py
Normal file
@@ -0,0 +1,648 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# Author: Juan Pablo Caram (c) #
|
||||
# Date: 12/18/2015 #
|
||||
# MIT Licence #
|
||||
# #
|
||||
# SVG Features supported: #
|
||||
# * Groups #
|
||||
# * Rectangles (w/ rounded corners) #
|
||||
# * Circles #
|
||||
# * Ellipses #
|
||||
# * Polygons #
|
||||
# * Polylines #
|
||||
# * Lines #
|
||||
# * Paths #
|
||||
# * All transformations #
|
||||
# #
|
||||
# Reference: www.w3.org/TR/SVG/Overview.html #
|
||||
############################################################
|
||||
|
||||
# import xml.etree.ElementTree as ET
|
||||
from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
|
||||
from svg.path.path import Move
|
||||
from shapely.geometry import LineString
|
||||
from shapely.affinity import skew, affine_transform
|
||||
import numpy as np
|
||||
|
||||
from flatcamParsers.ParseFont import *
|
||||
|
||||
log = logging.getLogger('base2')
|
||||
|
||||
|
||||
def svgparselength(lengthstr):
|
||||
"""
|
||||
Parse an SVG length string into a float and a units
|
||||
string, if any.
|
||||
|
||||
:param lengthstr: SVG length string.
|
||||
:return: Number and units pair.
|
||||
:rtype: tuple(float, str|None)
|
||||
"""
|
||||
|
||||
integer_re_str = r'[+-]?[0-9]+'
|
||||
number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
|
||||
r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
|
||||
length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?'
|
||||
|
||||
match = re.search(length_re_str, lengthstr)
|
||||
if match:
|
||||
return float(match.group(1)), match.group(2)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def path2shapely(path, object_type, res=1.0):
|
||||
"""
|
||||
Converts an svg.path.Path into a Shapely
|
||||
LinearRing or LinearString.
|
||||
|
||||
:rtype : LinearRing
|
||||
:rtype : LineString
|
||||
:param path: svg.path.Path instance
|
||||
:param res: Resolution (minimum step along path)
|
||||
:return: Shapely geometry object
|
||||
"""
|
||||
|
||||
points = []
|
||||
geometry = []
|
||||
geo_element = None
|
||||
|
||||
for component in path:
|
||||
|
||||
# Line
|
||||
if isinstance(component, Line):
|
||||
start = component.start
|
||||
x, y = start.real, start.imag
|
||||
if len(points) == 0 or points[-1] != (x, y):
|
||||
points.append((x, y))
|
||||
end = component.end
|
||||
points.append((end.real, end.imag))
|
||||
continue
|
||||
|
||||
# Arc, CubicBezier or QuadraticBezier
|
||||
if isinstance(component, Arc) or \
|
||||
isinstance(component, CubicBezier) or \
|
||||
isinstance(component, QuadraticBezier):
|
||||
|
||||
# How many points to use in the discrete representation.
|
||||
length = component.length(res / 10.0)
|
||||
steps = int(length / res + 0.5)
|
||||
|
||||
# solve error when step is below 1,
|
||||
# it may cause other problems, but LineString needs at least two points
|
||||
if steps == 0:
|
||||
steps = 1
|
||||
|
||||
frac = 1.0 / steps
|
||||
|
||||
# print length, steps, frac
|
||||
for i in range(steps):
|
||||
point = component.point(i * frac)
|
||||
x, y = point.real, point.imag
|
||||
if len(points) == 0 or points[-1] != (x, y):
|
||||
points.append((x, y))
|
||||
end = component.point(1.0)
|
||||
points.append((end.real, end.imag))
|
||||
continue
|
||||
|
||||
# Move
|
||||
if isinstance(component, Move):
|
||||
if object_type == 'geometry':
|
||||
geo_element = LineString(points)
|
||||
elif object_type == 'gerber':
|
||||
# Didn't worked out using Polygon because if there is a large outline it will envelope everything
|
||||
# and create issues with intersections. I will let the parameter obj_type present though
|
||||
# geo_element = Polygon(points)
|
||||
geo_element = LineString(points)
|
||||
else:
|
||||
log.error("[ERROR]: Not a valid target object.")
|
||||
if not points:
|
||||
continue
|
||||
else:
|
||||
geometry.append(geo_element)
|
||||
points = []
|
||||
continue
|
||||
log.warning("I don't know what this is: %s" % str(component))
|
||||
continue
|
||||
|
||||
# if there are still points in points then add them as a LineString
|
||||
if points:
|
||||
geo_element = LineString(points)
|
||||
geometry.append(geo_element)
|
||||
points = []
|
||||
|
||||
# if path.closed:
|
||||
# return Polygon(points).buffer(0)
|
||||
# # return LinearRing(points)
|
||||
# else:
|
||||
# return LineString(points)
|
||||
return geometry
|
||||
|
||||
def svgrect2shapely(rect, n_points=32):
|
||||
"""
|
||||
Converts an SVG rect into Shapely geometry.
|
||||
|
||||
:param rect: Rect Element
|
||||
:type rect: xml.etree.ElementTree.Element
|
||||
:return: shapely.geometry.polygon.LinearRing
|
||||
"""
|
||||
w = svgparselength(rect.get('width'))[0]
|
||||
h = svgparselength(rect.get('height'))[0]
|
||||
x_obj = rect.get('x')
|
||||
if x_obj is not None:
|
||||
x = svgparselength(x_obj)[0]
|
||||
else:
|
||||
x = 0
|
||||
y_obj = rect.get('y')
|
||||
if y_obj is not None:
|
||||
y = svgparselength(y_obj)[0]
|
||||
else:
|
||||
y = 0
|
||||
rxstr = rect.get('rx')
|
||||
rystr = rect.get('ry')
|
||||
|
||||
if rxstr is None and rystr is None: # Sharp corners
|
||||
pts = [
|
||||
(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
|
||||
]
|
||||
|
||||
else: # Rounded corners
|
||||
rx = 0.0 if rxstr is None else svgparselength(rxstr)[0]
|
||||
ry = 0.0 if rystr is None else svgparselength(rystr)[0]
|
||||
|
||||
n_points = int(n_points / 4 + 0.5)
|
||||
t = np.arange(n_points, dtype=float) / n_points / 4
|
||||
|
||||
x_ = (x + w - rx) + rx * np.cos(2 * np.pi * (t + 0.75))
|
||||
y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.75))
|
||||
|
||||
lower_right = [(x_[i], y_[i]) for i in range(n_points)]
|
||||
|
||||
x_ = (x + w - rx) + rx * np.cos(2 * np.pi * t)
|
||||
y_ = (y + h - ry) + ry * np.sin(2 * np.pi * t)
|
||||
|
||||
upper_right = [(x_[i], y_[i]) for i in range(n_points)]
|
||||
|
||||
x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.25))
|
||||
y_ = (y + h - ry) + ry * np.sin(2 * np.pi * (t + 0.25))
|
||||
|
||||
upper_left = [(x_[i], y_[i]) for i in range(n_points)]
|
||||
|
||||
x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.5))
|
||||
y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.5))
|
||||
|
||||
lower_left = [(x_[i], y_[i]) for i in range(n_points)]
|
||||
|
||||
pts = [(x + rx, y), (x - rx + w, y)] + \
|
||||
lower_right + \
|
||||
[(x + w, y + ry), (x + w, y + h - ry)] + \
|
||||
upper_right + \
|
||||
[(x + w - rx, y + h), (x + rx, y + h)] + \
|
||||
upper_left + \
|
||||
[(x, y + h - ry), (x, y + ry)] + \
|
||||
lower_left
|
||||
|
||||
return Polygon(pts).buffer(0)
|
||||
# return LinearRing(pts)
|
||||
|
||||
|
||||
def svgcircle2shapely(circle):
|
||||
"""
|
||||
Converts an SVG circle into Shapely geometry.
|
||||
|
||||
:param circle: Circle Element
|
||||
:type circle: xml.etree.ElementTree.Element
|
||||
:return: Shapely representation of the circle.
|
||||
:rtype: shapely.geometry.polygon.LinearRing
|
||||
"""
|
||||
# cx = float(circle.get('cx'))
|
||||
# cy = float(circle.get('cy'))
|
||||
# r = float(circle.get('r'))
|
||||
cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet
|
||||
cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet
|
||||
r = svgparselength(circle.get('r'))[0] # TODO: No units support yet
|
||||
|
||||
# TODO: No resolution specified.
|
||||
return Point(cx, cy).buffer(r)
|
||||
|
||||
|
||||
def svgellipse2shapely(ellipse, n_points=64):
|
||||
"""
|
||||
Converts an SVG ellipse into Shapely geometry
|
||||
|
||||
:param ellipse: Ellipse Element
|
||||
:type ellipse: xml.etree.ElementTree.Element
|
||||
:param n_points: Number of discrete points in output.
|
||||
:return: Shapely representation of the ellipse.
|
||||
:rtype: shapely.geometry.polygon.LinearRing
|
||||
"""
|
||||
|
||||
cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet
|
||||
cy = svgparselength(ellipse.get('cy'))[0] # TODO: No units support yet
|
||||
|
||||
rx = svgparselength(ellipse.get('rx'))[0] # TODO: No units support yet
|
||||
ry = svgparselength(ellipse.get('ry'))[0] # TODO: No units support yet
|
||||
|
||||
t = np.arange(n_points, dtype=float) / n_points
|
||||
x = cx + rx * np.cos(2 * np.pi * t)
|
||||
y = cy + ry * np.sin(2 * np.pi * t)
|
||||
pts = [(x[i], y[i]) for i in range(n_points)]
|
||||
|
||||
return Polygon(pts).buffer(0)
|
||||
# return LinearRing(pts)
|
||||
|
||||
|
||||
def svgline2shapely(line):
|
||||
"""
|
||||
|
||||
:param line: Line element
|
||||
:type line: xml.etree.ElementTree.Element
|
||||
:return: Shapely representation on the line.
|
||||
:rtype: shapely.geometry.polygon.LinearRing
|
||||
"""
|
||||
|
||||
x1 = svgparselength(line.get('x1'))[0]
|
||||
y1 = svgparselength(line.get('y1'))[0]
|
||||
x2 = svgparselength(line.get('x2'))[0]
|
||||
y2 = svgparselength(line.get('y2'))[0]
|
||||
|
||||
return LineString([(x1, y1), (x2, y2)])
|
||||
|
||||
|
||||
def svgpolyline2shapely(polyline):
|
||||
|
||||
ptliststr = polyline.get('points')
|
||||
points = parse_svg_point_list(ptliststr)
|
||||
|
||||
return LineString(points)
|
||||
|
||||
|
||||
def svgpolygon2shapely(polygon):
|
||||
|
||||
ptliststr = polygon.get('points')
|
||||
points = parse_svg_point_list(ptliststr)
|
||||
|
||||
return Polygon(points).buffer(0)
|
||||
# return LinearRing(points)
|
||||
|
||||
|
||||
def getsvggeo(node, object_type):
|
||||
"""
|
||||
Extracts and flattens all geometry from an SVG node
|
||||
into a list of Shapely geometry.
|
||||
|
||||
:param node: xml.etree.ElementTree.Element
|
||||
:return: List of Shapely geometry
|
||||
:rtype: list
|
||||
"""
|
||||
kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
|
||||
geo = []
|
||||
|
||||
# Recurse
|
||||
if len(node) > 0:
|
||||
for child in node:
|
||||
subgeo = getsvggeo(child, object_type)
|
||||
if subgeo is not None:
|
||||
geo += subgeo
|
||||
|
||||
# Parse
|
||||
elif kind == 'path':
|
||||
log.debug("***PATH***")
|
||||
P = parse_path(node.get('d'))
|
||||
P = path2shapely(P, object_type)
|
||||
# for path, the resulting geometry is already a list so no need to create a new one
|
||||
geo = P
|
||||
|
||||
elif kind == 'rect':
|
||||
log.debug("***RECT***")
|
||||
R = svgrect2shapely(node)
|
||||
geo = [R]
|
||||
|
||||
elif kind == 'circle':
|
||||
log.debug("***CIRCLE***")
|
||||
C = svgcircle2shapely(node)
|
||||
geo = [C]
|
||||
|
||||
elif kind == 'ellipse':
|
||||
log.debug("***ELLIPSE***")
|
||||
E = svgellipse2shapely(node)
|
||||
geo = [E]
|
||||
|
||||
elif kind == 'polygon':
|
||||
log.debug("***POLYGON***")
|
||||
poly = svgpolygon2shapely(node)
|
||||
geo = [poly]
|
||||
|
||||
elif kind == 'line':
|
||||
log.debug("***LINE***")
|
||||
line = svgline2shapely(node)
|
||||
geo = [line]
|
||||
|
||||
elif kind == 'polyline':
|
||||
log.debug("***POLYLINE***")
|
||||
pline = svgpolyline2shapely(node)
|
||||
geo = [pline]
|
||||
|
||||
else:
|
||||
log.warning("Unknown kind: " + kind)
|
||||
geo = None
|
||||
|
||||
# ignore transformation for unknown kind
|
||||
if geo is not None:
|
||||
# Transformations
|
||||
if 'transform' in node.attrib:
|
||||
trstr = node.get('transform')
|
||||
trlist = parse_svg_transform(trstr)
|
||||
# log.debug(trlist)
|
||||
|
||||
# Transformations are applied in reverse order
|
||||
for tr in trlist[::-1]:
|
||||
if tr[0] == 'translate':
|
||||
geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
|
||||
elif tr[0] == 'scale':
|
||||
geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
|
||||
for geoi in geo]
|
||||
elif tr[0] == 'rotate':
|
||||
geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
|
||||
for geoi in geo]
|
||||
elif tr[0] == 'skew':
|
||||
geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
|
||||
for geoi in geo]
|
||||
elif tr[0] == 'matrix':
|
||||
geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
|
||||
else:
|
||||
raise Exception('Unknown transformation: %s', tr)
|
||||
|
||||
return geo
|
||||
|
||||
|
||||
def getsvgtext(node, object_type, units='MM'):
|
||||
"""
|
||||
Extracts and flattens all geometry from an SVG node
|
||||
into a list of Shapely geometry.
|
||||
|
||||
:param node: xml.etree.ElementTree.Element
|
||||
:return: List of Shapely geometry
|
||||
:rtype: list
|
||||
"""
|
||||
kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
|
||||
geo = []
|
||||
|
||||
# Recurse
|
||||
if len(node) > 0:
|
||||
for child in node:
|
||||
subgeo = getsvgtext(child, object_type, units=units)
|
||||
if subgeo is not None:
|
||||
geo += subgeo
|
||||
|
||||
# Parse
|
||||
elif kind == 'tspan':
|
||||
current_attrib = node.attrib
|
||||
txt = node.text
|
||||
style_dict = {}
|
||||
parrent_attrib = node.getparent().attrib
|
||||
style = parrent_attrib['style']
|
||||
|
||||
try:
|
||||
style_list = style.split(';')
|
||||
for css in style_list:
|
||||
style_dict[css.rpartition(':')[0]] = css.rpartition(':')[-1]
|
||||
|
||||
pos_x = float(current_attrib['x'])
|
||||
pos_y = float(current_attrib['y'])
|
||||
|
||||
# should have used the instance from FlatCAMApp.App but how? without reworking everything ...
|
||||
pf = ParseFont()
|
||||
pf.get_fonts_by_types()
|
||||
font_name = style_dict['font-family'].replace("'", '')
|
||||
|
||||
if style_dict['font-style'] == 'italic' and style_dict['font-weight'] == 'bold':
|
||||
font_type = 'bi'
|
||||
elif style_dict['font-weight'] == 'bold':
|
||||
font_type = 'bold'
|
||||
elif style_dict['font-style'] == 'italic':
|
||||
font_type = 'italic'
|
||||
else:
|
||||
font_type = 'regular'
|
||||
|
||||
# value of 2.2 should have been 2.83 (conversion value from pixels to points)
|
||||
# but the dimensions from Inkscape did not corelate with the ones after importing in FlatCAM
|
||||
# so I adjusted this
|
||||
font_size = svgparselength(style_dict['font-size'])[0] * 2.2
|
||||
geo = [pf.font_to_geometry(txt,
|
||||
font_name=font_name,
|
||||
font_size=font_size,
|
||||
font_type=font_type,
|
||||
units=units,
|
||||
coordx=pos_x,
|
||||
coordy=pos_y)
|
||||
]
|
||||
|
||||
geo = [(scale(g, 1.0, -1.0)) for g in geo]
|
||||
except Exception as e:
|
||||
log.debug(str(e))
|
||||
else:
|
||||
geo = None
|
||||
|
||||
# ignore transformation for unknown kind
|
||||
if geo is not None:
|
||||
# Transformations
|
||||
if 'transform' in node.attrib:
|
||||
trstr = node.get('transform')
|
||||
trlist = parse_svg_transform(trstr)
|
||||
# log.debug(trlist)
|
||||
|
||||
# Transformations are applied in reverse order
|
||||
for tr in trlist[::-1]:
|
||||
if tr[0] == 'translate':
|
||||
geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
|
||||
elif tr[0] == 'scale':
|
||||
geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
|
||||
for geoi in geo]
|
||||
elif tr[0] == 'rotate':
|
||||
geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
|
||||
for geoi in geo]
|
||||
elif tr[0] == 'skew':
|
||||
geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
|
||||
for geoi in geo]
|
||||
elif tr[0] == 'matrix':
|
||||
geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
|
||||
else:
|
||||
raise Exception('Unknown transformation: %s', tr)
|
||||
|
||||
return geo
|
||||
|
||||
def parse_svg_point_list(ptliststr):
|
||||
"""
|
||||
Returns a list of coordinate pairs extracted from the "points"
|
||||
attribute in SVG polygons and polyline's.
|
||||
|
||||
:param ptliststr: "points" attribute string in polygon or polyline.
|
||||
:return: List of tuples with coordinates.
|
||||
"""
|
||||
|
||||
pairs = []
|
||||
last = None
|
||||
pos = 0
|
||||
i = 0
|
||||
|
||||
for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr.strip(' ')):
|
||||
|
||||
val = float(ptliststr[pos:match.start()])
|
||||
|
||||
if i % 2 == 1:
|
||||
pairs.append((last, val))
|
||||
else:
|
||||
last = val
|
||||
|
||||
pos = match.end()
|
||||
i += 1
|
||||
|
||||
# Check for last element
|
||||
val = float(ptliststr[pos:])
|
||||
if i % 2 == 1:
|
||||
pairs.append((last, val))
|
||||
else:
|
||||
log.warning("Incomplete coordinates.")
|
||||
|
||||
return pairs
|
||||
|
||||
|
||||
def parse_svg_transform(trstr):
|
||||
"""
|
||||
Parses an SVG transform string into a list
|
||||
of transform names and their parameters.
|
||||
|
||||
Possible transformations are:
|
||||
|
||||
* Translate: translate(<tx> [<ty>]), which specifies
|
||||
a translation by tx and ty. If <ty> is not provided,
|
||||
it is assumed to be zero. Result is
|
||||
['translate', tx, ty]
|
||||
|
||||
* Scale: scale(<sx> [<sy>]), which specifies a scale operation
|
||||
by sx and sy. If <sy> is not provided, it is assumed to be
|
||||
equal to <sx>. Result is: ['scale', sx, sy]
|
||||
|
||||
* Rotate: rotate(<rotate-angle> [<cx> <cy>]), which specifies
|
||||
a rotation by <rotate-angle> degrees about a given point.
|
||||
If optional parameters <cx> and <cy> are not supplied,
|
||||
the rotate is about the origin of the current user coordinate
|
||||
system. Result is: ['rotate', rotate-angle, cx, cy]
|
||||
|
||||
* Skew: skewX(<skew-angle>), which specifies a skew
|
||||
transformation along the x-axis. skewY(<skew-angle>), which
|
||||
specifies a skew transformation along the y-axis.
|
||||
Result is ['skew', angle-x, angle-y]
|
||||
|
||||
* Matrix: matrix(<a> <b> <c> <d> <e> <f>), which specifies a
|
||||
transformation in the form of a transformation matrix of six
|
||||
values. matrix(a,b,c,d,e,f) is equivalent to applying the
|
||||
transformation matrix [a b c d e f]. Result is
|
||||
['matrix', a, b, c, d, e, f]
|
||||
|
||||
Note: All parameters to the transformations are "numbers",
|
||||
i.e. no units present.
|
||||
|
||||
:param trstr: SVG transform string.
|
||||
:type trstr: str
|
||||
:return: List of transforms.
|
||||
:rtype: list
|
||||
"""
|
||||
trlist = []
|
||||
|
||||
assert isinstance(trstr, str)
|
||||
trstr = trstr.strip(' ')
|
||||
|
||||
integer_re_str = r'[+-]?[0-9]+'
|
||||
number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
|
||||
r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
|
||||
|
||||
# num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing
|
||||
comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
|
||||
translate_re_str = r'translate\s*\(\s*(' + \
|
||||
number_re_str + r')(?:' + \
|
||||
comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r'))?\s*\)'
|
||||
scale_re_str = r'scale\s*\(\s*(' + \
|
||||
number_re_str + r')' + \
|
||||
r'(?:' + comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r'))?\s*\)'
|
||||
skew_re_str = r'skew([XY])\s*\(\s*(' + \
|
||||
number_re_str + r')\s*\)'
|
||||
rotate_re_str = r'rotate\s*\(\s*(' + \
|
||||
number_re_str + r')' + \
|
||||
r'(?:' + comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r')' + \
|
||||
comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r'))?\s*\)'
|
||||
matrix_re_str = r'matrix\s*\(\s*' + \
|
||||
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r')' + comma_or_space_re_str + \
|
||||
r'(' + number_re_str + r')\s*\)'
|
||||
|
||||
while len(trstr) > 0:
|
||||
match = re.search(r'^' + translate_re_str, trstr)
|
||||
if match:
|
||||
trlist.append([
|
||||
'translate',
|
||||
float(match.group(1)),
|
||||
float(match.group(2)) if match.group else 0.0
|
||||
])
|
||||
trstr = trstr[len(match.group(0)):].strip(' ')
|
||||
continue
|
||||
|
||||
match = re.search(r'^' + scale_re_str, trstr)
|
||||
if match:
|
||||
trlist.append([
|
||||
'translate',
|
||||
float(match.group(1)),
|
||||
float(match.group(2)) if not None else float(match.group(1))
|
||||
])
|
||||
trstr = trstr[len(match.group(0)):].strip(' ')
|
||||
continue
|
||||
|
||||
match = re.search(r'^' + skew_re_str, trstr)
|
||||
if match:
|
||||
trlist.append([
|
||||
'skew',
|
||||
float(match.group(2)) if match.group(1) == 'X' else 0.0,
|
||||
float(match.group(2)) if match.group(1) == 'Y' else 0.0
|
||||
])
|
||||
trstr = trstr[len(match.group(0)):].strip(' ')
|
||||
continue
|
||||
|
||||
match = re.search(r'^' + rotate_re_str, trstr)
|
||||
if match:
|
||||
trlist.append([
|
||||
'rotate',
|
||||
float(match.group(1)),
|
||||
float(match.group(2)) if match.group(2) else 0.0,
|
||||
float(match.group(3)) if match.group(3) else 0.0
|
||||
])
|
||||
trstr = trstr[len(match.group(0)):].strip(' ')
|
||||
continue
|
||||
|
||||
match = re.search(r'^' + matrix_re_str, trstr)
|
||||
if match:
|
||||
trlist.append(['matrix'] + [float(x) for x in match.groups()])
|
||||
trstr = trstr[len(match.group(0)):].strip(' ')
|
||||
continue
|
||||
|
||||
# raise Exception("Don't know how to parse: %s" % trstr)
|
||||
log.error("[ERROR] Don't know how to parse: %s" % trstr)
|
||||
|
||||
return trlist
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# tree = ET.parse('tests/svg/drawing.svg')
|
||||
# root = tree.getroot()
|
||||
# ns = re.search(r'\{(.*)\}', root.tag).group(1)
|
||||
# print(ns)
|
||||
# for geo in getsvggeo(root):
|
||||
# print(geo)
|
||||
0
flatcamParsers/__init__.py
Normal file
0
flatcamParsers/__init__.py
Normal file
Reference in New Issue
Block a user