~freecad-community/freecad-extras/drawing-dimensioning

« back to all changes in this revision

Viewing changes to svgLib_dd.py

  • Committer: hamish2014
  • Date: 2015-10-22 11:27:03 UTC
  • Revision ID: git-v1:a67827ffc3ae7e17fb1d3ec11d3341c81e043fe0
Center points of arcs are not available for selection #64

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
import sys, numpy, copy
3
3
from PySide import QtGui, QtSvg, QtCore
4
4
from XMLlib import SvgXMLTreeNode
5
 
from circleLib import fitCircle_to_path, findCircularArcCentrePoint, pointsAlongCircularArc, fitCircle
6
 
from numpy import arctan2, pi, linspace, dot, sin, cos
 
5
from circleLib import fitCircle_to_path, findCircularArcCentrePoint, pointsAlongCircularArc, fitCircle, arccos2
 
6
from numpy import arctan2, pi, linspace, dot, sin, cos, array, cross
7
7
from numpy.linalg import norm
8
8
 
9
9
class SvgTextRenderer:
110
110
        self.lines = []
111
111
        self.arcs = []
112
112
        self.bezierCurves = []
 
113
        self.elements = [] # for preserving order of path elements, pen movements are not considered elements since they do not result in a direct visual effect
113
114
        dParmsXML_org = element.parms['d'].replace(',',' ')
114
115
        #<spacing corrections>
115
116
        dParmsXML = ''
116
117
        for a,b in zip(dParmsXML_org[:-1], dParmsXML_org[1:]):
117
 
            if a in 'MmLlACcQZzHhVv':
 
118
            if a in 'MmLlAaCcQZzHhVv':
118
119
                if len(dParmsXML) > 0 and dParmsXML[-1] <> ' ':
119
120
                    dParmsXML = dParmsXML + ' '
120
121
                dParmsXML = dParmsXML + a
124
125
                dParmsXML = dParmsXML + a + ' '
125
126
            else:
126
127
                dParmsXML = dParmsXML + a
127
 
        if b in 'MmLlACcQZzHhVv' and dParmsXML[-1] <> ' ':
 
128
        if b in 'MmLlAaCcQZzHhVv' and dParmsXML[-1] <> ' ':
128
129
            dParmsXML = dParmsXML + ' '
129
130
        dParmsXML = dParmsXML + b
130
131
        #<spacing corrections>
135
136
        pathDescriptor = None
136
137
        while j < len(parms):
137
138
            #print(parms[j:])
138
 
            if parms[j] in list('MmLlACcQZzHhVv,'):
 
139
            if parms[j] in list('MmLlAaCcQZzHhVv,'):
139
140
                pathDescriptor = parms[j]
140
141
            else: #using previous pathDescriptor
141
142
                if pathDescriptor == None:
183
184
                    end_x, end_y = path_start_x , path_start_y
184
185
                    j = j + 1
185
186
                self.lines.append( SvgPathLine( pen_x, pen_y, end_x, end_y ) )
 
187
                self.elements.append( self.lines[-1] )
186
188
                _pen_x, _pen_y = _end_x, _end_y
187
189
                pen_x, pen_y = end_x, end_y
188
190
 
189
 
            elif parms[j] == 'A':
 
191
            elif parms[j] == 'A' or parms[j] == 'a':
190
192
                # The arc command begins with the x and y radius and ends with the ending point of the arc. 
191
193
                # Between these are three other values: x axis rotation, large arc flag and sweep flag.
192
194
                rX, rY, xRotation, largeArc, sweep, _end_x, _end_y = map( float, parms[j+1:j+1 + 7] )
 
195
                #print(_end_x, _end_y)
 
196
                if parms[j] == 'a':
 
197
                    _end_x = _pen_x + _end_x
 
198
                    _end_y = _pen_y + _end_y
 
199
                    #print(_end_x, _end_y)
193
200
                end_x, end_y = element.applyTransforms( _end_x, _end_y )
194
 
                self.points.append( SvgPathPoint(_end_x, _end_y, end_x, end_y) )
195
 
                self.arcs.append( SvgPathArc( element, _pen_x, _pen_y,  rX, rY, xRotation, largeArc, sweep, _end_x, _end_y ) )
 
201
                if not ( _pen_x == _end_x and _pen_y == _end_y ) and rX <> 0 and rY <> 0:
 
202
                    self.points.append( SvgPathPoint(_end_x, _end_y, end_x, end_y) )
 
203
                    self.arcs.append( SvgPathArc( element, _pen_x, _pen_y,  rX, rY, xRotation, largeArc, sweep, _end_x, _end_y ) )
 
204
                    self.elements.append(self.arcs[-1])
196
205
                _pen_x, _pen_y = _end_x, _end_y
197
206
                pen_x, pen_y = end_x, end_y
198
207
                j = j + 8
216
225
                    j = j + 5
217
226
                    P = [ [pen_x, pen_y], element.applyTransforms(_x1, _y1), element.applyTransforms(_end_x, _end_y) ]
218
227
                self.bezierCurves.append( SvgPathBezierCurve(P) )
 
228
                self.elements.append(self.bezierCurves[-1])
