240
250
return (self.x1 + self.x2)/2, (self.y1 + self.y2)/2
242
252
class SvgPathArc:
254
http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
255
#seems to be more accurate then https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#Arcto
257
The elliptical arc command draws a section of an ellipse which meets the following constraints:
259
- the arc starts at the current point
260
- the arc ends at point (x, y)
261
- the ellipse has the two radii (rx, ry)
262
- the x-axis of the ellipse is rotated by x-axis-rotation relative to the x-axis of the current coordinate system.
264
For most situations, there are actually four different arcs (two different ellipses, each with two different arc sweeps) that satisfy these constraints.
265
large-arc-flag and sweep-flag indicate which one of the four arcs are drawn, as follows:
267
Of the four candidate arc sweeps, two will represent an arc sweep of greater than or equal to 180 degrees (the "large-arc"), and two will represent an arc sweep of less than or equal to 180 degrees (the "small-arc").
268
If large-arc-flag is '1', then one of the two larger arc sweeps will be chosen; otherwise, if large-arc-flag is '0', one of the smaller arc sweeps will be chosen.
269
If sweep-flag is '1', then the arc will be drawn in a "positive-angle" direction (i.e., the ellipse formula x=cx+rx*cos(theta) and y=cy+ry*sin(theta) is evaluated such that theta starts at an angle corresponding to the current point and increases positively until the arc reaches (x,y)).
270
A value of 0 causes the arc to be drawn in a "negative-angle" direction (i.e., theta starts at an angle value corresponding to the current point and decreases until the arc reaches (x,y)).
243
273
def __init__(self, element, _pen_x, _pen_y, rX, rY, xRotation, largeArc, sweep, _end_x, _end_y ):
244
self.element = element
245
self._pen_x = float(_pen_x)
246
self._pen_y = float(_pen_y)
276
circular coordinates - in this coordinate system, the elipse is circular and unrotated
277
elements coordinates - circular coordinates * elipse transformation (x*rX and y*Ry, then rotate to generate the elipse)
278
global coordinates - elements coordinates * upper element transformations
280
assert not ( _pen_x == _end_x and _pen_y == _end_y )
287
self.scaling = element.scaling2() #scaling between element and global coordinates
288
self.rX = rX #in elements coordinates
289
self.rY = rY #in elements coordinates
249
290
self.xRotation = xRotation
250
291
self.largeArc = largeArc
251
292
self.sweep = sweep
252
self._end_x = float(_end_x)
253
self._end_y = float(_end_y)
254
self.circular = False # and not eliptical
256
_c_x, _c_y = findCircularArcCentrePoint( rX, _pen_x, _pen_y, _end_x, _end_y, largeArc==1, sweep==1 ) #do in untranformed co-ordinates as to preserve sweep flag
257
if not numpy.isnan(_c_x):
259
self.c_x, self.c_y = element.applyTransforms( _c_x, _c_y )
262
element, _pen_x, _pen_y, rX, rY, xRotation, largeArc, sweep, _end_x, _end_y = self.element, self._pen_x, self._pen_y, self.rX, self.rY, self.xRotation, self.largeArc, self.sweep, self._end_x, self._end_y
264
path = QtGui.QPainterPath(QtCore.QPointF( * element.applyTransforms(_pen_x, _pen_y ) ) )
265
#path.arcTo(c_x - r, c_y -r , 2*r, 2*r, angle_1, angle_CCW) #dont know what is up with this function so trying something else.
266
for _p in pointsAlongCircularArc(rX, _pen_x, _pen_y, _end_x, _end_y, largeArc==1, sweep==1, noPoints=12):
267
path.lineTo(* element.applyTransforms(*_p) )
293
#finding center in circular coordinates
294
# X = T_ellipse dot Y
295
# where T_ellipse = [[c -s],[s, c]] dot [[ rX 0],[0 rY]], X is element coordinates, and Y is circular coordinates
297
##FreeCAD.Console.PrintMessage( 'Xrotation %f\n' % (self.xRotation))
298
c = cos( xRotation*pi/180)
299
s = sin( xRotation*pi/180)
300
T_ellipse = dot( array([[c,-s] ,[s,c]]), array([[ rX, 0], [0, rY] ]))
301
self.T_ellipse = T_ellipse
302
#FreeCAD.Console.PrintMessage( 'T %s\n' % (T))
303
x1,y1 = numpy.linalg.solve(T_ellipse, [_pen_x, _pen_y])
304
x2,y2 = numpy.linalg.solve(T_ellipse, [_end_x, _end_y])
305
c_x_Y, c_y_Y = findCircularArcCentrePoint( 1, x1, y1, x2, y2, largeArc==1, sweep==1 )
306
self.center_circular = array([c_x_Y, c_y_Y])
307
#now determining dtheta in circular coordinates
309
c = ( ( x2-x1 )**2 + ( y2-y1 )**2 ) ** 0.5
310
dtheta = arccos2( ( 1 + 1 - c**2 ) / ( 2 ) ) #cos rule
311
#print(x2,x1,y2,y1,c)
315
dtheta = 2*pi - dtheta
316
if not sweep: # If sweep-flag is '1', then the arc will be drawn in a "positive-angle" direction
319
self.theta_start = arctan2( y1 - c_y_Y, x1 - c_x_Y)
320
self.T_element, self.c_element = element.Transforms()
321
#print(self.valueAt(0), element.applyTransforms(_pen_x, _pen_y))
322
#print(self.valueAt(1), element.applyTransforms(_end_x, _end_y))
323
self.startPoint = self.valueAt(0)
324
self.endPoint = self.valueAt(1)
325
self.center = self.applyTransforms( self.center_circular )
326
self.circular = rX == rY
330
def applyTransforms(self, p):
331
'position in circular coordinates -> position in elements coordinates -> position in global coordinates'
332
return dot( self.T_element, dot(self.T_ellipse, p) ) + self.c_element
334
def valueAt(self, t):
335
#position in circular coordinates
336
theta = self.theta_start + t*self.dtheta
337
p_c = self.center_circular + array([cos(theta), sin(theta)])
338
return self.applyTransforms( p_c )
339
def valueAt_element(self, t):
340
theta = self.theta_start + t*self.dtheta
341
p_c = self.center_circular + array([cos(theta), sin(theta)])
342
return dot(self.T_ellipse, p_c )
346
return abs(self.dtheta) * self.rX * self.scaling
348
raise NotImplementedError, "arc.length for ellipsoidal arcs not implemented"
350
def tangentAt( self, t):
351
offset = pi/2 if self.dtheta >= 0 else -pi/2
352
theta = self.theta_start + self.dtheta*t + offset
353
p0_ = self.valueAt( t )
354
p1_ = p0_ + array([cos(theta), sin(theta)])
355
p0 = self.applyTransforms( p0_ )
356
p1 = self.applyTransforms( p1_ )
360
def t_of_position( self, pos ):
361
pos_element = numpy.linalg.solve( self.T_element, pos - self.c_element )
362
A = numpy.linalg.solve(self.T_ellipse, pos_element)
363
B = self.center_circular
364
C = B + array([cos(self.theta_start), sin(self.theta_start)])
365
AB = (A - B) / norm(A-B)
367
theta = arccos2(dot(AB,BC))
368
D = array([ A[0] - B[0], A[1] - B[1], 0.0 ])
369
E = array([ A[0] - C[0], A[1] - C[1], 0.0 ])
373
#print(theta, self.dtheta, theta / self.dtheta )
374
return theta / self.dtheta
376
self.theta_start = self.theta_start + self.dtheta
377
self.dtheta = -self.dtheta
378
self.sweep = not self.sweep
379
self._pen_x, self._pen_y, self._end_x, self._end_y = self._end_x, self._end_y, self._pen_x, self._pen_y
380
self.startPoint = self.valueAt(0)
381
self.endPoint = self.valueAt(1)
382
self.center = self.applyTransforms( self.center_circular )
384
def svg( self, strokeColor='blue', strokeWidth=0.3, fill= 'none'):
385
transform_txt = 'matrix(%f %f %f %f %f %f)' % (
386
self.T_element[0,0], self.T_element[0,1], self.T_element[1,0], self.T_element[1,1], self.c_element[0], self.c_element[1]
388
return '<path transform="%s" stroke = "%s" stroke-width = "%f" fill = "none" d = "M %f %f A %f %f %f %i %i %f %f"/>' % (
391
strokeWidth / self.scaling,
392
self._pen_x, self._pen_y,
397
self._end_x, self._end_y,
400
def QPainterPath( self, n=12 ):
401
path = QtGui.QPainterPath( QtCore.QPointF( *self.valueAt(0) ) )
402
#path.arcTo #dont know what is up with this function so trying something else.
403
for t in numpy.linspace(0,1,n)[1:]:
404
path.lineTo(*self.valueAt(t))
269
407
def dxfwrite_arc_parms(self, yT):
270
408
'returns [ [radius=1.0, center=(0., 0.), startangle=0., endangle=360.], [...], ...]'
271
element = self.element
272
x_c, y_c = self.c_x, yT(self.c_y)
274
pen_x, pen_y = element.applyTransforms(self._pen_x, self._pen_y)
277
for _p in pointsAlongCircularArc(self.rX, self._pen_x, self._pen_y, self._end_x, self._end_y, self.largeArc==1, self.sweep==1, noPoints=n):
278
x,y = element.applyTransforms( *_p )
409
x_c, y_c = self.applyTransforms(self.center_circular)
413
for t in numpy.linspace(0, 1.0, 13):
414
x, y = self.valueAt(t)
281
417
#end_x, end_y = element.applyTransforms(self._end_x, self._end_y)
283
419
#FreeCAD.Console.PrintMessage( 'X %s pen_x %f end_x %f\n' % (X, pen_x, end_x))
284
420
#FreeCAD.Console.PrintMessage( 'Y %s pen_y %f end_y %f\n' % (Y, pen_y, end_y))
285
421
#for i in range(len(X) -1):
286
422
# drawing.add( dxf.line( (X[i], yT(Y[i])), (X[i+1],yT(Y[i+1]) ) ) )
288
423
return dxfwrite_arc_parms( X, Y, x_c, y_c)
289
424
def approximate_via_lines(self, n=12):
290
'''Transform between X elements cordinate (before parent element tranforms) and Y (circular rX == 1 and rY == 1, x_c,y_c = (?,?) is
292
where T = [[c -s],[s, c]] dot [[ rX 0],[0 rY]]
295
#FreeCAD.Console.PrintMessage( 'arc.approximate_via_line __dict__ %s\n' % (self.__dict__))
296
#FreeCAD.Console.PrintMessage( 'Xrotation %f\n' % (self.xRotation))
297
c = cos(self.xRotation*pi/180)
298
s = sin(self.xRotation*pi/180)
299
T = dot( numpy.array([[c,-s] ,[s,c]]), numpy.array([[ self.rX, 0], [0, self.rY] ]))
300
#FreeCAD.Console.PrintMessage( 'T %s\n' % (T))
301
x1,y1 = numpy.linalg.solve(T, [self._pen_x, self._pen_y])
302
x2,y2 = numpy.linalg.solve(T, [self._end_x, self._end_y])
303
c_x_Y, c_y_Y = findCircularArcCentrePoint( 1, x1, y1, x2, y2, self.largeArc==1, self.sweep==1 )
304
#FreeCAD.Console.PrintMessage( 'c_x_Y %f, c_y_Y %f\n' % (c_x_Y, c_y_Y))
305
#FreeCAD.Console.PrintMessage( 'norm([x1 - c_x_Y, y1 - c_y_Y]) = %f\n' % ( norm([x1 - c_x_Y, y1 - c_y_Y]) ) )
306
pen_x, pen_y = self.element.applyTransforms( self._pen_x, self._pen_y )
309
for _p in pointsAlongCircularArc(1, x1, y1, x2, y2, self.largeArc==1, self.sweep==1, noPoints=n):
310
p_element = dot(T, _p)
311
x,y = self.element.applyTransforms( *p_element )
427
for t in numpy.linspace(0, 1.0, n):
428
x, y = self.valueAt(t)