1 /*
2  * Authors:
3  *   Lauris Kaplinski <lauris@kaplinski.com>
4  *   Michael G. Sloan <mgsloan@gmail.com>
5  *
6  * This code is in public domain
7  */
8 
9 #include <2geom/affine.h>
10 #include <2geom/point.h>
11 #include <2geom/polynomial.h>
12 #include <2geom/utils.h>
13 
14 namespace Geom {
15 
16 /** Creates a Affine given an axis and origin point.
17  *  The axis is represented as two vectors, which represent skew, rotation, and scaling in two dimensions.
18  *  from_basis(Point(1, 0), Point(0, 1), Point(0, 0)) would return the identity matrix.
19 
20  \param x_basis the vector for the x-axis.
21  \param y_basis the vector for the y-axis.
22  \param offset the translation applied by the matrix.
23  \return The new Affine.
24  */
25 //NOTE: Inkscape's version is broken, so when including this version, you'll have to search for code with this func
from_basis(Point const & x_basis,Point const & y_basis,Point const & offset)26 Affine from_basis(Point const &x_basis, Point const &y_basis, Point const &offset) {
27     return Affine(x_basis[X], x_basis[Y],
28                   y_basis[X], y_basis[Y],
29                   offset [X], offset [Y]);
30 }
31 
xAxis() const32 Point Affine::xAxis() const {
33     return Point(_c[0], _c[1]);
34 }
35 
yAxis() const36 Point Affine::yAxis() const {
37     return Point(_c[2], _c[3]);
38 }
39 
40 /// Gets the translation imparted by the Affine.
translation() const41 Point Affine::translation() const {
42     return Point(_c[4], _c[5]);
43 }
44 
setXAxis(Point const & vec)45 void Affine::setXAxis(Point const &vec) {
46     for(int i = 0; i < 2; i++)
47         _c[i] = vec[i];
48 }
49 
setYAxis(Point const & vec)50 void Affine::setYAxis(Point const &vec) {
51     for(int i = 0; i < 2; i++)
52         _c[i + 2] = vec[i];
53 }
54 
55 /// Sets the translation imparted by the Affine.
setTranslation(Point const & loc)56 void Affine::setTranslation(Point const &loc) {
57     for(int i = 0; i < 2; i++)
58         _c[i + 4] = loc[i];
59 }
60 
61 /** Calculates the amount of x-scaling imparted by the Affine.  This is the scaling applied to
62  *  the original x-axis region.  It is \emph{not} the overall x-scaling of the transformation.
63  *  Equivalent to L2(m.xAxis()). */
expansionX() const64 double Affine::expansionX() const {
65     return sqrt(_c[0] * _c[0] + _c[1] * _c[1]);
66 }
67 
68 /** Calculates the amount of y-scaling imparted by the Affine.  This is the scaling applied before
69  *  the other transformations.  It is \emph{not} the overall y-scaling of the transformation.
70  *  Equivalent to L2(m.yAxis()). */
expansionY() const71 double Affine::expansionY() const {
72     return sqrt(_c[2] * _c[2] + _c[3] * _c[3]);
73 }
74 
setExpansionX(double val)75 void Affine::setExpansionX(double val) {
76     double exp_x = expansionX();
77     if (exp_x != 0.0) {  //TODO: best way to deal with it is to skip op?
78         double coef = val / expansionX();
79         for (unsigned i = 0; i < 2; ++i) {
80             _c[i] *= coef;
81         }
82     }
83 }
84 
setExpansionY(double val)85 void Affine::setExpansionY(double val) {
86     double exp_y = expansionY();
87     if (exp_y != 0.0) {  //TODO: best way to deal with it is to skip op?
88         double coef = val / expansionY();
89         for (unsigned i = 2; i < 4; ++i) {
90             _c[i] *= coef;
91         }
92     }
93 }
94 
95 /** Sets this matrix to be the Identity Affine. */
setIdentity()96 void Affine::setIdentity() {
97     _c[0] = 1.0; _c[1] = 0.0;
98     _c[2] = 0.0; _c[3] = 1.0;
99     _c[4] = 0.0; _c[5] = 0.0;
100 }
101 
102 /** @brief Check whether this matrix is an identity matrix.
103  * @param eps Numerical tolerance
104  * @return True iff the matrix is of the form
105  *         \f$\left[\begin{array}{ccc}
106            1 & 0 & 0 \\
107            0 & 1 & 0 \\
108            0 & 0 & 1 \end{array}\right]\f$ */
isIdentity(Coord eps) const109 bool Affine::isIdentity(Coord eps) const {
110     return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) &&
111            are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps) &&
112            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps);
113 }
114 
115 /** @brief Check whether this matrix represents a pure translation.
116  * Will return true for the identity matrix, which represents a zero translation.
117  * @param eps Numerical tolerance
118  * @return True iff the matrix is of the form
119  *         \f$\left[\begin{array}{ccc}
120            1 & 0 & 0 \\
121            0 & 1 & 0 \\
122            a & b & 1 \end{array}\right]\f$ */
isTranslation(Coord eps) const123 bool Affine::isTranslation(Coord eps) const {
124     return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) &&
125            are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps);
126 }
127 /** @brief Check whether this matrix represents a pure nonzero translation.
128  * @param eps Numerical tolerance
129  * @return True iff the matrix is of the form
130  *         \f$\left[\begin{array}{ccc}
131            1 & 0 & 0 \\
132            0 & 1 & 0 \\
133            a & b & 1 \end{array}\right]\f$ and \f$a, b \neq 0\f$ */
isNonzeroTranslation(Coord eps) const134 bool Affine::isNonzeroTranslation(Coord eps) const {
135     return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) &&
136            are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps) &&
137            (!are_near(_c[4], 0.0, eps) || !are_near(_c[5], 0.0, eps));
138 }
139 
140 /** @brief Check whether this matrix represents pure scaling.
141  * @param eps Numerical tolerance
142  * @return True iff the matrix is of the form
143  *         \f$\left[\begin{array}{ccc}
144            a & 0 & 0 \\
145            0 & b & 0 \\
146            0 & 0 & 1 \end{array}\right]\f$. */
isScale(Coord eps) const147 bool Affine::isScale(Coord eps) const {
148     if (isSingular(eps)) return false;
149     return are_near(_c[1], 0.0, eps) && are_near(_c[2], 0.0, eps) &&
150            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps);
151 }
152 
153 /** @brief Check whether this matrix represents pure, nonzero scaling.
154  * @param eps Numerical tolerance
155  * @return True iff the matrix is of the form
156  *         \f$\left[\begin{array}{ccc}
157            a & 0 & 0 \\
158            0 & b & 0 \\
159            0 & 0 & 1 \end{array}\right]\f$ and \f$a, b \neq 1\f$. */
isNonzeroScale(Coord eps) const160 bool Affine::isNonzeroScale(Coord eps) const {
161     if (isSingular(eps)) return false;
162     return (!are_near(_c[0], 1.0, eps) || !are_near(_c[3], 1.0, eps)) &&  //NOTE: these are the diags, and the next line opposite diags
163            are_near(_c[1], 0.0, eps) && are_near(_c[2], 0.0, eps) &&
164            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps);
165 }
166 
167 /** @brief Check whether this matrix represents pure uniform scaling.
168  * @param eps Numerical tolerance
169  * @return True iff the matrix is of the form
170  *         \f$\left[\begin{array}{ccc}
171            a_1 & 0 & 0 \\
172            0 & a_2 & 0 \\
173            0 & 0 & 1 \end{array}\right]\f$ where \f$|a_1| = |a_2|\f$. */
isUniformScale(Coord eps) const174 bool Affine::isUniformScale(Coord eps) const {
175     if (isSingular(eps)) return false;
176     return are_near(fabs(_c[0]), fabs(_c[3]), eps) &&
177            are_near(_c[1], 0.0, eps) && are_near(_c[2], 0.0, eps) &&
178            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps);
179 }
180 
181 /** @brief Check whether this matrix represents pure, nonzero uniform scaling.
182  * @param eps Numerical tolerance
183  * @return True iff the matrix is of the form
184  *         \f$\left[\begin{array}{ccc}
185            a_1 & 0 & 0 \\
186            0 & a_2 & 0 \\
187            0 & 0 & 1 \end{array}\right]\f$ where \f$|a_1| = |a_2|\f$
188  * and \f$a_1, a_2 \neq 1\f$. */
isNonzeroUniformScale(Coord eps) const189 bool Affine::isNonzeroUniformScale(Coord eps) const {
190     if (isSingular(eps)) return false;
191     // we need to test both c0 and c3 to handle the case of flips,
192     // which should be treated as nonzero uniform scales
193     return !(are_near(_c[0], 1.0, eps) && are_near(_c[3], 1.0, eps)) &&
194            are_near(fabs(_c[0]), fabs(_c[3]), eps) &&
195            are_near(_c[1], 0.0, eps) && are_near(_c[2], 0.0, eps) &&
196            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps);
197 }
198 
199 /** @brief Check whether this matrix represents a pure rotation.
200  * @param eps Numerical tolerance
201  * @return True iff the matrix is of the form
202  *         \f$\left[\begin{array}{ccc}
203            a & b & 0 \\
204            -b & a & 0 \\
205            0 & 0 & 1 \end{array}\right]\f$ and \f$a^2 + b^2 = 1\f$. */
isRotation(Coord eps) const206 bool Affine::isRotation(Coord eps) const {
207     return are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps) &&
208            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps) &&
209            are_near(_c[0]*_c[0] + _c[1]*_c[1], 1.0, eps);
210 }
211 
212 /** @brief Check whether this matrix represents a pure, nonzero rotation.
213  * @param eps Numerical tolerance
214  * @return True iff the matrix is of the form
215  *         \f$\left[\begin{array}{ccc}
216            a & b & 0 \\
217            -b & a & 0 \\
218            0 & 0 & 1 \end{array}\right]\f$, \f$a^2 + b^2 = 1\f$ and \f$a \neq 1\f$. */
isNonzeroRotation(Coord eps) const219 bool Affine::isNonzeroRotation(Coord eps) const {
220     return !are_near(_c[0], 1.0, eps) &&
221            are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps) &&
222            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps) &&
223            are_near(_c[0]*_c[0] + _c[1]*_c[1], 1.0, eps);
224 }
225 
226 /** @brief Check whether this matrix represents a non-zero rotation about any point.
227  * @param eps Numerical tolerance
228  * @return True iff the matrix is of the form
229  *         \f$\left[\begin{array}{ccc}
230            a & b & 0 \\
231            -b & a & 0 \\
232            c & d & 1 \end{array}\right]\f$, \f$a^2 + b^2 = 1\f$ and \f$a \neq 1\f$. */
isNonzeroNonpureRotation(Coord eps) const233 bool Affine::isNonzeroNonpureRotation(Coord eps) const {
234     return !are_near(_c[0], 1.0, eps) &&
235            are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps) &&
236            are_near(_c[0]*_c[0] + _c[1]*_c[1], 1.0, eps);
237 }
238 
239 /** @brief For a (possibly non-pure) non-zero-rotation matrix, calculate the rotation center.
240  * @pre The matrix must be a non-zero-rotation matrix to prevent division by zero, see isNonzeroNonpureRotation().
241  * @return The rotation center x, the solution to the equation
242  *         \f$A x = x\f$. */
rotationCenter() const243 Point Affine::rotationCenter() const {
244     Coord x = (_c[2]*_c[5]+_c[4]-_c[4]*_c[3]) / (1-_c[3]-_c[0]+_c[0]*_c[3]-_c[2]*_c[1]);
245     Coord y = (_c[1]*x + _c[5]) / (1 - _c[3]);
246     return Point(x,y);
247 };
248 
249 /** @brief Check whether this matrix represents pure horizontal shearing.
250  * @param eps Numerical tolerance
251  * @return True iff the matrix is of the form
252  *         \f$\left[\begin{array}{ccc}
253            1 & 0 & 0 \\
254            k & 1 & 0 \\
255            0 & 0 & 1 \end{array}\right]\f$. */
isHShear(Coord eps) const256 bool Affine::isHShear(Coord eps) const {
257     return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) &&
258            are_near(_c[3], 1.0, eps) && are_near(_c[4], 0.0, eps) &&
259            are_near(_c[5], 0.0, eps);
260 }
261 /** @brief Check whether this matrix represents pure, nonzero horizontal shearing.
262  * @param eps Numerical tolerance
263  * @return True iff the matrix is of the form
264  *         \f$\left[\begin{array}{ccc}
265            1 & 0 & 0 \\
266            k & 1 & 0 \\
267            0 & 0 & 1 \end{array}\right]\f$ and \f$k \neq 0\f$. */
isNonzeroHShear(Coord eps) const268 bool Affine::isNonzeroHShear(Coord eps) const {
269     return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) &&
270           !are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps) &&
271            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps);
272 }
273 
274 /** @brief Check whether this matrix represents pure vertical shearing.
275  * @param eps Numerical tolerance
276  * @return True iff the matrix is of the form
277  *         \f$\left[\begin{array}{ccc}
278            1 & k & 0 \\
279            0 & 1 & 0 \\
280            0 & 0 & 1 \end{array}\right]\f$. */
isVShear(Coord eps) const281 bool Affine::isVShear(Coord eps) const {
282     return are_near(_c[0], 1.0, eps) && are_near(_c[2], 0.0, eps) &&
283            are_near(_c[3], 1.0, eps) && are_near(_c[4], 0.0, eps) &&
284            are_near(_c[5], 0.0, eps);
285 }
286 
287 /** @brief Check whether this matrix represents pure, nonzero vertical shearing.
288  * @param eps Numerical tolerance
289  * @return True iff the matrix is of the form
290  *         \f$\left[\begin{array}{ccc}
291            1 & k & 0 \\
292            0 & 1 & 0 \\
293            0 & 0 & 1 \end{array}\right]\f$ and \f$k \neq 0\f$. */
isNonzeroVShear(Coord eps) const294 bool Affine::isNonzeroVShear(Coord eps) const {
295     return are_near(_c[0], 1.0, eps) && !are_near(_c[1], 0.0, eps) &&
296            are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps) &&
297            are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps);
298 }
299 
300 /** @brief Check whether this matrix represents zooming.
301  * Zooming is any combination of translation and uniform non-flipping scaling.
302  * It preserves angles, ratios of distances between arbitrary points
303  * and unit vectors of line segments.
304  * @param eps Numerical tolerance
305  * @return True iff the matrix is invertible and of the form
306  *         \f$\left[\begin{array}{ccc}
307            a & 0 & 0 \\
308            0 & a & 0 \\
309            b & c & 1 \end{array}\right]\f$. */
isZoom(Coord eps) const310 bool Affine::isZoom(Coord eps) const {
311     if (isSingular(eps)) return false;
312     return are_near(_c[0], _c[3], eps) && are_near(_c[1], 0, eps) && are_near(_c[2], 0, eps);
313 }
314 
315 /** @brief Check whether the transformation preserves areas of polygons.
316  * This means that the transformation can be any combination of translation, rotation,
317  * shearing and squeezing (non-uniform scaling such that the absolute value of the product
318  * of Y-scale and X-scale is 1).
319  * @param eps Numerical tolerance
320  * @return True iff \f$|\det A| = 1\f$. */
preservesArea(Coord eps) const321 bool Affine::preservesArea(Coord eps) const
322 {
323     return are_near(descrim2(), 1.0, eps);
324 }
325 
326 /** @brief Check whether the transformation preserves angles between lines.
327  * This means that the transformation can be any combination of translation, uniform scaling,
328  * rotation and flipping.
329  * @param eps Numerical tolerance
330  * @return True iff the matrix is of the form
331  *         \f$\left[\begin{array}{ccc}
332              a & b & 0 \\
333              -b & a & 0 \\
334              c & d & 1 \end{array}\right]\f$ or
335            \f$\left[\begin{array}{ccc}
336              -a & b & 0 \\
337              b & a & 0 \\
338              c & d & 1 \end{array}\right]\f$. */
preservesAngles(Coord eps) const339 bool Affine::preservesAngles(Coord eps) const
340 {
341     if (isSingular(eps)) return false;
342     return (are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps)) ||
343            (are_near(_c[0], -_c[3], eps) && are_near(_c[1], _c[2], eps));
344 }
345 
346 /** @brief Check whether the transformation preserves distances between points.
347  * This means that the transformation can be any combination of translation,
348  * rotation and flipping.
349  * @param eps Numerical tolerance
350  * @return True iff the matrix is of the form
351  *         \f$\left[\begin{array}{ccc}
352            a & b & 0 \\
353            -b & a & 0 \\
354            c & d & 1 \end{array}\right]\f$ or
355            \f$\left[\begin{array}{ccc}
356            -a & b & 0 \\
357            b & a & 0 \\
358            c & d & 1 \end{array}\right]\f$ and \f$a^2 + b^2 = 1\f$. */
preservesDistances(Coord eps) const359 bool Affine::preservesDistances(Coord eps) const
360 {
361     return ((are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps)) ||
362             (are_near(_c[0], -_c[3], eps) && are_near(_c[1], _c[2], eps))) &&
363            are_near(_c[0] * _c[0] + _c[1] * _c[1], 1.0, eps);
364 }
365 
366 /** @brief Check whether this transformation flips objects.
367  * A transformation flips objects if it has a negative scaling component. */
flips() const368 bool Affine::flips() const {
369     return det() < 0;
370 }
371 
372 /** @brief Check whether this matrix is singular.
373  * Singular matrices have no inverse, which means that applying them to a set of points
374  * results in a loss of information.
375  * @param eps Numerical tolerance
376  * @return True iff the determinant is near zero. */
isSingular(Coord eps) const377 bool Affine::isSingular(Coord eps) const {
378     return are_near(det(), 0.0, eps);
379 }
380 
381 /** @brief Compute the inverse matrix.
382  * Inverse is a matrix (denoted \f$A^{-1}\f$) such that \f$AA^{-1} = A^{-1}A = I\f$.
383  * Singular matrices have no inverse (for example a matrix that has two of its columns equal).
384  * For such matrices, the identity matrix will be returned instead.
385  * @param eps Numerical tolerance
386  * @return Inverse of the matrix, or the identity matrix if the inverse is undefined.
387  * @post (m * m.inverse()).isIdentity() == true */
inverse() const388 Affine Affine::inverse() const {
389     Affine d;
390 
391     double mx = std::max(fabs(_c[0]) + fabs(_c[1]),
392                          fabs(_c[2]) + fabs(_c[3])); // a random matrix norm (either l1 or linfty
393     if(mx > 0) {
394         Geom::Coord const determ = det();
395         if (!rel_error_bound(std::sqrt(fabs(determ)), mx)) {
396             Geom::Coord const ideterm = 1.0 / (determ);
397 
398             d._c[0] =  _c[3] * ideterm;
399             d._c[1] = -_c[1] * ideterm;
400             d._c[2] = -_c[2] * ideterm;
401             d._c[3] =  _c[0] * ideterm;
402             d._c[4] = (-_c[4] * d._c[0] - _c[5] * d._c[2]);
403             d._c[5] = (-_c[4] * d._c[1] - _c[5] * d._c[3]);
404         } else {
405             d.setIdentity();
406         }
407     } else {
408         d.setIdentity();
409     }
410 
411     return d;
412 }
413 
414 /** @brief Calculate the determinant.
415  * @return \f$\det A\f$. */
det() const416 Coord Affine::det() const {
417     // TODO this can overflow
418     return _c[0] * _c[3] - _c[1] * _c[2];
419 }
420 
421 /** @brief Calculate the square of the descriminant.
422  * This is simply the absolute value of the determinant.
423  * @return \f$|\det A|\f$. */
descrim2() const424 Coord Affine::descrim2() const {
425     return fabs(det());
426 }
427 
428 /** @brief Calculate the descriminant.
429  * If the matrix doesn't contain a shearing or non-uniform scaling component, this value says
430  * how will the length of any line segment change after applying this transformation
431  * to arbitrary objects on a plane. The new length will be
432  * @code line_seg.length() * m.descrim()) @endcode
433  * @return \f$\sqrt{|\det A|}\f$. */
descrim() const434 Coord Affine::descrim() const {
435     return sqrt(descrim2());
436 }
437 
438 /** @brief Combine this transformation with another one.
439  * After this operation, the matrix will correspond to the transformation
440  * obtained by first applying the original version of this matrix, and then
441  * applying @a m. */
operator *=(Affine const & o)442 Affine &Affine::operator*=(Affine const &o) {
443     Coord nc[6];
444     for(int a = 0; a < 5; a += 2) {
445         for(int b = 0; b < 2; b++) {
446             nc[a + b] = _c[a] * o._c[b] + _c[a + 1] * o._c[b + 2];
447         }
448     }
449     for(int a = 0; a < 6; ++a) {
450         _c[a] = nc[a];
451     }
452     _c[4] += o._c[4];
453     _c[5] += o._c[5];
454     return *this;
455 }
456 
457 //TODO: What's this!?!
458 /** Given a matrix m such that unit_circle = m*x, this returns the
459  * quadratic form x*A*x = 1.
460  * @relates Affine */
elliptic_quadratic_form(Affine const & m)461 Affine elliptic_quadratic_form(Affine const &m) {
462     double od = m[0] * m[1]  +  m[2] * m[3];
463     Affine ret (m[0]*m[0] + m[1]*m[1], od,
464                 od, m[2]*m[2] + m[3]*m[3],
465                 0, 0);
466     return ret; // allow NRVO
467 }
468 
Eigen(Affine const & m)469 Eigen::Eigen(Affine const &m) {
470     double const B = -m[0] - m[3];
471     double const C = m[0]*m[3] - m[1]*m[2];
472 
473     std::vector<double> v = solve_quadratic(1, B, C);
474 
475     for (unsigned i = 0; i < v.size(); ++i) {
476         values[i] = v[i];
477         vectors[i] = unit_vector(rot90(Point(m[0] - values[i], m[1])));
478     }
479     for (unsigned i = v.size(); i < 2; ++i) {
480         values[i] = 0;
481         vectors[i] = Point(0,0);
482     }
483 }
484 
Eigen(double m[2][2])485 Eigen::Eigen(double m[2][2]) {
486     double const B = -m[0][0] - m[1][1];
487     double const C = m[0][0]*m[1][1] - m[1][0]*m[0][1];
488 
489     std::vector<double> v = solve_quadratic(1, B, C);
490 
491     for (unsigned i = 0; i < v.size(); ++i) {
492         values[i] = v[i];
493         vectors[i] = unit_vector(rot90(Point(m[0][0] - values[i], m[0][1])));
494     }
495     for (unsigned i = v.size(); i < 2; ++i) {
496         values[i] = 0;
497         vectors[i] = Point(0,0);
498     }
499 }
500 
501 /** @brief Nearness predicate for affine transforms.
502  * @returns True if all entries of matrices are within eps of each other.
503  * @relates Affine */
are_near(Affine const & a,Affine const & b,Coord eps)504 bool are_near(Affine const &a, Affine const &b, Coord eps)
505 {
506     return are_near(a[0], b[0], eps) && are_near(a[1], b[1], eps) &&
507            are_near(a[2], b[2], eps) && are_near(a[3], b[3], eps) &&
508            are_near(a[4], b[4], eps) && are_near(a[5], b[5], eps);
509 }
510 
511 }  //namespace Geom
512 
513 /*
514   Local Variables:
515   mode:c++
516   c-file-style:"stroustrup"
517   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
518   indent-tabs-mode:nil
519   fill-column:99
520   End:
521 */
522 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
523