219
229
                end_x, end_y = P[-1]
220
230
                self.points.append(  SvgPathPoint(_end_x, _end_y, end_x, end_y) )
221
231
                
240
250
        return (self.x1 + self.x2)/2, (self.y1 + self.y2)/2
241
251
 
242
252
class SvgPathArc:
 
253
    '''
 
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
 
256
 
 
257
    The elliptical arc command draws a section of an ellipse which meets the following constraints:
 
258
 
 
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.
 
263
 
 
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:
 
266
 
 
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)).
 
271
    '''
 
272
 
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)
247
 
        self.rX = rX
248
 
        self.rY = rY
 
274
        """
 
275
        3 coordinate systems
 
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
 
279
        """
 
280
        assert not ( _pen_x == _end_x and _pen_y == _end_y )
 
281
        assert rX <> 0
 
282
        assert rY <> 0
 
283
        self._pen_x = _pen_x
 
284
        self._pen_y = _pen_y
 
285
        self._end_x = _end_x
 
286
        self._end_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
255
 
        if rX == rY:
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): 
258
 
                self.circular = True
259
 
                self.c_x, self.c_y = element.applyTransforms( _c_x, _c_y )
260
 
                self.r = rX
261
 
    def svgPath( self ):
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
263
 
        assert rX == rY
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
 
296
        ##import FreeCAD
 
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
 
308
        #a,b = 1,1
 
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)
 
312
        #print(dtheta)
 
313
        assert dtheta >= 0
 
314
        if largeArc:
 
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
 
317
            dtheta = -dtheta
 
318
        self.dtheta = dtheta
 
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
 
327
        if self.circular:
 
328
            self.r = rX
 
329
 
 
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
 
333
                 
 
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 )
 
343
 
 
344
    def length(self):
 
345
        if self.circular:
 
346
            return abs(self.dtheta) * self.rX * self.scaling
 
347
        else:
 
348
            raise NotImplementedError, "arc.length for ellipsoidal arcs not implemented"
 
349
 
 
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_ )
 
357
        dP = p1 - p0
 
358
        dP = dP / norm(dP)
 
359
        return dP
 
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)
 
366
        BC = C - 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 ])
 
370
        F = cross(D,E)
 
371
        if F[2] < 0:
 
372
            theta = -theta
 
373
        #print(theta, self.dtheta, theta / self.dtheta )
 
374
        return theta / self.dtheta
 
375
    def flip( self):
 
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 )
 
383
 
 
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]
 
387
            )
 
388
        return '<path transform="%s" stroke = "%s" stroke-width = "%f" fill = "none" d = "M %f %f A %f %f %f %i %i %f %f"/>' % (
 
389
            transform_txt,
 
390
            strokeColor,
 
391
            strokeWidth / self.scaling,
 
392
            self._pen_x, self._pen_y,
 
393
            self.rX, self.rY,
 
394
            self.xRotation,
 
395
            self.largeArc,
 
396
            self.sweep,
 
397
            self._end_x, self._end_y,
 
398
            )
 
399
 
 
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))
268
405
        return path
 
406
 
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)
273
 
        n = 12
274
 
        pen_x, pen_y = element.applyTransforms(self._pen_x, self._pen_y)
275
 
        X = [ pen_x ]
276
 
        Y = [ 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)
 
410
        y_c = yT(y_c)
 
411
        X = []
 
412
        Y = []
 
413
        for t in numpy.linspace(0, 1.0, 13):
 
414
            x, y = self.valueAt(t)
279
415
            X.append( x )
280
 
            Y.append( y )
 
416
            Y.append( yT(y) )
281
417
        #end_x, end_y = element.applyTransforms(self._end_x, self._end_y)
282
418
        #import FreeCAD
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]) ) ) )
287
 
        Y = map( yT, Y )
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 
291
 
        X = T dot Y
292
 
        where T = [[c -s],[s, c]] dot [[ rX 0],[0 rY]]
293
 
        '''
294
 
        #import FreeCAD
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 )       
307
 
        X = [ pen_x ]
308
 
        Y = [ 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 )
 
425
        X = []
 
426
        Y = []
 
427
        for t in numpy.linspace(0, 1.0, n):
 
428
            x, y = self.valueAt(t)
312
429
            X.append( x )
313
430
            Y.append( y )
314
431
        lines = []
360
477
    def fitCircle( self ):
361
478
        'returns x, y, r, r_error'
362
479
        return fitCircle_to_path([self.P])
363
 
    def svgPath( self ):
 
480
    def QPainterPath( self ):
364
481
        P = self.P
365
482
        path = QtGui.QPainterPath(QtCore.QPointF(*P[0]))
366
483
        if len(P) == 4:
369
486
            path.quadTo( QtCore.QPointF(*P[1]), QtCore.QPointF(*P[2]) )
370
487
        return path
371
488
    def points_along_curve( self, no=12):
372
 
        T = linspace(0,1,points_per_segment)
 
489
        T = linspace(0, 1, no)
373
490
        if len(self.P) == 4: #then cubic Bezier
374
491
            p0, p1, p2, p3 = self.P
375
492
            t0 =    T**0 * (1-T)**3