- grouped all parsers files in flatcamParsers folder

This commit is contained in:
Marius Stanciu
2019-03-11 12:28:44 +02:00
committed by Marius
parent 9d33e08ecf
commit 5a4d61ee8f
8 changed files with 12 additions and 38 deletions

453
flatcamParsers/ParseDXF.py Normal file
View 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

View 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
View 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
View 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)

View File