Code

c4bbc31e10d57198c30bfed72b856171908a3257
[inkscape.git] / src / live_effects / effect.cpp
1 #define INKSCAPE_LIVEPATHEFFECT_CPP
3 /*
4  * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl>
5  *
6  * Released under GNU GPL, read the file 'COPYING' for more information
7  */
9 #include "live_effects/effect.h"
11 #include "display/display-forward.h"
12 #include "xml/node-event-vector.h"
13 #include "sp-object.h"
14 #include "attributes.h"
15 #include "message-stack.h"
16 #include "desktop.h"
17 #include "inkscape.h"
18 #include "document.h"
19 #include "document-private.h"
20 #include "xml/document.h"
21 #include <glibmm/i18n.h>
22 #include "pen-context.h"
23 #include "tools-switch.h"
24 #include "message-stack.h"
25 #include "desktop.h"
26 #include "nodepath.h"
28 #include "live_effects/lpeobject.h"
29 #include "live_effects/parameter/parameter.h"
30 #include <glibmm/ustring.h>
31 #include "libnr/n-art-bpath-2geom.h"
32 #include "display/curve.h"
33 #include <gtkmm.h>
35 #include <exception>
37 #include <2geom/sbasis-to-bezier.h>
38 #include <2geom/matrix.h>
41 // include effects:
42 #include "live_effects/lpe-patternalongpath.h"
43 #include "live_effects/lpe-bendpath.h"
44 #include "live_effects/lpe-sketch.h"
45 #include "live_effects/lpe-vonkoch.h"
46 #include "live_effects/lpe-knot.h"
47 #include "live_effects/lpe-test-doEffect-stack.h"
48 #include "live_effects/lpe-gears.h"
49 #include "live_effects/lpe-curvestitch.h"
50 #include "live_effects/lpe-circle_with_radius.h"
51 #include "live_effects/lpe-perspective_path.h"
52 #include "live_effects/lpe-spiro.h"
53 #include "live_effects/lpe-lattice.h"
54 #include "live_effects/lpe-envelope.h"
55 #include "live_effects/lpe-constructgrid.h"
56 #include "live_effects/lpe-perp_bisector.h"
57 #include "live_effects/lpe-tangent_to_curve.h"
58 #include "live_effects/lpe-mirror_symmetry.h"
59 #include "live_effects/lpe-circle_3pts.h"
60 #include "live_effects/lpe-angle_bisector.h"
61 #include "live_effects/lpe-parallel.h"
62 #include "live_effects/lpe-copy_rotate.h"
63 #include "live_effects/lpe-offset.h"
64 #include "live_effects/lpe-ruler.h"
65 #include "live_effects/lpe-boolops.h"
66 #include "live_effects/lpe-interpolate.h"
67 // end of includes
69 namespace Inkscape {
71 namespace LivePathEffect {
73 const Util::EnumData<EffectType> LPETypeData[] = {
74     // {constant defined in effect.h, N_("name of your effect"), "name of your effect in SVG"}
75     {ANGLE_BISECTOR,        N_("Angle bisector"),          "angle_bisector"},
76     {BEND_PATH,             N_("Bend"),                     "bend_path"},
77     {BOOLOPS,               N_("Boolops"),                 "boolops"},
78     {CIRCLE_WITH_RADIUS,    N_("Circle (center+radius)"),   "circle_with_radius"},
79     {CIRCLE_3PTS,           N_("Circle through 3 points"), "circle_3pts"},
80     {CONSTRUCT_GRID,        N_("Construct grid"),          "construct_grid"},
81 #ifdef LPE_ENABLE_TEST_EFFECTS
82     {DOEFFECTSTACK_TEST,    N_("doEffect stack test"),     "doeffectstacktest"},
83 #endif
84     {ENVELOPE,              N_("Envelope Deformation"),    "envelope"},
85     {FREEHAND_SHAPE,        N_("Freehand Shape"),          "freehand_shape"}, // this is actually a special type of PatternAlongPath, used to paste shapes in pen/pencil tool
86     {GEARS,                 N_("Gears"),                   "gears"},
87     {INTERPOLATE,           N_("Interpolate Sub-Paths"),   "interpolate"},
88     {KNOT,                  N_("Knot"),                    "knot"},
89     {LATTICE,               N_("Lattice Deformation"),     "lattice"},
90     {MIRROR_SYMMETRY,       N_("Mirror symmetry"),         "mirror_symmetry"},
91     {OFFSET,                N_("Offset"),                  "offset"},
92     {PARALLEL,              N_("Parallel"),                "parallel"},
93     {PATTERN_ALONG_PATH,    N_("Pattern Along Path"),      "skeletal"},   // for historic reasons, this effect is called skeletal(strokes) in Inkscape:SVG
94     {PERP_BISECTOR,         N_("Perpendicular bisector"),  "perp_bisector"},
95     {PERSPECTIVE_PATH,      N_("Perspective path"),        "perspective_path"},
96     {COPY_ROTATE,           N_("Rotate copies"),           "copy_rotate"},
97     {RULER,                 N_("Ruler"),                   "ruler"},
98     {SKETCH,                N_("Sketch"),                  "sketch"},
99     {SPIRO,                 N_("Spiro spline"),            "spiro"},
100     {CURVE_STITCH,          N_("Stitch Sub-Paths"),        "curvestitching"},
101     {TANGENT_TO_CURVE,      N_("Tangent to curve"),        "tangent_to_curve"},
102     {VONKOCH,               N_("VonKoch"),                 "vonkoch"},
103 };
104 const Util::EnumDataConverter<EffectType> LPETypeConverter(LPETypeData, sizeof(LPETypeData)/sizeof(*LPETypeData));
106 Effect*
107 Effect::New(EffectType lpenr, LivePathEffectObject *lpeobj)
109     Effect* neweffect = NULL;
110     switch (lpenr) {
111         case PATTERN_ALONG_PATH:
112             neweffect = static_cast<Effect*> ( new LPEPatternAlongPath(lpeobj) );
113             break;
114         case FREEHAND_SHAPE:
115             neweffect = static_cast<Effect*> ( new LPEFreehandShape(lpeobj) );
116             break;
117         case BEND_PATH:
118             neweffect = static_cast<Effect*> ( new LPEBendPath(lpeobj) );
119             break;
120         case SKETCH:
121             neweffect = static_cast<Effect*> ( new LPESketch(lpeobj) );
122             break;
123         case VONKOCH:
124             neweffect = static_cast<Effect*> ( new LPEVonKoch(lpeobj) );
125             break;
126         case KNOT:
127             neweffect = static_cast<Effect*> ( new LPEKnot(lpeobj) );
128             break;
129 #ifdef LPE_ENABLE_TEST_EFFECTS
130         case DOEFFECTSTACK_TEST:
131             neweffect = static_cast<Effect*> ( new LPEdoEffectStackTest(lpeobj) );
132             break;
133 #endif
134         case GEARS:
135             neweffect = static_cast<Effect*> ( new LPEGears(lpeobj) );
136             break;
137         case CURVE_STITCH:
138             neweffect = static_cast<Effect*> ( new LPECurveStitch(lpeobj) );
139             break;
140         case LATTICE:
141             neweffect = static_cast<Effect*> ( new LPELattice(lpeobj) );
142             break;
143         case ENVELOPE:
144             neweffect = static_cast<Effect*> ( new LPEEnvelope(lpeobj) );
145             break;
146         case CIRCLE_WITH_RADIUS:
147             neweffect = static_cast<Effect*> ( new LPECircleWithRadius(lpeobj) );
148             break;
149         case PERSPECTIVE_PATH:
150             neweffect = static_cast<Effect*> ( new LPEPerspectivePath(lpeobj) );
151             break;
152         case SPIRO:
153             neweffect = static_cast<Effect*> ( new LPESpiro(lpeobj) );
154             break;
155         case CONSTRUCT_GRID:
156             neweffect = static_cast<Effect*> ( new LPEConstructGrid(lpeobj) );
157             break;
158         case PERP_BISECTOR:
159             neweffect = static_cast<Effect*> ( new LPEPerpBisector(lpeobj) );
160             break;
161         case TANGENT_TO_CURVE:
162             neweffect = static_cast<Effect*> ( new LPETangentToCurve(lpeobj) );
163             break;
164         case MIRROR_SYMMETRY:
165             neweffect = static_cast<Effect*> ( new LPEMirrorSymmetry(lpeobj) );
166             break;
167         case CIRCLE_3PTS:
168             neweffect = static_cast<Effect*> ( new LPECircle3Pts(lpeobj) );
169             break;
170         case ANGLE_BISECTOR:
171             neweffect = static_cast<Effect*> ( new LPEAngleBisector(lpeobj) );
172             break;
173         case PARALLEL:
174             neweffect = static_cast<Effect*> ( new LPEParallel(lpeobj) );
175             break;
176         case COPY_ROTATE:
177             neweffect = static_cast<Effect*> ( new LPECopyRotate(lpeobj) );
178             break;
179         case OFFSET:
180             neweffect = static_cast<Effect*> ( new LPEOffset(lpeobj) );
181             break;
182         case RULER:
183             neweffect = static_cast<Effect*> ( new LPERuler(lpeobj) );
184             break;
185         case BOOLOPS:
186             neweffect = static_cast<Effect*> ( new LPEBoolops(lpeobj) );
187             break;
188         case INTERPOLATE:
189             neweffect = static_cast<Effect*> ( new LPEInterpolate(lpeobj) );
190             break;
191         default:
192             g_warning("LivePathEffect::Effect::New   called with invalid patheffect type (%d)", lpenr);
193             neweffect = NULL;
194             break;
195     }
197     if (neweffect) {
198         neweffect->readallParameters(SP_OBJECT_REPR(lpeobj));
199     }
201     return neweffect;
204 void
205 Effect::createAndApply(const char* name, SPDocument *doc, SPItem *item)
207     // Path effect definition
208     Inkscape::XML::Document *xml_doc = sp_document_repr_doc(doc);
209     Inkscape::XML::Node *repr = xml_doc->createElement("inkscape:path-effect");
210     repr->setAttribute("effect", name);
212     SP_OBJECT_REPR(SP_DOCUMENT_DEFS(doc))->addChild(repr, NULL); // adds to <defs> and assigns the 'id' attribute
213     const gchar * repr_id = repr->attribute("id");
214     Inkscape::GC::release(repr);
216     gchar *href = g_strdup_printf("#%s", repr_id);
217     sp_lpe_item_add_path_effect(SP_LPE_ITEM(item), href, true);
218     g_free(href);
221 void
222 Effect::createAndApply(EffectType type, SPDocument *doc, SPItem *item)
224     createAndApply(LPETypeConverter.get_key(type).c_str(), doc, item);
227 Effect::Effect(LivePathEffectObject *lpeobject)
228     : oncanvasedit_it(0),
229       is_visible(_("Is visible?"), _("If unchecked, the effect remains applied to the object but is temporarily disabled on canvas"), "is_visible", &wr, this, true),
230       done_pathparam_set(false),
231       show_orig_path(false),
232       lpeobj(lpeobject),
233       concatenate_before_pwd2(false),
234       provides_own_flash_paths(true) // is automatically set to false if providesOwnFlashPaths() is not overridden
236     registerParameter( dynamic_cast<Parameter *>(&is_visible) );
239 Effect::~Effect()
243 Glib::ustring
244 Effect::getName()
246     if (lpeobj->effecttype_set && LPETypeConverter.is_valid_id(lpeobj->effecttype) )
247         return Glib::ustring( _(LPETypeConverter.get_label(lpeobj->effecttype).c_str()) );
248     else
249         return Glib::ustring( _("No effect") );
252 EffectType
253 Effect::effectType() {
254     return lpeobj->effecttype;
257 /**
258  * Is performed a single time when the effect is freshly applied to a path
259  */
260 void
261 Effect::doOnApply (SPLPEItem */*lpeitem*/)
265 /**
266  * Is performed each time before the effect is updated.
267  */
268 void
269 Effect::doBeforeEffect (SPLPEItem */*lpeitem*/)
271     //Do nothing for simple effects
274 /**
275  * Effects can have a parameter path set before they are applied by accepting a nonzero number of
276  * mouse clicks. This method activates the pen context, which waits for the specified number of
277  * clicks. Override Effect::acceptsNumParams() to return the number of expected mouse clicks.
278  */
279 void
280 Effect::doAcceptPathPreparations(SPLPEItem *lpeitem)
282     // switch to pen context
283     SPDesktop *desktop = inkscape_active_desktop(); // TODO: Is there a better method to find the item's desktop?
284     if (!tools_isactive(desktop, TOOLS_FREEHAND_PEN)) {
285         tools_switch(desktop, TOOLS_FREEHAND_PEN);
286     }
288     SPEventContext *ec = desktop->event_context;
289     SPPenContext *pc = SP_PEN_CONTEXT(ec);
290     pc->expecting_clicks_for_LPE = this->acceptsNumParams();
291     pc->waiting_LPE = this;
292     pc->waiting_item = lpeitem;
293     pc->polylines_only = true;
295     ec->desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE,
296         g_strdup_printf(_("Please specify a parameter path for the LPE '%s' with %d mouse clicks"),
297                         getName().c_str(), acceptsNumParams()));
300 void
301 Effect::writeParamsToSVG() {
302     std::vector<Inkscape::LivePathEffect::Parameter *>::iterator p;
303     for (p = param_vector.begin(); p != param_vector.end(); ++p) {
304         (*p)->write_to_SVG();
305     }
308 /**
309  * If the effect expects a path parameter (specified by a number of mouse clicks) before it is
310  * applied, this is the method that processes the resulting path. Override it to customize it for
311  * your LPE. But don't forget to call the parent method so that done_pathparam_set is set to true!
312  */
313 void
314 Effect::acceptParamPath (SPPath */*param_path*/) {
315     done_pathparam_set = true;
318 /*
319  *  Here be the doEffect function chain:
320  */
321 void
322 Effect::doEffect (SPCurve * curve)
324     std::vector<Geom::Path> orig_pathv = curve->get_pathvector();
326     std::vector<Geom::Path> result_pathv = doEffect_path(orig_pathv);
328     curve->set_pathvector(result_pathv);
331 std::vector<Geom::Path>
332 Effect::doEffect_path (std::vector<Geom::Path> const & path_in)
334     std::vector<Geom::Path> path_out;
336     if ( !concatenate_before_pwd2 ) {
337         // default behavior
338         for (unsigned int i=0; i < path_in.size(); i++) {
339             Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_in = path_in[i].toPwSb();
340             Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_out = doEffect_pwd2(pwd2_in);
341             std::vector<Geom::Path> path = Geom::path_from_piecewise( pwd2_out, LPE_CONVERSION_TOLERANCE);
342             // add the output path vector to the already accumulated vector:
343             for (unsigned int j=0; j < path.size(); j++) {
344                 path_out.push_back(path[j]);
345             }
346         }
347     } else {
348       // concatenate the path into possibly discontinuous pwd2
349         Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_in;
350         for (unsigned int i=0; i < path_in.size(); i++) {
351             pwd2_in.concat( path_in[i].toPwSb() );
352         }
353         Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_out = doEffect_pwd2(pwd2_in);
354         path_out = Geom::path_from_piecewise( pwd2_out, LPE_CONVERSION_TOLERANCE);
355     }
357     return path_out;
360 Geom::Piecewise<Geom::D2<Geom::SBasis> >
361 Effect::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in)
363     g_warning("Effect has no doEffect implementation");
364     return pwd2_in;
367 void
368 Effect::readallParameters(Inkscape::XML::Node * repr)
370     std::vector<Parameter *>::iterator it = param_vector.begin();
371     while (it != param_vector.end()) {
372         Parameter * param = *it;
373         const gchar * key = param->param_key.c_str();
374         const gchar * value = repr->attribute(key);
375         if (value) {
376             bool accepted = param->param_readSVGValue(value);
377             if (!accepted) {
378                 g_warning("Effect::readallParameters - '%s' not accepted for %s", value, key);
379             }
380         } else {
381             // set default value
382             param->param_set_default();
383         }
385         it++;
386     }
389 /* This function does not and SHOULD NOT write to XML */
390 void
391 Effect::setParameter(const gchar * key, const gchar * new_value)
393     Parameter * param = getParameter(key);
394     if (param) {
395         if (new_value) {
396             bool accepted = param->param_readSVGValue(new_value);
397             if (!accepted) {
398                 g_warning("Effect::setParameter - '%s' not accepted for %s", new_value, key);
399             }
400         } else {
401             // set default value
402             param->param_set_default();
403         }
404     }
407 void
408 Effect::registerParameter(Parameter * param)
410     param_vector.push_back(param);
413 // TODO: should we provide a way to alter the handle's appearance?
414 void
415 Effect::registerKnotHolderHandle(KnotHolderEntity* entity, const char* descr)
417     kh_entity_vector.push_back(std::make_pair(entity, descr));
420 /**
421  * Add all registered LPE knotholder handles to the knotholder
422  */
423 void
424 Effect::addHandles(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) {
425     using namespace Inkscape::LivePathEffect;
427     // add handles provided by the effect itself
428     std::vector<std::pair<KnotHolderEntity*, const char*> >::iterator i;
429     for (i = kh_entity_vector.begin(); i != kh_entity_vector.end(); ++i) {
430         KnotHolderEntity *entity = i->first;
431         const char *descr = i->second;
433         entity->create(desktop, item, knotholder, descr);
434         knotholder->add(entity);
435     }
437     // add handles provided by the effect's parameters (if any)
438     for (std::vector<Parameter *>::iterator p = param_vector.begin(); p != param_vector.end(); ++p) {
439         (*p)->addKnotHolderEntities(knotholder, desktop, item);
440     }
443 void
444 Effect::addHelperPaths(SPLPEItem *lpeitem, SPDesktop *desktop)
446     g_return_if_fail(desktop);
447     g_return_if_fail(SP_IS_PATH(lpeitem));
449     if (providesKnotholder() && showOrigPath()) {
450         // TODO: we assume that if the LPE provides its own knotholder, there is no nodepath so we
451         // must create the helper curve for the original path manually; once we allow nodepaths and
452         // knotholders alongside each other, this needs to be rethought!
453         SPCanvasItem *canvasitem = sp_nodepath_generate_helperpath(desktop, SP_PATH(lpeitem));
454         Inkscape::Display::TemporaryItem* tmpitem = desktop->add_temporary_canvasitem (canvasitem, 0);
455         lpeitem->lpe_helperpaths.push_back(tmpitem);
456     }
458     for (std::vector<Parameter *>::iterator p = param_vector.begin(); p != param_vector.end(); ++p) {
459         if ( Inkscape::LivePathEffect::PathParam *pathparam = dynamic_cast<Inkscape::LivePathEffect::PathParam*>(*p) ) {
460             SPCurve *c = new SPCurve(pathparam->get_pathvector());
462             // TODO: factor this out (also the copied code above); see also lpe-lattice.cpp
463             SPCanvasItem *canvasitem = sp_nodepath_generate_helperpath(desktop, c, SP_ITEM(lpeitem), 0x009000ff);
464             Inkscape::Display::TemporaryItem* tmpitem = desktop->add_temporary_canvasitem (canvasitem, 0);
465             lpeitem->lpe_helperpaths.push_back(tmpitem);
466         }
467     }
469     addHelperPathsImpl(lpeitem, desktop);
472 void
473 Effect::addHelperPathsImpl(SPLPEItem */*lpeitem*/, SPDesktop */*desktop*/)
475     // if this method is overloaded in derived classes, provides_own_flash_paths will be true
476     provides_own_flash_paths = false;
479 /**
480  * This *creates* a new widget, management of deletion should be done by the caller
481  */
482 Gtk::Widget *
483 Effect::newWidget(Gtk::Tooltips * tooltips)
485     // use manage here, because after deletion of Effect object, others might still be pointing to this widget.
486     Gtk::VBox * vbox = Gtk::manage( new Gtk::VBox() );
488     vbox->set_border_width(5);
490     std::vector<Parameter *>::iterator it = param_vector.begin();
491     while (it != param_vector.end()) {
492         Parameter * param = *it;
493         Gtk::Widget * widg = param->param_newWidget(tooltips);
494         Glib::ustring * tip = param->param_getTooltip();
495         if (widg) {
496            vbox->pack_start(*widg, true, true, 2);
497             if (tip != NULL) {
498                 tooltips->set_tip(*widg, *tip);
499             }
500         }
502         it++;
503     }
505     return dynamic_cast<Gtk::Widget *>(vbox);
509 Inkscape::XML::Node *
510 Effect::getRepr()
512     return SP_OBJECT_REPR(lpeobj);
515 SPDocument *
516 Effect::getSPDoc()
518     if (SP_OBJECT_DOCUMENT(lpeobj) == NULL) g_message("Effect::getSPDoc() returns NULL");
519     return SP_OBJECT_DOCUMENT(lpeobj);
522 Parameter *
523 Effect::getParameter(const char * key)
525     Glib::ustring stringkey(key);
527     std::vector<Parameter *>::iterator it = param_vector.begin();
528     while (it != param_vector.end()) {
529         Parameter * param = *it;
530         if ( param->param_key == key) {
531             return param;
532         }
534         it++;
535     }
537     return NULL;
540 Parameter *
541 Effect::getNextOncanvasEditableParam()
543     if (param_vector.size() == 0) // no parameters
544         return NULL;
546     oncanvasedit_it++;
547     if (oncanvasedit_it >= static_cast<int>(param_vector.size())) {
548         oncanvasedit_it = 0;
549     }
550     int old_it = oncanvasedit_it;
552     do {
553         Parameter * param = param_vector[oncanvasedit_it];
554         if(param && param->oncanvas_editable) {
555             return param;
556         } else {
557             oncanvasedit_it++;
558             if (oncanvasedit_it == static_cast<int>(param_vector.size())) {  // loop round the map
559                 oncanvasedit_it = 0;
560             }
561         }
562     } while (oncanvasedit_it != old_it); // iterate until complete loop through map has been made
564     return NULL;
567 void
568 Effect::editNextParamOncanvas(SPItem * item, SPDesktop * desktop)
570     if (!desktop) return;
572     Parameter * param = getNextOncanvasEditableParam();
573     if (param) {
574         param->param_editOncanvas(item, desktop);
575         gchar *message = g_strdup_printf(_("Editing parameter <b>%s</b>."), param->param_label.c_str());
576         desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, message);
577         g_free(message);
578     } else {
579         desktop->messageStack()->flash( Inkscape::WARNING_MESSAGE,
580                                         _("None of the applied path effect's parameters can be edited on-canvas.") );
581     }
584 /* This function should reset the defaults and is used for example to initialize an effect right after it has been applied to a path
585 * The nice thing about this is that this function can use knowledge of the original path and set things accordingly for example to the size or origin of the original path!
586 */
587 void
588 Effect::resetDefaults(SPItem * /*item*/)
590     // do nothing for simple effects
593 void
594 Effect::setup_nodepath(Inkscape::NodePath::Path *np)
596     np->helperpath_rgba = 0xff0000ff;
597     np->helperpath_width = 1.0;
600 void
601 Effect::transform_multiply(Geom::Matrix const& postmul, bool set)
603     // cycle through all parameters. Most parameters will not need transformation, but path and point params do.
604     for (std::vector<Parameter *>::iterator it = param_vector.begin(); it != param_vector.end(); it++) {
605         Parameter * param = *it;
606         param->param_transform_multiply(postmul, set);
607     }
610 // TODO: take _all_ parameters into account, not only PointParams
611 bool
612 Effect::providesKnotholder()
614     // does the effect actively provide any knotholder entities of its own?
615     if (kh_entity_vector.size() > 0)
616         return true;
618     // otherwise: are there any PointParams?
619     for (std::vector<Parameter *>::iterator p = param_vector.begin(); p != param_vector.end(); ++p) {
620         if ( Inkscape::LivePathEffect::PointParam *pointparam = dynamic_cast<Inkscape::LivePathEffect::PointParam*>(*p) ) {
621             return true;
622         }
623     }
625     return false;
628 } /* namespace LivePathEffect */
630 } /* namespace Inkscape */
632 /*
633   Local Variables:
634   mode:c++
635   c-file-style:"stroustrup"
636   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
637   indent-tabs-mode:nil
638   fill-column:99
639   End:
640 */
641 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :