2.2 Path operations

Often, one wants to perform geometrical operations with a path before placing it on a canvas by stroking or filling it. For instance, one might want to intersect one path with another one, split the paths at the intersection points, and then join the segments together in a new way. PyX supports such tasks by means of a number of path methods, which we will introduce in the following.

Suppose you want to draw the radii to the intersection points of a circle with a straight line. This task can be done using the following code which results in Fig. 2.2

from pyx import *

c = canvas.canvas()

circle = path.circle(0, 0, 2)
line = path.line(-3, 1, 3, 2)
c.stroke(circle, [style.linewidth.Thick])
c.stroke(line, [style.linewidth.Thick])

isects_circle, isects_line = circle.intersect(line)
for isect in isects_circle:
    isectx, isecty = circle.at(isect)
    c.stroke(path.line(0, 0, isectx, isecty))

c.writeEPSfile("radii")
c.writePDFfile("radii")
Figure 2.2: Example: Intersection of circle with line yielding two radii.
\includegraphics{radii}
Here, the basic elements, a circle around the point $(0, 0)$ with radius $2$ and a straight line, are defined. Then, passing the line, to the intersect() method of circle, we obtain a tuple of parameter values of the intersection points. The first element of the tuple is a list of parameter values for the path whose intersect() method has been called, the second element is the corresponding list for the path passed as argument to this method. In the present example, we only need one list of parameter values, namely isects_circle. Using the at() path method to obtain the point corresponding to the parameter value, we draw the radii for the different intersection points.

Another powerful feature of PyX is its ability to split paths at a given set of parameters. For instance, in order to fill in the previous example the segment of the circle delimited by the straight line (cf. Fig. 2.3), one first has to construct a path corresponding to the outline of this segment. The following code snippet yields this segment

arc1, arc2 = circle.split(isects_circle)
if arc1.arclen() < arc2.arclen():
    arc = arc1
else:
    arc = arc2

isects_line.sort()
line1, line2, line3 = line.split(isects_line)

segment = line2 << arc
Figure 2.3: Example: Intersection of circle with line yielding radii and circle segment.
\includegraphics{radii2}
Here, we first split the circle using the split() method passing the list of parameters obtained above. Since the circle is closed, this yields two arc segments. We then use the arclen(), which returns the arc length of the path, to find the shorter of the two arcs. Before splitting the line, we have to take into account that the split() method only accepts a sorted list of parameters. Finally, we join the straight line and the arc segment. For this, we make use of the << operator, which not only adds the paths (which could be done using "line2 + arc"), but also joins the last subpath of line2 and the first one of arc. Thus, segment consists of only a single subpath and filling works as expected.

An important issue when operating on paths is the parametrisation used. Internally, PyX uses a parametrisation which uses an interval of length $1$ for each path element of a path. For instance, for a simple straight line, the possible parameter values range from $0$ to $1$, corresponding to the first and last point, respectively, of the line. Appending another straight line, would extend this range to a maximal value of $2$.

However, the situation becomes more complicated if more complex objects like a circle are involved. Then, one could be tempted to assume that again the parameter value ranges from $0$ to $1$, because the predefined circle consists just of one arc together with a closepath element. However, this is not the case: the actual range is much larger. The reason for this behaviour lies in the internal path handling of PyX: Before performing any non-trivial geometrical operation with a path, it will automatically be converted into an instance of the normpath class (see also Sect. 2.4.3). These so generated paths are already separated in their subpaths and only contain straight lines and Bézier curve segments. Thus, as is easily imaginable, they are much simpler to deal with.

XXX explain normpathparams and things like p.begin(), p.end()-1,

A more geometrical way of accessing a point on the path is to use the arc length of the path segment from the first point of the path to the given point. Thus, all PyX path methods that accept a parameter value also allow the user to pass an arc length. For instance,

from math import pi

r = 2
pt1 = path.circle(0, 0, r).at(r*pi)
pt2 = path.circle(0, 0, r).at(r*3*pi/2)

c.stroke(path.path(path.moveto(*pt1), path.lineto(*pt2)))
will draw a straight line from a point at angle $180$ degrees (in radians $\pi$) to another point at angle $270$ degrees (in radians $3\pi/2$) on a circle with radius $r=2$. Note however, that the mapping arc length $\to$ point is in general discontinuous at the begin and the end of a subpath, and thus PyX does not guarantee any particular result for this boundary case.

More information on the available path methods can be found in Sect. 2.4.1.