- correct slots
[PyX/mjg.git] / pyx / deformer.py
blob1b1618abfd6c313862ec79d070b31dcb6855ff1c
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2003-2005 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2003-2005 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 import math, warnings
26 import attr, helper, path, normpath, unit, color
27 from path import degrees
29 # specific exception for an invalid parameterization point
30 # used in parallel
31 class InvalidParamException(Exception):
33 def __init__(self, param):
34 self.normsubpathitemparam = param
36 def curvescontrols_from_endlines_pt(B, tangent1, tangent2, r1, r2, softness): # <<<
37 # calculates the parameters for two bezier curves connecting two lines (curvature=0)
38 # starting at B - r1*tangent1
39 # ending at B + r2*tangent2
41 # Takes the corner B
42 # and two tangent vectors heading to and from B
43 # and two radii r1 and r2:
44 # All arguments must be in Points
45 # Returns the seven control points of the two bezier curves:
46 # - start d1
47 # - control points g1 and f1
48 # - midpoint e
49 # - control points f2 and g2
50 # - endpoint d2
52 # make direction vectors d1: from B to A
53 # d2: from B to C
54 d1 = -tangent1[0] / math.hypot(*tangent1), -tangent1[1] / math.hypot(*tangent1)
55 d2 = tangent2[0] / math.hypot(*tangent2), tangent2[1] / math.hypot(*tangent2)
57 # 0.3192 has turned out to be the maximum softness available
58 # for straight lines ;-)
59 f = 0.3192 * softness
60 g = (15.0 * f + math.sqrt(-15.0*f*f + 24.0*f))/12.0
62 # make the control points of the two bezier curves
63 f1 = B[0] + f * r1 * d1[0], B[1] + f * r1 * d1[1]
64 f2 = B[0] + f * r2 * d2[0], B[1] + f * r2 * d2[1]
65 g1 = B[0] + g * r1 * d1[0], B[1] + g * r1 * d1[1]
66 g2 = B[0] + g * r2 * d2[0], B[1] + g * r2 * d2[1]
67 d1 = B[0] + r1 * d1[0], B[1] + r1 * d1[1]
68 d2 = B[0] + r2 * d2[0], B[1] + r2 * d2[1]
69 e = 0.5 * (f1[0] + f2[0]), 0.5 * (f1[1] + f2[1])
71 return (d1, g1, f1, e, f2, g2, d2)
72 # >>>
74 def controldists_from_endgeometry_pt(A, B, tangA, tangB, curvA, curvB, epsilon): # <<<
76 """For a curve with given tangents and curvatures at the endpoints this gives the distances between the controlpoints
78 This helper routine returns a list of two distances between the endpoints and the
79 corresponding control points of a (cubic) bezier curve that has
80 prescribed tangents tangentA, tangentB and curvatures curvA, curvB at the
81 end points.
83 Note: The returned distances are not always positive.
84 But only positive values are geometrically correct, so please check!
85 The outcome is sorted so that the first entry is expected to be the
86 most reasonable one
87 """
89 # these two thresholds are dimensionless, not lengths:
90 fallback_threshold = 1.0e-3
92 # some shortcuts
93 T = tangA[0] * tangB[1] - tangA[1] * tangB[0]
94 D = tangA[0] * (B[1]-A[1]) - tangA[1] * (B[0]-A[0])
95 E = tangB[0] * (A[1]-B[1]) - tangB[1] * (A[0]-B[0])
96 AB = math.hypot(A[0] - B[0], A[1] - B[1])
98 # For curvatures zero we found no solutions (during testing)
99 # Terefore, we need a fallback.
100 # When the curvature is nearly zero or when T is nearly zero
101 # we know the exact answer to the problem.
103 # extreme case: all parameters are nearly zero
104 a = b = 0.3 * AB
105 if max([abs(1.5*a*a*curvA), abs(1.5*b*b*curvB), abs(a*T), abs(b*T), abs(D), abs(E)]) < epsilon:
106 return [(a, b)]
108 # extreme case: curvA geometrically too big
109 if fallback_threshold * abs(curvA*AB) > 1:
110 a = math.sqrt(abs(D / (1.5 * curvA))) * helper.sign(D*curvA)
111 b = (D - 1.5*curvA*a*abs(a)) / T
112 return [(a, b)]
114 # extreme case: curvB geometrically too big
115 if fallback_threshold * abs(curvB*AB) > 1:
116 b = math.sqrt(abs(E / (1.5 * curvB))) * helper.sign(E*curvB)
117 a = (E - 1.5*curvB*b*abs(b)) / T
118 return [(a, b)]
120 # extreme case: curvA much smaller than T
121 try:
122 b = D / T
123 a = (E - 1.5*curvB*b*abs(b)) / T
124 except ZeroDivisionError:
125 pass
126 else:
127 if abs(1.5*a*a*curvA) < fallback_threshold * abs(b*T):
128 return [(a, b)]
130 # extreme case: curvB much smaller than T
131 try:
132 a = E / T
133 b = (D - 1.5*curvA*a*abs(a)) / T
134 except ZeroDivisionError:
135 pass
136 else:
137 if abs(1.5*b*b*curvB) < fallback_threshold * abs(a*T):
138 return [(a, b)]
140 # extreme case: T much smaller than both curvatures
141 try:
142 a = math.sqrt(abs(D / (1.5 * curvA))) * helper.sign(D*curvA)
143 except ZeroDivisionError:
144 # we have tried the case with small curvA already
145 # and we cannot divide by T
146 pass
147 else:
148 try:
149 b = math.sqrt(abs(E / (1.5 * curvB))) * helper.sign(E*curvB)
150 except ZeroDivisionError:
151 # we have tried the case with small curvB already
152 # and we cannot divide by T
153 pass
154 else:
155 if (abs(b*T) < fallback_threshold * abs(1.5*a*a*curvA) and
156 abs(a*T) < fallback_threshold * abs(1.5*b*b*curvB) ):
157 return [(a, b)]
159 # Now the general case:
160 # we try to find all the zeros of the decoupled 4th order problem
161 # for the combined problem:
162 # The control points of a cubic Bezier curve are given by a, b:
163 # A, A + a*tangA, B - b*tangB, B
164 # for the derivation see /design/beziers.tex
165 # 0 = 1.5 a |a| curvA + b * T - D
166 # 0 = 1.5 b |b| curvB + a * T - E
167 # because of the absolute values we get several possibilities for the signs
168 # in the equation. We test all signs, also the invalid ones!
169 candidates_a, candidates_b = [], []
170 for sign_a in [+1, -1]:
171 for sign_b in [+1, -1]:
172 coeffs_a = (sign_b*1.5*curvB*D*D - T*T*E, T**3, -sign_b*sign_a*4.5*curvA*curvB*D, 0.0, sign_b*3.375*curvA*curvA*curvB)
173 coeffs_b = (sign_a*1.5*curvA*E*E - T*T*D, T**3, -sign_a*sign_b*4.5*curvA*curvB*E, 0.0, sign_a*3.375*curvA*curvB*curvB)
174 candidates_a += [root for root in helper.realpolyroots(coeffs_a) if sign_a*root >= 0]
175 candidates_b += [root for root in helper.realpolyroots(coeffs_b) if sign_b*root >= 0]
177 # check the combined equations
178 # and find the candidate pairs
179 solutions = []
180 for a in candidates_a:
181 for b in candidates_b:
182 # check if the calculated radii of curvature do not differ from 1/curv
183 # by more than epsilon. This uses epsilon like in all normpath
184 # methods as a criterion for "length".
185 # In mathematical language this is
186 # abs(1.5a|a|/(D-bT) - 1/curvA) < epsilon
187 if ( abs(1.5*curvA*a*abs(a) + b*T - D) < epsilon * abs(curvA*(D - b*T)) and
188 abs(1.5*curvB*b*abs(b) + a*T - E) < epsilon * abs(curvB*(E - a*T)) ):
189 solutions.append((a, b))
191 # sort the solutions: the more reasonable values at the beginning
192 def mycmp(x,y):
193 # first the pairs that are purely positive, then all the pairs with some negative signs
194 # inside the two sets: sort by magnitude
195 sx = x[0] > 0 and x[1] > 0
196 sy = y[0] > 0 and y[1] > 0
197 if sx == sy:
198 return cmp(x[0]**2 + x[1]**2, y[0]**2 + y[1]**2)
199 else:
200 return cmp(sy, sx)
202 solutions.sort(mycmp)
204 # XXX should the solutions's list also be unique'fied?
206 # XXX we will stop here, if solutions is empty
207 if not solutions:
208 #print curvA, curvB, T, D, E
209 raise ValueError, "no curve found. Try adjusting the fallback-parameters."
211 return solutions
212 # >>>
214 def normcurve_from_endgeometry_pt(A, B, tangA, tangB, curvA, curvB): # <<<
215 a, b = controldists_from_endgeometry_pt(A, B, tangA, tangB, curvA, curvB, epsilon=epsilon)[0]
216 return normpath.normcurve_pt(A[0], A[1],
217 A[0] + a * tangA[0], A[1] + a * tangA[1],
218 B[0] - b * tangB[0], B[1] - b * tangB[1], B[0], B[1])
219 # >>>
221 def intersection(A, D, tangA, tangD): # <<<
223 """returns the intersection parameters of two evens
225 they are defined by:
226 x(t) = A + t * tangA
227 x(s) = D + s * tangD
229 det = -tangA[0] * tangD[1] + tangA[1] * tangD[0]
230 try:
231 1.0 / det
232 except ArithmeticError:
233 return None, None
235 DA = D[0] - A[0], D[1] - A[1]
237 t = (-tangD[1]*DA[0] + tangD[0]*DA[1]) / det
238 s = (-tangA[1]*DA[0] + tangA[0]*DA[1]) / det
240 return t, s
241 # >>>
243 class deformer(attr.attr):
245 def deform (self, basepath):
246 return basepath
248 class cycloid(deformer): # <<<
249 """Wraps a cycloid around a path.
251 The outcome looks like a spring with the originalpath as the axis.
252 radius: radius of the cycloid
253 halfloops: number of halfloops
254 skipfirst/skiplast: undeformed end lines of the original path
255 curvesperhloop:
256 sign: start left (1) or right (-1) with the first halfloop
257 turnangle: angle of perspective on a (3D) spring
258 turnangle=0 will produce a sinus-like cycloid,
259 turnangle=90 will procude a row of connected circles
263 def __init__(self, radius=0.5*unit.t_cm, halfloops=10,
264 skipfirst=1*unit.t_cm, skiplast=1*unit.t_cm, curvesperhloop=3, sign=1, turnangle=45):
265 self.skipfirst = skipfirst
266 self.skiplast = skiplast
267 self.radius = radius
268 self.halfloops = halfloops
269 self.curvesperhloop = curvesperhloop
270 self.sign = sign
271 self.turnangle = turnangle
273 def __call__(self, radius=None, halfloops=None,
274 skipfirst=None, skiplast=None, curvesperhloop=None, sign=None, turnangle=None):
275 if radius is None:
276 radius = self.radius
277 if halfloops is None:
278 halfloops = self.halfloops
279 if skipfirst is None:
280 skipfirst = self.skipfirst
281 if skiplast is None:
282 skiplast = self.skiplast
283 if curvesperhloop is None:
284 curvesperhloop = self.curvesperhloop
285 if sign is None:
286 sign = self.sign
287 if turnangle is None:
288 turnangle = self.turnangle
290 return cycloid(radius=radius, halfloops=halfloops, skipfirst=skipfirst, skiplast=skiplast,
291 curvesperhloop=curvesperhloop, sign=sign, turnangle=turnangle)
293 def deform(self, basepath):
294 resultnormsubpaths = [self.deformsubpath(nsp) for nsp in basepath.normpath().normsubpaths]
295 return normpath.normpath(resultnormsubpaths)
297 def deformsubpath(self, normsubpath):
299 skipfirst = abs(unit.topt(self.skipfirst))
300 skiplast = abs(unit.topt(self.skiplast))
301 radius = abs(unit.topt(self.radius))
302 turnangle = degrees(self.turnangle)
303 sign = helper.sign(self.sign)
305 cosTurn = math.cos(turnangle)
306 sinTurn = math.sin(turnangle)
308 # make list of the lengths and parameters at points on normsubpath
309 # where we will add cycloid-points
310 totlength = normsubpath.arclen_pt()
311 if totlength <= skipfirst + skiplast + 2*radius*sinTurn:
312 warnings.warn("normsubpath is too short for deformation with cycloid -- skipping...")
313 return normsubpath
315 # parameterization is in rotation-angle around the basepath
316 # differences in length, angle ... between two basepoints
317 # and between basepoints and controlpoints
318 Dphi = math.pi / self.curvesperhloop
319 phis = [i * Dphi for i in range(self.halfloops * self.curvesperhloop + 1)]
320 DzDphi = (totlength - skipfirst - skiplast - 2*radius*sinTurn) * 1.0 / (self.halfloops * math.pi * cosTurn)
321 # Dz = (totlength - skipfirst - skiplast - 2*radius*sinTurn) * 1.0 / (self.halfloops * self.curvesperhloop * cosTurn)
322 # zs = [i * Dz for i in range(self.halfloops * self.curvesperhloop + 1)]
323 # from path._arctobcurve:
324 # optimal relative distance along tangent for second and third control point
325 L = 4 * radius * (1 - math.cos(Dphi/2)) / (3 * math.sin(Dphi/2))
327 # Now the transformation of z into the turned coordinate system
328 Zs = [ skipfirst + radius*sinTurn # here the coordinate z starts
329 - sinTurn*radius*math.cos(phi) + cosTurn*DzDphi*phi # the transformed z-coordinate
330 for phi in phis]
331 params = normsubpath._arclentoparam_pt(Zs)[0]
333 # get the positions of the splitpoints in the cycloid
334 points = []
335 for phi, param in zip(phis, params):
336 # the cycloid is a circle that is stretched along the normsubpath
337 # here are the points of that circle
338 basetrafo = normsubpath.trafo([param])[0]
340 # The point on the cycloid, in the basepath's local coordinate system
341 baseZ, baseY = 0, radius*math.sin(phi)
343 # The tangent there, also in local coords
344 tangentX = -cosTurn*radius*math.sin(phi) + sinTurn*DzDphi
345 tangentY = radius*math.cos(phi)
346 tangentZ = sinTurn*radius*math.sin(phi) + DzDphi*cosTurn
347 norm = math.sqrt(tangentX*tangentX + tangentY*tangentY + tangentZ*tangentZ)
348 tangentY, tangentZ = tangentY/norm, tangentZ/norm
350 # Respect the curvature of the basepath for the cycloid's curvature
351 # XXX this is only a heuristic, not a "true" expression for
352 # the curvature in curved coordinate systems
353 pathradius = normsubpath.curveradius_pt([param])[0]
354 if pathradius is not normpath.invalid:
355 factor = (pathradius - baseY) / pathradius
356 factor = abs(factor)
357 else:
358 factor = 1
359 l = L * factor
361 # The control points prior and after the point on the cycloid
362 preeZ, preeY = baseZ - l * tangentZ, baseY - l * tangentY
363 postZ, postY = baseZ + l * tangentZ, baseY + l * tangentY
365 # Now put everything at the proper place
366 points.append(basetrafo.apply_pt(preeZ, sign * preeY) +
367 basetrafo.apply_pt(baseZ, sign * baseY) +
368 basetrafo.apply_pt(postZ, sign * postY))
370 if len(points) <= 1:
371 warnings.warn("normsubpath is too short for deformation with cycloid -- skipping...")
372 return normsubpath
374 # Build the path from the pointlist
375 # containing (control x 2, base x 2, control x 2)
376 if skipfirst > normsubpath.epsilon:
377 normsubpathitems = normsubpath.segments([0, params[0]])[0]
378 normsubpathitems.append(normpath.normcurve_pt(*(points[0][2:6] + points[1][0:4])))
379 else:
380 normsubpathitems = [normpath.normcurve_pt(*(points[0][2:6] + points[1][0:4]))]
381 for i in range(1, len(points)-1):
382 normsubpathitems.append(normpath.normcurve_pt(*(points[i][2:6] + points[i+1][0:4])))
383 if skiplast > normsubpath.epsilon:
384 for nsp in normsubpath.segments([params[-1], len(normsubpath)]):
385 normsubpathitems.extend(nsp.normsubpathitems)
387 # That's it
388 return normpath.normsubpath(normsubpathitems, epsilon=normsubpath.epsilon)
389 # >>>
391 cycloid.clear = attr.clearclass(cycloid)
393 class smoothed(deformer): # <<<
395 """Bends corners in a normpath.
397 This decorator replaces corners in a normpath with bezier curves. There are two cases:
398 - If the corner lies between two lines, _two_ bezier curves will be used
399 that are highly optimized to look good (their curvature is to be zero at the ends
400 and has to have zero derivative in the middle).
401 Additionally, it can controlled by the softness-parameter.
402 - If the corner lies between curves then _one_ bezier is used that is (except in some
403 special cases) uniquely determined by the tangents and curvatures at its end-points.
404 In some cases it is necessary to use only the absolute value of the curvature to avoid a
405 cusp-shaped connection of the new bezier to the old path. In this case the use of
406 "obeycurv=0" allows the sign of the curvature to switch.
407 - The radius argument gives the arclength-distance of the corner to the points where the
408 old path is cut and the beziers are inserted.
409 - Path elements that are too short (shorter than the radius) are skipped
412 def __init__(self, radius, softness=1, obeycurv=0, relskipthres=0.01):
413 self.radius = radius
414 self.softness = softness
415 self.obeycurv = obeycurv
416 self.relskipthres = relskipthres
418 def __call__(self, radius=None, softness=None, obeycurv=None, relskipthres=None):
419 if radius is None:
420 radius = self.radius
421 if softness is None:
422 softness = self.softness
423 if obeycurv is None:
424 obeycurv = self.obeycurv
425 if relskipthres is None:
426 relskipthres = self.relskipthres
427 return smoothed(radius=radius, softness=softness, obeycurv=obeycurv, relskipthres=relskipthres)
429 def deform(self, basepath):
430 return normpath.normpath([self.deformsubpath(normsubpath)
431 for normsubpath in basepath.normpath().normsubpaths])
433 def deformsubpath(self, normsubpath):
434 radius_pt = unit.topt(self.radius)
435 epsilon = normsubpath.epsilon
437 # remove too short normsubpath items (shorter than self.relskipthres*radius_pt or epsilon)
438 pertinentepsilon = max(epsilon, self.relskipthres*radius_pt)
439 pertinentnormsubpath = normpath.normsubpath(normsubpath.normsubpathitems,
440 epsilon=pertinentepsilon)
441 pertinentnormsubpath.flushskippedline()
442 pertinentnormsubpathitems = pertinentnormsubpath.normsubpathitems
444 # calculate the splitting parameters for the pertinentnormsubpathitems
445 arclens_pt = []
446 params = []
447 for pertinentnormsubpathitem in pertinentnormsubpathitems:
448 arclen_pt = pertinentnormsubpathitem.arclen_pt(epsilon)
449 arclens_pt.append(arclen_pt)
450 l1_pt = min(radius_pt, 0.5*arclen_pt)
451 l2_pt = max(0.5*arclen_pt, arclen_pt - radius_pt)
452 params.append(pertinentnormsubpathitem.arclentoparam_pt([l1_pt, l2_pt], epsilon))
454 # handle the first and last pertinentnormsubpathitems for a non-closed normsubpath
455 if not normsubpath.closed:
456 l1_pt = 0
457 l2_pt = max(0, arclens_pt[0] - radius_pt)
458 params[0] = pertinentnormsubpathitems[0].arclentoparam_pt([l1_pt, l2_pt], epsilon)
459 l1_pt = min(radius_pt, arclens_pt[-1])
460 l2_pt = arclens_pt[-1]
461 params[-1] = pertinentnormsubpathitems[-1].arclentoparam_pt([l1_pt, l2_pt], epsilon)
463 newnormsubpath = normpath.normsubpath(epsilon=normsubpath.epsilon)
464 for i in range(len(pertinentnormsubpathitems)):
465 this = i
466 next = (i+1) % len(pertinentnormsubpathitems)
467 thisparams = params[this]
468 nextparams = params[next]
469 thisnormsubpathitem = pertinentnormsubpathitems[this]
470 nextnormsubpathitem = pertinentnormsubpathitems[next]
471 thisarclen_pt = arclens_pt[this]
472 nextarclen_pt = arclens_pt[next]
474 # insert the middle segment
475 newnormsubpath.append(thisnormsubpathitem.segments(thisparams)[0])
477 # insert replacement curves for the corners
478 if next or normsubpath.closed:
480 t1 = thisnormsubpathitem.rotation([thisparams[1]])[0].apply_pt(1, 0)
481 t2 = nextnormsubpathitem.rotation([nextparams[0]])[0].apply_pt(1, 0)
482 # TODO: normpath.invalid
484 if (isinstance(thisnormsubpathitem, normpath.normline_pt) and
485 isinstance(nextnormsubpathitem, normpath.normline_pt)):
487 # case of two lines -> replace by two curves
488 d1, g1, f1, e, f2, g2, d2 = curvescontrols_from_endlines_pt(
489 thisnormsubpathitem.atend_pt(), t1, t2,
490 thisarclen_pt*(1-thisparams[1]), nextarclen_pt*(nextparams[0]), softness=self.softness)
492 p1 = thisnormsubpathitem.at_pt([thisparams[1]])[0]
493 p2 = nextnormsubpathitem.at_pt([nextparams[0]])[0]
495 newnormsubpath.append(normpath.normcurve_pt(*(d1 + g1 + f1 + e)))
496 newnormsubpath.append(normpath.normcurve_pt(*(e + f2 + g2 + d2)))
498 else:
500 # generic case -> replace by a single curve with prescribed tangents and curvatures
501 p1 = thisnormsubpathitem.at_pt([thisparams[1]])[0]
502 p2 = nextnormsubpathitem.at_pt([nextparams[0]])[0]
503 c1 = thisnormsubpathitem.curvature_pt([thisparams[1]])[0]
504 c2 = nextnormsubpathitem.curvature_pt([nextparams[0]])[0]
505 # TODO: normpath.invalid
507 # TODO: more intelligent fallbacks:
508 # circle -> line
509 # circle -> circle
511 if not self.obeycurv:
512 # do not obey the sign of the curvature but
513 # make the sign such that the curve smoothly passes to the next point
514 # this results in a discontinuous curvature
515 # (but the absolute value is still continuous)
516 s1 = helper.sign(t1[0] * (p2[1]-p1[1]) - t1[1] * (p2[0]-p1[0]))
517 s2 = helper.sign(t2[0] * (p2[1]-p1[1]) - t2[1] * (p2[0]-p1[0]))
518 c1 = s1 * abs(c1)
519 c2 = s2 * abs(c2)
521 # get the length of the control "arms"
522 a, d = controldists_from_endgeometry_pt(p1, p2, t1, t2, c1, c2, epsilon=epsilon)[0]
524 if a >= 0 and d >= 0:
525 # avoid curves with invalid parameterization
526 a = max(a, epsilon)
527 d = max(d, epsilon)
529 # avoid overshooting at the corners:
530 # this changes not only the sign of the curvature
531 # but also the magnitude
532 if not self.obeycurv:
533 t, s = intersection(p1, p2, t1, t2)
534 if (t is not None and s is not None and
535 t > 0 and s < 0):
536 a = min(a, abs(t))
537 d = min(d, abs(s))
539 else:
540 t, s = intersection(p1, p2, t1, t2)
541 if t is not None and s is not None:
542 a = abs(t)
543 d = abs(s)
544 else:
545 # if there is no useful result:
546 # take an arbitrary smoothing curve that does not obey
547 # the curvature constraints
548 dist = math.hypot(p1[0] - p2[0], p1[1] - p2[1])
549 a = dist / (3.0 * math.hypot(*t1))
550 d = dist / (3.0 * math.hypot(*t2))
552 # calculate the two missing control points
553 q1 = p1[0] + a * t1[0], p1[1] + a * t1[1]
554 q2 = p2[0] - d * t2[0], p2[1] - d * t2[1]
556 newnormsubpath.append(normpath.normcurve_pt(*(p1 + q1 + q2 + p2)))
558 if normsubpath.closed:
559 newnormsubpath.close()
560 return newnormsubpath
562 # >>>
564 smoothed.clear = attr.clearclass(smoothed)
566 class parallel(deformer): # <<<
568 """creates a parallel normpath with constant distance to the original normpath
570 A positive 'distance' results in a curve left of the original one -- and a
571 negative 'distance' in a curve at the right. Left/Right are understood in
572 terms of the parameterization of the original curve. For each path element
573 a parallel curve/line is constructed. At corners, either a circular arc is
574 drawn around the corner, or, if possible, the parallel curve is cut in
575 order to also exhibit a corner.
577 distance: the distance of the parallel normpath
578 relerr: distance*relerr is the maximal allowed error in the parallel distance
579 checkdistanceparams: a list of parameter values in the interval (0,1) where the
580 parallel distance is checked on each normpathitem
581 lookforcurvatures: number of points per normpathitem where is looked for
582 a critical value of the curvature
583 dointersection: boolean for doing the intersection step (default: 1).
584 Set this value to 0 if you want the whole parallel path
587 # TODO:
588 # * more testing of controldists_from_endgeometry_pt
589 # * do testing for curv=0, T=0, D=0, E=0 cases
590 # * do testing for several random curves
591 # -- does the recursive deformnicecurve converge?
593 # TODO for the test cases:
594 # * improve the intersection of nearly degenerate paths
597 def __init__(self, distance, relerr=0.05, sharpoutercorners=0, checkdistanceparams=[0.5],
598 lookforcurvatures=11, dointersection=1, debug=None):
599 self.distance = distance
600 self.relerr = relerr
601 self.sharpoutercorners = sharpoutercorners
602 self.checkdistanceparams = checkdistanceparams
603 self.lookforcurvatures = lookforcurvatures
604 self.dointersection = dointersection
605 self.debug = debug
607 def __call__(self, distance=None, relerr=None, sharpoutercorners=None, checkdistanceparams=None,
608 lookforcurvatures=None, dointersection=None, debug=None):
609 # returns a copy of the deformer with different parameters
610 if distance is None:
611 distance = self.distance
612 if relerr is None:
613 relerr = self.relerr
614 if sharpoutercorners is None:
615 sharpoutercorners = self.sharpoutercorners
616 if checkdistanceparams is None:
617 checkdistanceparams = self.checkdistanceparams
618 if lookforcurvatures is None:
619 lookforcurvatures = self.lookforcurvatures
620 if dointersection is None:
621 dointersection = self.dointersection
622 if debug is None:
623 debug = self.debug
625 return parallel(distance=distance, relerr=relerr, sharpoutercorners=sharpoutercorners,
626 checkdistanceparams=checkdistanceparams,
627 lookforcurvatures=lookforcurvatures, dointersection=dointersection,
628 debug=debug)
630 def deform(self, basepath):
631 self.dist_pt = unit.topt(self.distance)
632 resultnormsubpaths = []
633 for nsp in basepath.normpath().normsubpaths:
634 parallel_normpath = self.deformsubpath(nsp)
635 resultnormsubpaths += parallel_normpath.normsubpaths
636 result = normpath.normpath(resultnormsubpaths)
637 return result
639 def deformsubpath(self, orig_nsp): # <<<
641 """returns a list of normsubpaths building the parallel curve"""
643 dist = self.dist_pt
644 epsilon = orig_nsp.epsilon
646 # avoid too small dists: we would run into instabilities
647 if abs(dist) < abs(epsilon):
648 return orig_nsp
650 result = normpath.normpath()
652 # iterate over the normsubpath in the following manner:
653 # * for each item first append the additional arc / intersect
654 # and then add the next parallel piece
655 # * for the first item only add the parallel piece
656 # (because this is done for next_orig_nspitem, we need to start with next=0)
657 for i in range(len(orig_nsp.normsubpathitems)):
658 prev = i-1
659 next = i
660 prev_orig_nspitem = orig_nsp.normsubpathitems[prev]
661 next_orig_nspitem = orig_nsp.normsubpathitems[next]
663 stepsize = 0.01
664 prev_param, prev_rotation = self.valid_near_rotation(prev_orig_nspitem, 1, 0, stepsize, 0.5*epsilon)
665 next_param, next_rotation = self.valid_near_rotation(next_orig_nspitem, 0, 1, stepsize, 0.5*epsilon)
666 # TODO: eventually shorten next_orig_nspitem
668 prev_tangent = prev_rotation.apply_pt(1, 0)
669 next_tangent = next_rotation.apply_pt(1, 0)
671 # get the next parallel piece for the normpath
672 try:
673 next_parallel_normpath = self.deformsubpathitem(next_orig_nspitem, epsilon)
674 except InvalidParamException, e:
675 invalid_nspitem_param = e.normsubpathitemparam
676 # split the nspitem apart and continue with pieces that do not contain
677 # the invalid point anymore. At the end, simply take one piece, otherwise two.
678 stepsize = 0.01
679 if self.length_pt(next_orig_nspitem, invalid_nspitem_param, 0) > epsilon:
680 if self.length_pt(next_orig_nspitem, invalid_nspitem_param, 1) > epsilon:
681 p1, foo = self.valid_near_rotation(next_orig_nspitem, invalid_nspitem_param, 0, stepsize, 0.5*epsilon)
682 p2, foo = self.valid_near_rotation(next_orig_nspitem, invalid_nspitem_param, 1, stepsize, 0.5*epsilon)
683 segments = next_orig_nspitem.segments([0, p1, p2, 1])[0:3:2]
684 segments[1] = segments[1].modifiedbegin_pt(*(segments[0].atend_pt()))
685 else:
686 p1, foo = self.valid_near_rotation(next_orig_nspitem, invalid_nspitem_param, 0, stepsize, 0.5*epsilon)
687 segments = next_orig_nspitem.segments([0, p1])
688 else:
689 p2, foo = self.valid_near_rotation(next_orig_nspitem, invalid_nspitem_param, 1, stepsize, 0.5*epsilon)
690 segments = next_orig_nspitem.segments([p2, 1])
692 next_parallel_normpath = self.deformsubpath(normpath.normsubpath(segments, epsilon=epsilon))
694 if not (next_parallel_normpath.normsubpaths and next_parallel_normpath[0].normsubpathitems):
695 continue
697 # this starts the whole normpath
698 if not result.normsubpaths:
699 result = next_parallel_normpath
700 continue
702 # sinus of the angle between the tangents
703 # sinangle > 0 for a left-turning nexttangent
704 # sinangle < 0 for a right-turning nexttangent
705 sinangle = prev_tangent[0]*next_tangent[1] - prev_tangent[1]*next_tangent[0]
706 cosangle = prev_tangent[0]*next_tangent[0] + prev_tangent[1]*next_tangent[1]
707 if cosangle < 0 or abs(dist*math.asin(sinangle)) >= epsilon:
708 if self.sharpoutercorners and dist*sinangle < 0:
709 A1, A2 = result.atend_pt(), next_parallel_normpath.atbegin_pt()
710 t1, t2 = intersection(A1, A2, prev_tangent, next_tangent)
711 B = A1[0] + t1 * prev_tangent[0], A1[1] + t1 * prev_tangent[1]
712 arc_normpath = normpath.normpath([normpath.normsubpath([
713 normpath.normline_pt(A1[0], A1[1], B[0], B[1]),
714 normpath.normline_pt(B[0], B[1], A2[0], A2[1])
715 ])])
716 else:
717 # We must append an arc around the corner
718 arccenter = next_orig_nspitem.atbegin_pt()
719 arcbeg = result.atend_pt()
720 arcend = next_parallel_normpath.atbegin_pt()
721 angle1 = math.atan2(arcbeg[1] - arccenter[1], arcbeg[0] - arccenter[0])
722 angle2 = math.atan2(arcend[1] - arccenter[1], arcend[0] - arccenter[0])
724 # depending on the direction we have to use arc or arcn
725 if dist > 0:
726 arcclass = path.arcn_pt
727 else:
728 arcclass = path.arc_pt
729 arc_normpath = path.path(arcclass(
730 arccenter[0], arccenter[1], abs(dist),
731 degrees(angle1), degrees(angle2))).normpath(epsilon=epsilon)
733 # append the arc to the parallel path
734 result.join(arc_normpath)
735 # append the next parallel piece to the path
736 result.join(next_parallel_normpath)
737 else:
738 # The path is quite straight between prev and next item:
739 # normpath.normpath.join adds a straight line if necessary
740 result.join(next_parallel_normpath)
743 # end here if nothing has been found so far
744 if not (result.normsubpaths and result[-1].normsubpathitems):
745 return result
747 # the curve around the closing corner may still be missing
748 if orig_nsp.closed:
749 # TODO: normpath.invalid
750 stepsize = 0.01
751 prev_param, prev_rotation = self.valid_near_rotation(result[-1][-1], 1, 0, stepsize, 0.5*epsilon)
752 next_param, next_rotation = self.valid_near_rotation(result[0][0], 0, 1, stepsize, 0.5*epsilon)
753 # TODO: eventually shorten next_orig_nspitem
755 prev_tangent = prev_rotation.apply_pt(1, 0)
756 next_tangent = next_rotation.apply_pt(1, 0)
757 sinangle = prev_tangent[0]*next_tangent[1] - prev_tangent[1]*next_tangent[0]
758 cosangle = prev_tangent[0]*next_tangent[0] + prev_tangent[1]*next_tangent[1]
760 if cosangle < 0 or abs(dist*math.asin(sinangle)) >= epsilon:
761 # We must append an arc around the corner
762 # TODO: avoid the code dublication
763 if self.sharpoutercorners and dist*sinangle < 0:
764 A1, A2 = result.atend_pt(), result.atbegin_pt()
765 t1, t2 = intersection(A1, A2, prev_tangent, next_tangent)
766 B = A1[0] + t1 * prev_tangent[0], A1[1] + t1 * prev_tangent[1]
767 arc_normpath = normpath.normpath([normpath.normsubpath([
768 normpath.normline_pt(A1[0], A1[1], B[0], B[1]),
769 normpath.normline_pt(B[0], B[1], A2[0], A2[1])
770 ])])
771 else:
772 arccenter = orig_nsp.atend_pt()
773 arcbeg = result.atend_pt()
774 arcend = result.atbegin_pt()
775 angle1 = math.atan2(arcbeg[1] - arccenter[1], arcbeg[0] - arccenter[0])
776 angle2 = math.atan2(arcend[1] - arccenter[1], arcend[0] - arccenter[0])
778 # depending on the direction we have to use arc or arcn
779 if dist > 0:
780 arcclass = path.arcn_pt
781 else:
782 arcclass = path.arc_pt
783 arc_normpath = path.path(arcclass(
784 arccenter[0], arccenter[1], abs(dist),
785 degrees(angle1), degrees(angle2))).normpath(epsilon=epsilon)
787 # append the arc to the parallel path
788 if (result.normsubpaths and result[-1].normsubpathitems and
789 arc_normpath.normsubpaths and arc_normpath[-1].normsubpathitems):
790 result.join(arc_normpath)
792 if len(result) == 1:
793 result[0].close()
794 else:
795 # if the parallel normpath is split into several subpaths anyway,
796 # then use the natural beginning and ending
797 # closing is not possible anymore
798 for nspitem in result[0]:
799 result[-1].append(nspitem)
800 result.normsubpaths = result.normsubpaths[1:]
802 if self.dointersection:
803 result = self.rebuild_intersected_normpath(result, normpath.normpath([orig_nsp]), epsilon)
805 return result
806 # >>>
807 def deformsubpathitem(self, nspitem, epsilon): # <<<
809 """Returns a parallel normpath for a single normsubpathitem
811 Analyzes the curvature of a normsubpathitem and returns a normpath with
812 the appropriate number of normsubpaths. This must be a normpath because
813 a normcurve can be strongly curved, such that the parallel path must
814 contain a hole"""
816 dist = self.dist_pt
818 # for a simple line we return immediately
819 if isinstance(nspitem, normpath.normline_pt):
820 normal = nspitem.rotation([0])[0].apply_pt(0, 1)
821 start = nspitem.atbegin_pt()
822 end = nspitem.atend_pt()
823 return path.line_pt(
824 start[0] + dist * normal[0], start[1] + dist * normal[1],
825 end[0] + dist * normal[0], end[1] + dist * normal[1]).normpath(epsilon=epsilon)
827 # for a curve we have to check if the curvatures
828 # cross the singular value 1/dist
829 crossings = self.distcrossingparameters(nspitem, epsilon)
831 # depending on the number of crossings we must consider
832 # three different cases:
833 if crossings:
834 # The curvature crosses the borderline 1/dist
835 # the parallel curve contains points with infinite curvature!
836 result = normpath.normpath()
838 # we need the endpoints of the nspitem
839 if self.length_pt(nspitem, crossings[0], 0) > epsilon:
840 crossings.insert(0, 0)
841 if self.length_pt(nspitem, crossings[-1], 1) > epsilon:
842 crossings.append(1)
844 for i in range(len(crossings) - 1):
845 middleparam = 0.5*(crossings[i] + crossings[i+1])
846 middlecurv = nspitem.curvature_pt([middleparam])[0]
847 if middlecurv is normpath.invalid:
848 raise InvalidParamException(middleparam)
849 # the radius is good if
850 # - middlecurv and dist have opposite signs or
851 # - middlecurv is "smaller" than 1/dist
852 if middlecurv*dist < 0 or abs(dist*middlecurv) < 1:
853 parallel_nsp = self.deformnicecurve(nspitem.segments(crossings[i:i+2])[0], epsilon)
854 # never append empty normsubpaths
855 if parallel_nsp.normsubpathitems:
856 result.append(parallel_nsp)
858 return result
860 else:
861 # the curvature is either bigger or smaller than 1/dist
862 middlecurv = nspitem.curvature_pt([0.5])[0]
863 if dist*middlecurv < 0 or abs(dist*middlecurv) < 1:
864 # The curve is everywhere less curved than 1/dist
865 # We can proceed finding the parallel curve for the whole piece
866 parallel_nsp = self.deformnicecurve(nspitem, epsilon)
867 # never append empty normsubpaths
868 if parallel_nsp.normsubpathitems:
869 return normpath.normpath([parallel_nsp])
870 else:
871 return normpath.normpath()
872 else:
873 # the curve is everywhere stronger curved than 1/dist
874 # There is nothing to be returned.
875 return normpath.normpath()
877 # >>>
878 def deformnicecurve(self, normcurve, epsilon, startparam=0.0, endparam=1.0): # <<<
880 """Returns a parallel normsubpath for the normcurve.
882 This routine assumes that the normcurve is everywhere
883 'less' curved than 1/dist and contains no point with an
884 invalid parameterization
886 dist = self.dist_pt
887 T_threshold = 1.0e-5
889 # normalized tangent directions
890 tangA, tangD = normcurve.rotation([startparam, endparam])
891 # if we find an unexpected normpath.invalid we have to
892 # parallelise this normcurve on the level of split normsubpaths
893 if tangA is normpath.invalid:
894 raise InvalidParamException(startparam)
895 if tangD is normpath.invalid:
896 raise InvalidParamException(endparam)
897 tangA = tangA.apply_pt(1, 0)
898 tangD = tangD.apply_pt(1, 0)
900 # the new starting points
901 orig_A, orig_D = normcurve.at_pt([startparam, endparam])
902 A = orig_A[0] - dist * tangA[1], orig_A[1] + dist * tangA[0]
903 D = orig_D[0] - dist * tangD[1], orig_D[1] + dist * tangD[0]
905 # we need to end this _before_ we will run into epsilon-problems
906 # when creating curves we do not want to calculate the length of
907 # or even split it for recursive calls
908 if (math.hypot(A[0] - D[0], A[1] - D[1]) < epsilon and
909 math.hypot(tangA[0] - tangD[0], tangA[1] - tangD[1]) < T_threshold):
910 return normpath.normsubpath([normpath.normline_pt(A[0], A[1], D[0], D[1])], epsilon=epsilon)
912 result = normpath.normsubpath(epsilon=epsilon)
913 # is there enough space on the normals before they intersect?
914 a, d = intersection(orig_A, orig_D, (-tangA[1], tangA[0]), (-tangD[1], tangD[0]))
915 # a,d are the lengths to the intersection points:
916 # for a (and equally for b) we can proceed in one of the following cases:
917 # a is None (means parallel normals)
918 # a and dist have opposite signs (and the same for b)
919 # a has the same sign but is bigger
920 if ( (a is None or a*dist < 0 or abs(a) > abs(dist) + epsilon) or
921 (d is None or d*dist < 0 or abs(d) > abs(dist) + epsilon) ):
922 # the original path is long enough to draw a parallel piece
923 # this is the generic case. Get the parallel curves
924 orig_curvA, orig_curvD = normcurve.curvature_pt([startparam, endparam])
925 # normpath.invalid may not appear here because we have asked
926 # for this already at the tangents
927 assert orig_curvA is not normpath.invalid
928 assert orig_curvD is not normpath.invalid
929 curvA = orig_curvA / (1.0 - dist*orig_curvA)
930 curvD = orig_curvD / (1.0 - dist*orig_curvD)
932 # first try to approximate the normcurve with a single item
933 # TODO: is it good enough to get the first entry here?
934 # TODO: we rely on getting a result!
935 a, d = controldists_from_endgeometry_pt(A, D, tangA, tangD, curvA, curvD, epsilon=epsilon)[0]
936 if a >= 0 and d >= 0:
937 if a < epsilon and d < epsilon:
938 result = normpath.normsubpath([normpath.normline_pt(A[0], A[1], D[0], D[1])], epsilon=epsilon)
939 else:
940 # we avoid curves with invalid parameterization
941 a = max(a, epsilon)
942 d = max(d, epsilon)
943 result = normpath.normsubpath([normpath.normcurve_pt(
944 A[0], A[1],
945 A[0] + a * tangA[0], A[1] + a * tangA[1],
946 D[0] - d * tangD[0], D[1] - d * tangD[1],
947 D[0], D[1])], epsilon=epsilon)
949 # then try with two items, recursive call
950 if ((not result.normsubpathitems) or
951 (self.checkdistanceparams and result.normsubpathitems
952 and not self.distchecked(normcurve, result, epsilon, startparam, endparam))):
953 # TODO: does this ever converge?
954 # TODO: what if this hits epsilon?
955 firstnsp = self.deformnicecurve(normcurve, epsilon, startparam, 0.5*(startparam+endparam))
956 secondnsp = self.deformnicecurve(normcurve, epsilon, 0.5*(startparam+endparam), endparam)
957 if not (firstnsp.normsubpathitems and secondnsp.normsubpathitems):
958 result = normpath.normsubpath(
959 [normpath.normline_pt(A[0], A[1], D[0], D[1])], epsilon=epsilon)
960 else:
961 # we will get problems if the curves are too short:
962 result = firstnsp.joined(secondnsp)
964 return result
965 # >>>
967 def distchecked(self, orig_normcurve, parallel_normsubpath, epsilon, tstart, tend): # <<<
969 """Checks the distances between orig_normcurve and parallel_normsubpath
971 The checking is done at parameters self.checkdistanceparams of orig_normcurve."""
973 dist = self.dist_pt
974 # do not look closer than epsilon:
975 dist_relerr = helper.sign(dist) * max(abs(self.relerr*dist), epsilon)
977 checkdistanceparams = [tstart + (tend-tstart)*t for t in self.checkdistanceparams]
979 for param, P, rotation in zip(checkdistanceparams,
980 orig_normcurve.at_pt(checkdistanceparams),
981 orig_normcurve.rotation(checkdistanceparams)):
982 # check if the distance is really the wanted distance
983 # measure the distance in the "middle" of the original curve
984 if rotation is normpath.invalid:
985 raise InvalidParamException(param)
987 normal = rotation.apply_pt(0, 1)
989 # create a short cutline for intersection only:
990 cutline = normpath.normsubpath([normpath.normline_pt (
991 P[0] + (dist - 2*dist_relerr) * normal[0],
992 P[1] + (dist - 2*dist_relerr) * normal[1],
993 P[0] + (dist + 2*dist_relerr) * normal[0],
994 P[1] + (dist + 2*dist_relerr) * normal[1])], epsilon=epsilon)
996 cutparams = parallel_normsubpath.intersect(cutline)
997 distances = [math.hypot(P[0] - cutpoint[0], P[1] - cutpoint[1])
998 for cutpoint in cutline.at_pt(cutparams[1])]
1000 if (not distances) or (abs(min(distances) - abs(dist)) > abs(dist_relerr)):
1001 return 0
1003 return 1
1004 # >>>
1005 def distcrossingparameters(self, normcurve, epsilon, tstart=0, tend=1): # <<<
1007 """Returns a list of parameters where the curvature is 1/distance"""
1009 dist = self.dist_pt
1011 # we _need_ to do this with the curvature, not with the radius
1012 # because the curvature is continuous at the straight line and the radius is not:
1013 # when passing from one slightly curved curve to the other with opposite curvature sign,
1014 # via the straight line, then the curvature changes its sign at curv=0, while the
1015 # radius changes its sign at +/-infinity
1016 # this causes instabilities for nearly straight curves
1018 # include tstart and tend
1019 params = [tstart + i * (tend - tstart) * 1.0 / (self.lookforcurvatures - 1)
1020 for i in range(self.lookforcurvatures)]
1021 curvs = normcurve.curvature_pt(params)
1023 # break everything at invalid curvatures
1024 for param, curv in zip(params, curvs):
1025 if curv is normpath.invalid:
1026 raise InvalidParamException(param)
1028 parampairs = zip(params[:-1], params[1:])
1029 curvpairs = zip(curvs[:-1], curvs[1:])
1031 crossingparams = []
1032 for parampair, curvpair in zip(parampairs, curvpairs):
1033 begparam, endparam = parampair
1034 begcurv, endcurv = curvpair
1035 if (endcurv*dist - 1)*(begcurv*dist - 1) < 0:
1036 # the curvature crosses the value 1/dist
1037 # get the parmeter value by linear interpolation:
1038 middleparam = (
1039 (begparam * abs(begcurv*dist - 1) + endparam * abs(endcurv*dist - 1)) /
1040 (abs(begcurv*dist - 1) + abs(endcurv*dist - 1)))
1041 middleradius = normcurve.curveradius_pt([middleparam])[0]
1043 if middleradius is normpath.invalid:
1044 raise InvalidParamException(middleparam)
1046 if abs(middleradius - dist) < epsilon:
1047 # get the parmeter value by linear interpolation:
1048 crossingparams.append(middleparam)
1049 else:
1050 # call recursively:
1051 cps = self.distcrossingparameters(normcurve, epsilon, tstart=begparam, tend=endparam)
1052 crossingparams += cps
1054 return crossingparams
1055 # >>>
1056 def valid_near_rotation(self, nspitem, param, otherparam, stepsize, epsilon): # <<<
1057 p = param
1058 rot = nspitem.rotation([p])[0]
1059 # run towards otherparam searching for a valid rotation
1060 while rot is normpath.invalid:
1061 p = (1-stepsize)*p + stepsize*otherparam
1062 rot = nspitem.rotation([p])[0]
1063 # walk back to param until near enough
1064 # but do not go further if an invalid point is hit
1065 end, new = nspitem.at_pt([param, p])
1066 far = math.hypot(end[0]-new[0], end[1]-new[1])
1067 pnew = p
1068 while far > epsilon:
1069 pnew = (1-stepsize)*pnew + stepsize*param
1070 end, new = nspitem.at_pt([param, pnew])
1071 far = math.hypot(end[0]-new[0], end[1]-new[1])
1072 if nspitem.rotation([pnew])[0] is normpath.invalid:
1073 break
1074 else:
1075 p = pnew
1076 return p, nspitem.rotation([p])[0]
1077 # >>>
1078 def length_pt(self, path, param1, param2): # <<<
1079 point1, point2 = path.at_pt([param1, param2])
1080 return math.hypot(point1[0] - point2[0], point1[1] - point2[1])
1081 # >>>
1083 def normpath_selfintersections(self, np, epsilon): # <<<
1085 """return all self-intersection points of normpath np.
1087 This does not include the intersections of a single normcurve with itself,
1088 but all intersections of one normpathitem with a different one in the path"""
1090 n = len(np)
1091 linearparams = []
1092 parampairs = []
1093 paramsriap = {}
1094 for nsp_i in range(n):
1095 for nsp_j in range(nsp_i, n):
1096 for nspitem_i in range(len(np[nsp_i])):
1097 if nsp_j == nsp_i:
1098 nspitem_j_range = range(nspitem_i+1, len(np[nsp_j]))
1099 else:
1100 nspitem_j_range = range(len(np[nsp_j]))
1101 for nspitem_j in nspitem_j_range:
1102 #print "intersecting (%d,%d) with (%d,%d)" %(nsp_i, nspitem_i, nsp_j, nspitem_j)
1103 intsparams = np[nsp_i][nspitem_i].intersect(np[nsp_j][nspitem_j], epsilon)
1104 #if 1:#intsparams:
1105 # print np[nsp_i][nspitem_i]
1106 # print np[nsp_j][nspitem_j]
1107 # print intsparams
1108 if intsparams:
1109 for intsparam_i, intsparam_j in intsparams:
1110 if ( (abs(intsparam_i) < epsilon and abs(1-intsparam_j) < epsilon) or
1111 (abs(intsparam_j) < epsilon and abs(1-intsparam_i) < epsilon) ):
1112 continue
1113 npp_i = normpath.normpathparam(np, nsp_i, float(nspitem_i)+intsparam_i)
1114 npp_j = normpath.normpathparam(np, nsp_j, float(nspitem_j)+intsparam_j)
1115 linearparams.append(npp_i)
1116 linearparams.append(npp_j)
1117 paramsriap[npp_i] = len(parampairs)
1118 paramsriap[npp_j] = len(parampairs)
1119 parampairs.append((npp_i, npp_j))
1120 linearparams.sort()
1121 return linearparams, parampairs, paramsriap
1123 # >>>
1124 def can_continue(self, par_np, param1, param2): # <<<
1125 dist = self.dist_pt
1127 rot1, rot2 = par_np.rotation([param1, param2])
1128 if rot1 is normpath.invalid or rot2 is normpath.invalid:
1129 return 0
1130 curv1, curv2 = par_np.curvature_pt([param1, param2])
1131 tang2 = rot2.apply_pt(1, 0)
1132 norm1 = rot1.apply_pt(0, -1)
1133 norm1 = (dist*norm1[0], dist*norm1[1])
1135 # the self-intersection is valid if the tangents
1136 # point into the correct direction or, for parallel tangents,
1137 # if the curvature is such that the on-going path does not
1138 # enter the region defined by dist
1139 mult12 = norm1[0]*tang2[0] + norm1[1]*tang2[1]
1140 eps = 1.0e-6
1141 if abs(mult12) > eps:
1142 return (mult12 < 0)
1143 else:
1144 # tang1 and tang2 are parallel
1145 if curv2 is normpath.invalid or curv1 is normpath.invalid:
1146 return 0
1147 if dist > 0:
1148 return (curv2 <= curv1)
1149 else:
1150 return (curv2 >= curv1)
1151 # >>>
1152 def rebuild_intersected_normpath(self, par_np, orig_np, epsilon): # <<<
1154 dist = self.dist_pt
1156 # calculate the self-intersections of the par_np
1157 selfintparams, selfintpairs, selfintsriap = self.normpath_selfintersections(par_np, epsilon)
1158 # calculate the intersections of the par_np with the original path
1159 origintparams = par_np.intersect(orig_np)[0]
1161 # visualize the intersection points: # <<<
1162 if self.debug is not None:
1163 for param1, param2 in selfintpairs:
1164 point1, point2 = par_np.at([param1, param2])
1165 self.debug.fill(path.circle(point1[0], point1[1], 0.05), [color.rgb.red])
1166 self.debug.fill(path.circle(point2[0], point2[1], 0.03), [color.rgb.black])
1167 for param in origintparams:
1168 point = par_np.at([param])[0]
1169 self.debug.fill(path.circle(point[0], point[1], 0.05), [color.rgb.green])
1170 # >>>
1172 result = normpath.normpath()
1173 if not selfintparams:
1174 if origintparams:
1175 return result
1176 else:
1177 return par_np
1179 beginparams = []
1180 endparams = []
1181 for i in range(len(par_np)):
1182 beginparams.append(normpath.normpathparam(par_np, i, 0))
1183 endparams.append(normpath.normpathparam(par_np, i, len(par_np[i])))
1185 allparams = selfintparams + origintparams + beginparams + endparams
1186 allparams.sort()
1187 allparamindices = {}
1188 for i, param in enumerate(allparams):
1189 allparamindices[param] = i
1191 done = {}
1192 for param in allparams:
1193 done[param] = 0
1195 def otherparam(p): # <<<
1196 pair = selfintpairs[selfintsriap[p]]
1197 if (p is pair[0]):
1198 return pair[1]
1199 else:
1200 return pair[0]
1201 # >>>
1202 def trial_parampairs(startp): # <<<
1203 tried = {}
1204 for param in allparams:
1205 tried[param] = done[param]
1207 lastp = startp
1208 currentp = allparams[allparamindices[startp] + 1]
1209 result = []
1211 while 1:
1212 if currentp is startp:
1213 result.append((lastp, currentp))
1214 return result
1215 if currentp in selfintparams and otherparam(currentp) is startp:
1216 result.append((lastp, currentp))
1217 return result
1218 if currentp in endparams:
1219 result.append((lastp, currentp))
1220 return result
1221 if tried[currentp]:
1222 return []
1223 if currentp in origintparams:
1224 return []
1225 # follow the crossings on valid startpairs until
1226 # the normsubpath is closed or the end is reached
1227 if (currentp in selfintparams and
1228 self.can_continue(par_np, currentp, otherparam(currentp))):
1229 # go to the next pair on the curve, seen from currentpair[1]
1230 result.append((lastp, currentp))
1231 lastp = otherparam(currentp)
1232 tried[currentp] = 1
1233 tried[otherparam(currentp)] = 1
1234 currentp = allparams[allparamindices[otherparam(currentp)] + 1]
1235 else:
1236 # go to the next pair on the curve, seen from currentpair[0]
1237 tried[currentp] = 1
1238 tried[otherparam(currentp)] = 1
1239 currentp = allparams[allparamindices[currentp] + 1]
1240 assert 0
1241 # >>>
1243 # first the paths that start at the beginning of a subnormpath:
1244 for startp in beginparams + selfintparams:
1245 if done[startp]:
1246 continue
1248 parampairs = trial_parampairs(startp)
1249 if not parampairs:
1250 continue
1252 # collect all the pieces between parampairs
1253 add_nsp = normpath.normsubpath(epsilon=epsilon)
1254 for begin, end in parampairs:
1255 # check that trial_parampairs works correctly
1256 assert begin is not end
1257 # we do not cross the border of a normsubpath here
1258 assert begin.normsubpathindex is end.normsubpathindex
1259 for item in par_np[begin.normsubpathindex].segments(
1260 [begin.normsubpathparam, end.normsubpathparam])[0].normsubpathitems:
1261 # TODO: this should be obsolete with an improved intersecion algorithm
1262 # guaranteeing epsilon
1263 if add_nsp.normsubpathitems:
1264 item = item.modifiedbegin_pt(*(add_nsp.atend_pt()))
1265 add_nsp.append(item)
1267 if begin in selfintparams:
1268 done[begin] = 1
1269 #done[otherparam(begin)] = 1
1270 if end in selfintparams:
1271 done[end] = 1
1272 #done[otherparam(end)] = 1
1274 # eventually close the path
1275 if (parampairs[0][0] is parampairs[-1][-1] or
1276 (parampairs[0][0] in selfintparams and otherparam(parampairs[0][0]) is parampairs[-1][-1])):
1277 add_nsp.normsubpathitems[-1] = add_nsp.normsubpathitems[-1].modifiedend_pt(*add_nsp.atbegin_pt())
1278 add_nsp.close()
1280 result.extend([add_nsp])
1282 return result
1284 # >>>
1286 # >>>
1288 parallel.clear = attr.clearclass(parallel)
1290 # vim:foldmethod=marker:foldmarker=<<<,>>>