Code

noop: address ‘no newline at end of file’ warning
[inkscape.git] / src / sp-spiral.cpp
1 #define __SP_SPIRAL_C__
3 /** \file
4  * <sodipodi:spiral> implementation
5  */
6 /*
7  * Authors:
8  *   Mitsuru Oka <oka326@parkcity.ne.jp>
9  *   Lauris Kaplinski <lauris@kaplinski.com>
10  *
11  * Copyright (C) 1999-2002 Lauris Kaplinski
12  * Copyright (C) 2000-2001 Ximian, Inc.
13  *
14  * Released under GNU GPL, read the file 'COPYING' for more information
15  */
17 #include "config.h"
20 #include "svg/svg.h"
21 #include "attributes.h"
22 #include "display/bezier-utils.h"
23 #include "display/curve.h"
24 #include <glibmm/i18n.h>
25 #include "xml/repr.h"
26 #include "document.h"
28 #include "sp-spiral.h"
30 static void sp_spiral_class_init (SPSpiralClass *klass);
31 static void sp_spiral_init (SPSpiral *spiral);
33 static void sp_spiral_build (SPObject * object, SPDocument * document, Inkscape::XML::Node * repr);
34 static Inkscape::XML::Node *sp_spiral_write (SPObject *object, Inkscape::XML::Node *repr, guint flags);
35 static void sp_spiral_set (SPObject *object, unsigned int key, const gchar *value);
36 static void sp_spiral_update (SPObject *object, SPCtx *ctx, guint flags);
38 static gchar * sp_spiral_description (SPItem * item);
39 static void sp_spiral_snappoints(SPItem const *item, SnapPointsIter p);
41 static void sp_spiral_set_shape (SPShape *shape);
42 static void sp_spiral_update_patheffect (SPLPEItem *lpeitem, bool write);
44 static NR::Point sp_spiral_get_tangent (SPSpiral const *spiral, gdouble t);
46 static SPShapeClass *parent_class;
48 /**
49  * Register SPSpiral class and return its type number.
50  */
51 GType
52 sp_spiral_get_type (void)
53 {
54         static GType spiral_type = 0;
56         if (!spiral_type) {
57                 GTypeInfo spiral_info = {
58                         sizeof (SPSpiralClass),
59                         NULL,   /* base_init */
60                         NULL,   /* base_finalize */
61                         (GClassInitFunc) sp_spiral_class_init,
62                         NULL,   /* class_finalize */
63                         NULL,   /* class_data */
64                         sizeof (SPSpiral),
65                         16,     /* n_preallocs */
66                         (GInstanceInitFunc) sp_spiral_init,
67                         NULL,   /* value_table */
68                 };
69                 spiral_type = g_type_register_static (SP_TYPE_SHAPE, "SPSpiral", &spiral_info, (GTypeFlags)0);
70         }
71         return spiral_type;
72 }
74 /**
75  * SPSpiral vtable initialization.
76  */
77 static void
78 sp_spiral_class_init (SPSpiralClass *klass)
79 {
80         GObjectClass * gobject_class;
81         SPObjectClass * sp_object_class;
82         SPItemClass * item_class;
83         SPLPEItemClass * lpe_item_class;
84         SPShapeClass *shape_class;
86         gobject_class = (GObjectClass *) klass;
87         sp_object_class = (SPObjectClass *) klass;
88         item_class = (SPItemClass *) klass;
89         lpe_item_class = (SPLPEItemClass *) klass;
90         shape_class = (SPShapeClass *) klass;
92         parent_class = (SPShapeClass *)g_type_class_ref (SP_TYPE_SHAPE);
94         sp_object_class->build = sp_spiral_build;
95         sp_object_class->write = sp_spiral_write;
96         sp_object_class->set = sp_spiral_set;
97         sp_object_class->update = sp_spiral_update;
99         item_class->description = sp_spiral_description;
100         item_class->snappoints = sp_spiral_snappoints;
102     lpe_item_class->update_patheffect = sp_spiral_update_patheffect;
104     shape_class->set_shape = sp_spiral_set_shape;
107 /**
108  * Callback for SPSpiral object initialization.
109  */
110 static void
111 sp_spiral_init (SPSpiral * spiral)
113         spiral->cx         = 0.0;
114         spiral->cy         = 0.0;
115         spiral->exp        = 1.0;
116         spiral->revo       = 3.0;
117         spiral->rad        = 1.0;
118         spiral->arg        = 0.0;
119         spiral->t0         = 0.0;
122 /**
123  * Virtual build: set spiral properties from corresponding repr.
124  */
125 static void
126 sp_spiral_build (SPObject * object, SPDocument * document, Inkscape::XML::Node * repr)
128         if (((SPObjectClass *) parent_class)->build)
129                 ((SPObjectClass *) parent_class)->build (object, document, repr);
131         sp_object_read_attr (object, "sodipodi:cx");
132         sp_object_read_attr (object, "sodipodi:cy");
133         sp_object_read_attr (object, "sodipodi:expansion");
134         sp_object_read_attr (object, "sodipodi:revolution");
135         sp_object_read_attr (object, "sodipodi:radius");
136         sp_object_read_attr (object, "sodipodi:argument");
137         sp_object_read_attr (object, "sodipodi:t0");
140 /**
141  * Virtual write: write spiral attributes to corresponding repr.
142  */
143 static Inkscape::XML::Node *
144 sp_spiral_write (SPObject *object, Inkscape::XML::Node *repr, guint flags)
146         SPSpiral *spiral = SP_SPIRAL (object);
148         if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) {
149                 Inkscape::XML::Document *xml_doc = sp_document_repr_doc(SP_OBJECT_DOCUMENT(object));
150                 repr = xml_doc->createElement("svg:path");
151         }
153         if (flags & SP_OBJECT_WRITE_EXT) {
154                 /* Fixme: we may replace these attributes by
155                  * sodipodi:spiral="cx cy exp revo rad arg t0"
156                  */
157                 repr->setAttribute("sodipodi:type", "spiral");
158                 sp_repr_set_svg_double(repr, "sodipodi:cx", spiral->cx);
159                 sp_repr_set_svg_double(repr, "sodipodi:cy", spiral->cy);
160                 sp_repr_set_svg_double(repr, "sodipodi:expansion", spiral->exp);
161                 sp_repr_set_svg_double(repr, "sodipodi:revolution", spiral->revo);
162                 sp_repr_set_svg_double(repr, "sodipodi:radius", spiral->rad);
163                 sp_repr_set_svg_double(repr, "sodipodi:argument", spiral->arg);
164                 sp_repr_set_svg_double(repr, "sodipodi:t0", spiral->t0);
165         }
167      // make sure the curve is rebuilt with all up-to-date parameters
168      sp_spiral_set_shape ((SPShape *) spiral);
170         //Duplicate the path
171         SPCurve *curve = ((SPShape *) spiral)->curve;
172         //Nulls might be possible if this called iteratively
173         if ( !curve ) {
174                 //g_warning("sp_spiral_write(): No path to copy\n");
175                 return NULL;
176         }
177         NArtBpath *bpath = SP_CURVE_BPATH(curve);
178         if ( !bpath ) {
179                 //g_warning("sp_spiral_write(): No path to copy\n");
180                 return NULL;
181         }
182         char *d = sp_svg_write_path ( bpath );
183         repr->setAttribute("d", d);
184         g_free (d);
186         if (((SPObjectClass *) (parent_class))->write)
187                 ((SPObjectClass *) (parent_class))->write (object, repr, flags | SP_SHAPE_WRITE_PATH);
189         return repr;
192 /**
193  * Virtual set: change spiral object attribute.
194  */
195 static void
196 sp_spiral_set (SPObject *object, unsigned int key, const gchar *value)
198         SPSpiral *spiral;
199         SPShape  *shape;
201         spiral = SP_SPIRAL (object);
202         shape  = SP_SHAPE (object);
204         /// \todo fixme: we should really collect updates
205         switch (key) {
206         case SP_ATTR_SODIPODI_CX:
207                 if (!sp_svg_length_read_computed_absolute (value, &spiral->cx)) {
208                         spiral->cx = 0.0;
209                 }
210                 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
211                 break;
212         case SP_ATTR_SODIPODI_CY:
213                 if (!sp_svg_length_read_computed_absolute (value, &spiral->cy)) {
214                         spiral->cy = 0.0;
215                 }
216                 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
217                 break;
218         case SP_ATTR_SODIPODI_EXPANSION:
219                 if (value) {
220                         /** \todo
221                          * FIXME: check that value looks like a (finite)
222                          * number. Create a routine that uses strtod, and
223                          * accepts a default value (if strtod finds an error).
224                          * N.B. atof/sscanf/strtod consider "nan" and "inf"
225                          * to be valid numbers.
226                          */
227                         spiral->exp = g_ascii_strtod (value, NULL);
228                         spiral->exp = CLAMP (spiral->exp, 0.0, 1000.0);
229                 } else {
230                         spiral->exp = 1.0;
231                 }
232                 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
233                 break;
234         case SP_ATTR_SODIPODI_REVOLUTION:
235                 if (value) {
236                         spiral->revo = g_ascii_strtod (value, NULL);
237                         spiral->revo = CLAMP (spiral->revo, 0.05, 1024.0);
238                 } else {
239                         spiral->revo = 3.0;
240                 }
241                 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
242                 break;
243         case SP_ATTR_SODIPODI_RADIUS:
244                 if (!sp_svg_length_read_computed_absolute (value, &spiral->rad)) {
245                         spiral->rad = MAX (spiral->rad, 0.001);
246                 }
247                 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
248                 break;
249         case SP_ATTR_SODIPODI_ARGUMENT:
250                 if (value) {
251                         spiral->arg = g_ascii_strtod (value, NULL);
252                         /** \todo
253                          * FIXME: We still need some bounds on arg, for
254                          * numerical reasons. E.g., we don't want inf or NaN,
255                          * nor near-infinite numbers. I'm inclined to take
256                          * modulo 2*pi.  If so, then change the knot editors,
257                          * which use atan2 - revo*2*pi, which typically
258                          * results in very negative arg.
259                          */
260                 } else {
261                         spiral->arg = 0.0;
262                 }
263                 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
264                 break;
265         case SP_ATTR_SODIPODI_T0:
266                 if (value) {
267                         spiral->t0 = g_ascii_strtod (value, NULL);
268                         spiral->t0 = CLAMP (spiral->t0, 0.0, 0.999);
269                         /** \todo
270                          * Have shared constants for the allowable bounds for
271                          * attributes. There was a bug here where we used -1.0
272                          * as the minimum (which leads to NaN via, e.g.,
273                          * pow(-1.0, 0.5); see sp_spiral_get_xy for
274                          * requirements.
275                          */
276                 } else {
277                         spiral->t0 = 0.0;
278                 }
279                 object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
280                 break;
281         default:
282                 if (((SPObjectClass *) parent_class)->set)
283                         ((SPObjectClass *) parent_class)->set (object, key, value);
284                 break;
285         }
288 /**
289  * Virtual update callback.
290  */
291 static void
292 sp_spiral_update (SPObject *object, SPCtx *ctx, guint flags)
294         if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) {
295                 sp_shape_set_shape ((SPShape *) object);
296         }
298         if (((SPObjectClass *) parent_class)->update)
299                 ((SPObjectClass *) parent_class)->update (object, ctx, flags);
302 static void
303 sp_spiral_update_patheffect(SPLPEItem *lpeitem, bool write)
305     SPShape *shape = (SPShape *) lpeitem;
306     sp_spiral_set_shape(shape);
308     if (write) {
309         Inkscape::XML::Node *repr = SP_OBJECT_REPR(shape);
310         if ( shape->curve != NULL ) {
311             NArtBpath *abp = sp_curve_first_bpath(shape->curve);
312             if (abp) {
313                 gchar *str = sp_svg_write_path(abp);
314                 repr->setAttribute("d", str);
315                 g_free(str);
316             } else {
317                 repr->setAttribute("d", "");
318             }
319         } else {
320             repr->setAttribute("d", NULL);
321         }
322     }
324     ((SPObject *)shape)->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
327 /**
328  * Return textual description of spiral.
329  */
330 static gchar *
331 sp_spiral_description (SPItem * item)
333         // TRANSLATORS: since turn count isn't an integer, please adjust the
334         // string as needed to deal with an localized plural forms.
335         return g_strdup_printf (_("<b>Spiral</b> with %3f turns"), SP_SPIRAL(item)->revo);
339 /**
340  * Fit beziers together to spiral and draw it.
341  *
342  * \pre dstep \> 0.
343  * \pre is_unit_vector(*hat1).
344  * \post is_unit_vector(*hat2).
345  **/
346 static void
347 sp_spiral_fit_and_draw (SPSpiral const *spiral,
348                         SPCurve  *c,
349                         double dstep,
350                         NR::Point darray[],
351                         NR::Point const &hat1,
352                         NR::Point &hat2,
353                         double *t)
355 #define BEZIER_SIZE   4
356 #define FITTING_MAX_BEZIERS 4
357 #define BEZIER_LENGTH (BEZIER_SIZE * FITTING_MAX_BEZIERS)
358         g_assert (dstep > 0);
359         g_assert (is_unit_vector (hat1));
361         NR::Point bezier[BEZIER_LENGTH];
362         double d;
363         int depth, i;
365         for (d = *t, i = 0; i <= SAMPLE_SIZE; d += dstep, i++) {
366                 darray[i] = sp_spiral_get_xy(spiral, d);
368                 /* Avoid useless adjacent dups.  (Otherwise we can have all of darray filled with
369                    the same value, which upsets chord_length_parameterize.) */
370                 if ((i != 0)
371                     && (darray[i] == darray[i - 1])
372                     && (d < 1.0)) {
373                         i--;
374                         d += dstep;
375                         /** We mustn't increase dstep for subsequent values of
376                          * i: for large spiral.exp values, rate of growth
377                          * increases very rapidly.
378                          */
379                         /** \todo
380                          * Get the function itself to decide what value of d
381                          * to use next: ensure that we move at least 0.25 *
382                          * stroke width, for example.  The derivative (as used
383                          * for get_tangent before normalization) would be
384                          * useful for estimating the appropriate d value.  Or
385                          * perhaps just start with a small dstep and scale by
386                          * some small number until we move >= 0.25 *
387                          * stroke_width.  Must revert to the original dstep
388                          * value for next iteration to avoid the problem
389                          * mentioned above.
390                          */
391                 }
392         }
394         double const next_t = d - 2 * dstep;
395         /* == t + (SAMPLE_SIZE - 1) * dstep, in absence of dups. */
397         hat2 = -sp_spiral_get_tangent (spiral, next_t);
399         /** \todo
400          * We should use better algorithm to specify maximum error.
401          */
402         depth = sp_bezier_fit_cubic_full (bezier, NULL, darray, SAMPLE_SIZE,
403                                           hat1, hat2,
404                                           SPIRAL_TOLERANCE*SPIRAL_TOLERANCE,
405                                           FITTING_MAX_BEZIERS);
406         g_assert(depth * BEZIER_SIZE <= gint(G_N_ELEMENTS(bezier)));
407 #ifdef SPIRAL_DEBUG
408         if (*t == spiral->t0 || *t == 1.0)
409                 g_print ("[%s] depth=%d, dstep=%g, t0=%g, t=%g, arg=%g\n",
410                          debug_state, depth, dstep, spiral->t0, *t, spiral->arg);
411 #endif
412         if (depth != -1) {
413                 for (i = 0; i < 4*depth; i += 4) {
414                         sp_curve_curveto (c,
415                                           bezier[i + 1],
416                                           bezier[i + 2],
417                                           bezier[i + 3]);
418                 }
419         } else {
420 #ifdef SPIRAL_VERBOSE
421                 g_print ("cant_fit_cubic: t=%g\n", *t);
422 #endif
423                 for (i = 1; i < SAMPLE_SIZE; i++)
424                         sp_curve_lineto (c, darray[i]);
425         }
426         *t = next_t;
427         g_assert (is_unit_vector (hat2));
430 static void
431 sp_spiral_set_shape (SPShape *shape)
433         NR::Point darray[SAMPLE_SIZE + 1];
434         double t;
436         SPSpiral *spiral = SP_SPIRAL(shape);
438         SP_OBJECT (spiral)->requestModified(SP_OBJECT_MODIFIED_FLAG);
440         SPCurve *c = sp_curve_new ();
441         
442 #ifdef SPIRAL_VERBOSE
443         g_print ("cx=%g, cy=%g, exp=%g, revo=%g, rad=%g, arg=%g, t0=%g\n",
444                  spiral->cx,
445                  spiral->cy,
446                  spiral->exp,
447                  spiral->revo,
448                  spiral->rad,
449                  spiral->arg,
450                  spiral->t0);
451 #endif
453         /* Initial moveto. */
454         sp_curve_moveto(c, sp_spiral_get_xy(spiral, spiral->t0));
456         double const tstep = SAMPLE_STEP / spiral->revo;
457         double const dstep = tstep / (SAMPLE_SIZE - 1);
459         NR::Point hat1 = sp_spiral_get_tangent (spiral, spiral->t0);
460         NR::Point hat2;
461         for (t = spiral->t0; t < (1.0 - tstep);) {
462                 sp_spiral_fit_and_draw (spiral, c, dstep, darray, hat1, hat2, &t);
464                 hat1 = -hat2;
465         }
466         if ((1.0 - t) > SP_EPSILON)
467                 sp_spiral_fit_and_draw (spiral, c, (1.0 - t)/(SAMPLE_SIZE - 1.0),
468                                         darray, hat1, hat2, &t);
470     sp_lpe_item_perform_path_effect(SP_LPE_ITEM (spiral), c);
471     sp_shape_set_curve_insync ((SPShape *) spiral, c, TRUE);
472     sp_curve_unref (c);
475 /**
476  * Set spiral properties and update display.
477  */
478 void
479 sp_spiral_position_set       (SPSpiral          *spiral,
480                      gdouble            cx,
481                      gdouble            cy,
482                      gdouble            exp,
483                      gdouble            revo,
484                      gdouble            rad,
485                      gdouble            arg,
486                      gdouble            t0)
488         g_return_if_fail (spiral != NULL);
489         g_return_if_fail (SP_IS_SPIRAL (spiral));
491         /** \todo
492          * Consider applying CLAMP or adding in-bounds assertions for
493          * some of these parameters.
494          */
495         spiral->cx         = cx;
496         spiral->cy         = cy;
497         spiral->exp        = exp;
498         spiral->revo       = revo;
499         spiral->rad        = MAX (rad, 0.001);
500         spiral->arg        = arg;
501         spiral->t0         = CLAMP(t0, 0.0, 0.999);
502         
503         ((SPObject *)spiral)->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
506 /**
507  * Virtual snappoints callback.
508  */
509 static void sp_spiral_snappoints(SPItem const *item, SnapPointsIter p)
511         if (((SPItemClass *) parent_class)->snappoints) {
512                 ((SPItemClass *) parent_class)->snappoints (item, p);
513         }
516 /**
517  * Return one of the points on the spiral.
518  *
519  * \param t specifies how far along the spiral.
520  * \pre \a t in [0.0, 2.03].  (It doesn't make sense for t to be much more
521  * than 1.0, though some callers go slightly beyond 1.0 for curve-fitting
522  * purposes.)
523  */
524 NR::Point sp_spiral_get_xy (SPSpiral const *spiral, gdouble t)
526         g_assert (spiral != NULL);
527         g_assert (SP_IS_SPIRAL(spiral));
528         g_assert (spiral->exp >= 0.0);
529         /* Otherwise we get NaN for t==0. */
530         g_assert (spiral->exp <= 1000.0);
531         /* Anything much more results in infinities.  Even allowing 1000 is somewhat overkill. */
532         g_assert (t >= 0.0);
533         /* Any callers passing -ve t will have a bug for non-integral values of exp. */
535         double const rad = spiral->rad * pow(t, (double) spiral->exp);
536         double const arg = 2.0 * M_PI * spiral->revo * t + spiral->arg;
538         return NR::Point(rad * cos (arg) + spiral->cx,
539                          rad * sin (arg) + spiral->cy);
543 /**
544  * Returns the derivative of sp_spiral_get_xy with respect to t,
545  *  scaled to a unit vector.
546  *
547  *  \pre spiral != 0.
548  *  \pre 0 \<= t.
549  *  \pre p != NULL.
550  *  \post is_unit_vector(*p).
551  */
552 static NR::Point
553 sp_spiral_get_tangent (SPSpiral const *spiral, gdouble t)
555         NR::Point ret(1.0, 0.0);
556         g_return_val_if_fail (( ( spiral != NULL )
557                                 && SP_IS_SPIRAL(spiral) ),
558                               ret);
559         g_assert (t >= 0.0);
560         g_assert (spiral->exp >= 0.0);
561         /* See above for comments on these assertions. */
563         double const t_scaled = 2.0 * M_PI * spiral->revo * t;
564         double const arg = t_scaled + spiral->arg;
565         double const s = sin (arg);
566         double const c = cos (arg);
568         if (spiral->exp == 0.0) {
569                 ret = NR::Point(-s, c);
570         } else if (t_scaled == 0.0) {
571                 ret = NR::Point(c, s);
572         } else {
573                 NR::Point unrotated(spiral->exp, t_scaled);
574                 double const s_len = L2 (unrotated);
575                 g_assert (s_len != 0);
576                 /** \todo
577                  * Check that this isn't being too hopeful of the hypot
578                  * function.  E.g. test with numbers around 2**-1070
579                  * (denormalized numbers), preferably on a few different
580                  * platforms.  However, njh says that the usual implementation
581                  * does handle both very big and very small numbers.
582                  */
583                 unrotated /= s_len;
585                 /* ret = spiral->exp * (c, s) + t_scaled * (-s, c);
586                    alternatively ret = (spiral->exp, t_scaled) * (( c, s),
587                                                                   (-s, c)).*/
588                 ret = NR::Point(dot(unrotated, NR::Point(c, -s)),
589                                 dot(unrotated, NR::Point(s, c)));
590                 /* ret should already be approximately normalized: the
591                    matrix ((c, -s), (s, c)) is orthogonal (it just
592                    rotates by arg), and unrotated has been normalized,
593                    so ret is already of unit length other than numerical
594                    error in the above matrix multiplication. */
596                 /** \todo
597                  * I haven't checked how important it is for ret to be very
598                  * near unit length; we could get rid of the below.
599                  */
601                 ret.normalize();
602                 /* Proof that ret length is non-zero: see above.  (Should be near 1.) */
603         }
605         g_assert (is_unit_vector (ret));
606         return ret;
609 /**
610  * Compute rad and/or arg for point on spiral.
611  */
612 void
613 sp_spiral_get_polar (SPSpiral const *spiral, gdouble t, gdouble *rad, gdouble *arg)
615         g_return_if_fail (spiral != NULL);
616         g_return_if_fail (SP_IS_SPIRAL(spiral));
618         if (rad)
619                 *rad = spiral->rad * pow(t, (double) spiral->exp);
620         if (arg)
621                 *arg = 2.0 * M_PI * spiral->revo * t + spiral->arg;
624 /**
625  * Return true if spiral has properties that make it invalid.
626  */
627 bool
628 sp_spiral_is_invalid (SPSpiral const *spiral)
630         gdouble rad;
632         sp_spiral_get_polar (spiral, 0.0, &rad, NULL);
633         if (rad < 0.0 || rad > SP_HUGE) {
634                 g_print ("rad(t=0)=%g\n", rad);
635                 return TRUE;
636         }
637         sp_spiral_get_polar (spiral, 1.0, &rad, NULL);
638         if (rad < 0.0 || rad > SP_HUGE) {
639                 g_print ("rad(t=1)=%g\n", rad);
640                 return TRUE;
641         }
642         return FALSE;
645 /*
646   Local Variables:
647   mode:c++
648   c-file-style:"stroustrup"
649   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
650   indent-tabs-mode:nil
651   fill-column:99
652   End:
653 */
654 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :