source: trunk/packages/vizservers/nanovis/FlowCmd.cpp @ 4167

Last change on this file since 4167 was 4106, checked in by ldelgass, 10 years ago

Commit change missing from r4105

  • Property svn:eol-style set to native
File size: 39.4 KB
Line 
1/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2/*
3 * Copyright (c) 2004-2013  HUBzero Foundation, LLC
4 *
5 */
6#include <assert.h>
7#define _OPEN_SYS
8#include <fcntl.h>
9#define _XOPEN_SOURCE_EXTENDED 1
10#include <sys/uio.h>
11#include <sys/stat.h>
12#include <stdlib.h>
13#include <stddef.h>
14#include <limits.h>
15#include <stdint.h>
16#include <unistd.h>
17#include <poll.h>
18
19#include <sstream>
20
21#include <tcl.h>
22
23#include <RpBuffer.h>
24
25#include <vrmath/Vector3f.h>
26
27#include "nvconf.h"
28
29#include "nanovisServer.h"
30#include "nanovis.h"
31#include "CmdProc.h"
32#include "Command.h"
33#include "PPMWriter.h"
34#ifdef USE_VTK
35#include "VtkDataSetReader.h"
36#else
37#include "VtkReader.h"
38#endif
39#include "FlowCmd.h"
40#include "FlowTypes.h"
41#include "Flow.h"
42#include "FlowBox.h"
43#include "FlowParticles.h"
44#include "Switch.h"
45#include "TransferFunction.h"
46#include "LIC.h"
47#include "Unirect.h"
48#include "VelocityArrowsSlice.h"
49#include "Volume.h"
50#include "Trace.h"
51
52using namespace nv;
53using namespace vrmath;
54
55static SwitchParseProc AxisSwitchProc;
56static SwitchCustom axisSwitch = {
57    AxisSwitchProc, NULL, 0,
58};
59
60static SwitchParseProc ColorSwitchProc;
61static SwitchCustom colorSwitch = {
62    ColorSwitchProc, NULL, 0,
63};
64
65static SwitchParseProc PointSwitchProc;
66static SwitchCustom pointSwitch = {
67    PointSwitchProc, NULL, 0,
68};
69
70static SwitchParseProc PositionSwitchProc;
71static SwitchCustom positionSwitch = {
72    PositionSwitchProc, NULL, 0,
73};
74
75static SwitchParseProc TransferFunctionSwitchProc;
76static SwitchCustom transferFunctionSwitch = {
77    TransferFunctionSwitchProc, NULL, 0,
78};
79
80SwitchSpec Flow::_switches[] = {
81    {SWITCH_FLOAT, "-ambient", "value",
82     offsetof(FlowValues, ambient), 0},
83    {SWITCH_BOOLEAN, "-arrows", "boolean",
84     offsetof(FlowValues, showArrows), 0},
85    {SWITCH_CUSTOM, "-axis", "axis",
86     offsetof(FlowValues, slicePos.axis), 0, 0, &axisSwitch},
87    {SWITCH_FLOAT, "-diffuse", "value",
88     offsetof(FlowValues, diffuse), 0},
89    {SWITCH_BOOLEAN, "-hide", "boolean",
90     offsetof(FlowValues, isHidden), 0},
91    {SWITCH_BOOLEAN, "-light2side", "boolean",
92     offsetof(FlowValues, twoSidedLighting), 0},
93    {SWITCH_FLOAT, "-opacity", "value",
94     offsetof(FlowValues, opacity), 0},
95    {SWITCH_BOOLEAN, "-outline", "boolean",
96     offsetof(FlowValues, showOutline), 0},
97    {SWITCH_CUSTOM, "-position", "number",
98     offsetof(FlowValues, slicePos), 0, 0, &positionSwitch},
99    {SWITCH_BOOLEAN, "-slice", "boolean",
100     offsetof(FlowValues, sliceVisible), 0},
101    {SWITCH_FLOAT, "-specularExp", "value",
102     offsetof(FlowValues, specularExp), 0},
103    {SWITCH_FLOAT, "-specularLevel", "value",
104     offsetof(FlowValues, specular), 0},
105    {SWITCH_CUSTOM, "-transferfunction", "name",
106     offsetof(FlowValues, transferFunction), 0, 0, &transferFunctionSwitch},
107    {SWITCH_BOOLEAN, "-volume", "boolean",
108     offsetof(FlowValues, showVolume), 0},
109    {SWITCH_END}
110};
111
112SwitchSpec FlowParticles::_switches[] = {
113    {SWITCH_CUSTOM, "-axis", "axis",
114     offsetof(FlowParticlesValues, position.axis), 0, 0, &axisSwitch},
115    {SWITCH_CUSTOM, "-color", "{r g b a}",
116     offsetof(FlowParticlesValues, color), 0, 0,  &colorSwitch},
117    {SWITCH_BOOLEAN, "-hide", "boolean",
118     offsetof(FlowParticlesValues, isHidden), 0},
119    {SWITCH_CUSTOM, "-position", "number",
120     offsetof(FlowParticlesValues, position), 0, 0, &positionSwitch},
121    {SWITCH_FLOAT, "-size", "float",
122     offsetof(FlowParticlesValues, particleSize), 0},
123    {SWITCH_END}
124};
125
126SwitchSpec FlowBox::_switches[] = {
127    {SWITCH_CUSTOM, "-color", "{r g b a}",
128     offsetof(FlowBoxValues, color), 0, 0,  &colorSwitch},
129    {SWITCH_CUSTOM, "-corner1", "{x y z}",
130     offsetof(FlowBoxValues, corner1), 0, 0, &pointSwitch},
131    {SWITCH_CUSTOM, "-corner2", "{x y z}",
132     offsetof(FlowBoxValues, corner2), 0, 0, &pointSwitch},
133    {SWITCH_BOOLEAN, "-hide", "boolean",
134     offsetof(FlowBoxValues, isHidden), 0},
135    {SWITCH_FLOAT, "-linewidth", "number",
136     offsetof(FlowBoxValues, lineWidth), 0},
137    {SWITCH_END}
138};
139
140/**
141 * $flow data follows nbytes nComponents
142 */
143static int
144FlowDataFollowsOp(ClientData clientData, Tcl_Interp *interp, int objc,
145                  Tcl_Obj *const *objv)
146{
147    TRACE("Enter");
148
149    int nBytes;
150    if (Tcl_GetIntFromObj(interp, objv[3], &nBytes) != TCL_OK) {
151        ERROR("Bad nBytes \"%s\"", Tcl_GetString(objv[3]));
152        return TCL_ERROR;
153    }
154    if (nBytes <= 0) {
155        Tcl_AppendResult(interp, "bad # bytes request \"",
156                         Tcl_GetString(objv[3]), "\" for \"data follows\"", (char *)NULL);
157        ERROR("Bad nbytes %d", nBytes);
158        return TCL_ERROR;
159    }
160    int nComponents;
161    if (Tcl_GetIntFromObj(interp, objv[4], &nComponents) != TCL_OK) {
162        ERROR("Bad # of components \"%s\"", Tcl_GetString(objv[4]));
163        return TCL_ERROR;
164    }
165    if (nComponents <= 0) {
166        Tcl_AppendResult(interp, "bad # of components request \"",
167                         Tcl_GetString(objv[4]), "\" for \"data follows\"", (char *)NULL);
168        ERROR("Bad # of components %d", nComponents);
169        return TCL_ERROR;
170    }
171    Rappture::Buffer buf(nBytes);
172    TRACE("Flow data loading bytes: %d components: %d", nBytes, nComponents);
173    if (GetDataStream(interp, buf, nBytes) != TCL_OK) {
174        return TCL_ERROR;
175    }
176    char *bytes = (char *)buf.bytes();
177    size_t length = buf.size();
178
179    Unirect3d *unirect = NULL;
180    Volume *volume = NULL;
181
182    Flow *flow = (Flow *)clientData;
183    if ((length > 4) && (strncmp(bytes, "<DX>", 4) == 0)) {
184#ifdef USE_DX_READER
185        unirect = new Unirect3d(nComponents);
186        if (!unirect->importDx(nComponents, length - 4, bytes + 4)) {
187            Tcl_AppendResult(interp, "Failed to load DX file", (char *)NULL);
188            delete unirect;
189            return TCL_ERROR;
190        }
191#else
192        Tcl_AppendResult(interp, "Loading DX files is not supported by this server", (char*)NULL);
193        return TCL_ERROR;
194#endif
195    } else if ((length > 10) && (strncmp(bytes, "unirect3d ", 10) == 0)) {
196        unirect = new Unirect3d(nComponents);
197        if (unirect->parseBuffer(interp, bytes, length) != TCL_OK) {
198            delete unirect;
199            return TCL_ERROR;
200        }
201    } else if ((length > 10) && (strncmp(bytes, "unirect2d ", 10) == 0)) {
202        unirect = new Unirect3d(nComponents);
203        Unirect2d *u2dPtr;
204        u2dPtr = new Unirect2d(nComponents);
205        if (u2dPtr->parseBuffer(interp, bytes, length) != TCL_OK) {
206            delete unirect;
207            delete u2dPtr;
208            return TCL_ERROR;
209        }
210        unirect->convert(u2dPtr);
211        delete u2dPtr;
212    } else if ((length > 14) && (strncmp(bytes, "# vtk DataFile", 14) == 0)) {
213        TRACE("VTK loading...");
214        if (length <= 0) {
215            ERROR("data buffer is empty");
216            abort();
217        }
218#ifdef USE_VTK
219        volume = load_vtk_volume_stream(flow->name(), bytes, length);
220#else
221        std::stringstream fdata;
222        fdata.write(bytes, length);
223        volume = load_vtk_volume_stream(flow->name(), fdata);
224#endif
225        if (volume == NULL) {
226            Tcl_AppendResult(interp, "Failed to load VTK file", (char*)NULL);
227            return TCL_ERROR;
228        }
229    } else {
230#ifdef USE_DX_READER
231        TRACE("header is %.14s", buf.bytes());
232        unirect = new Unirect3d(nComponents);
233        if (!unirect->importDx(nComponents, length, bytes)) {
234            Tcl_AppendResult(interp, "Failed to load DX file", (char *)NULL);
235            delete unirect;
236            return TCL_ERROR;
237        }
238#else
239        Tcl_AppendResult(interp, "Loading DX files is not supported by this server", (char*)NULL);
240        return TCL_ERROR;
241#endif
242    }
243    if (unirect != NULL && unirect->nValues() == 0) {
244        delete unirect;
245        Tcl_AppendResult(interp, "no data found in stream", (char *)NULL);
246        return TCL_ERROR;
247    }
248    if (unirect != NULL) {
249        TRACE("nx = %d ny = %d nz = %d",
250              unirect->xNum(), unirect->yNum(), unirect->zNum());
251        TRACE("x0 = %lg y0 = %lg z0 = %lg",
252              unirect->xMin(), unirect->yMin(), unirect->zMin());
253        TRACE("lx = %lg ly = %lg lz = %lg",
254              unirect->xMax() - unirect->xMin(),
255              unirect->yMax() - unirect->yMin(),
256              unirect->zMax() - unirect->zMin());
257        TRACE("dx = %lg dy = %lg dz = %lg",
258              unirect->xNum() > 1 ? (unirect->xMax() - unirect->xMin())/(unirect->xNum()-1) : 0,
259              unirect->yNum() > 1 ? (unirect->yMax() - unirect->yMin())/(unirect->yNum()-1) : 0,
260              unirect->zNum() > 1 ? (unirect->zMax() - unirect->zMin())/(unirect->zNum()-1) : 0);
261        TRACE("magMin = %lg magMax = %lg",
262              unirect->magMin(), unirect->magMax());
263
264        flow->data(unirect);
265    } else {
266        flow->data(volume);
267    }
268    double range[2];
269    flow->getVectorRange(range);
270    {
271        char info[1024];
272        int length =
273            sprintf(info, "nv>data tag %s min %g max %g\n",
274                    flow->name(), range[0], range[1]);
275#ifdef USE_THREADS
276        queueResponse(info, (size_t)length, Response::VOLATILE);
277#else
278        if (SocketWrite(info, (size_t)length) < 0) {
279            return TCL_ERROR;
280        }
281#endif
282    }
283    Flow::updatePending = true;
284    NanoVis::eventuallyRedraw();
285    return TCL_OK;
286}
287
288static CmdSpec flowDataOps[] = {
289    {"follows", 2, FlowDataFollowsOp, 5, 5, "size nComponents",},
290};
291static int nFlowDataOps = NumCmdSpecs(flowDataOps);
292
293static int
294FlowDataOp(ClientData clientData, Tcl_Interp *interp, int objc,
295           Tcl_Obj *const *objv)
296{
297    Tcl_ObjCmdProc *proc;
298
299    proc = GetOpFromObj(interp, nFlowDataOps, flowDataOps,
300                        CMDSPEC_ARG2, objc, objv, 0);
301    if (proc == NULL) {
302        return TCL_ERROR;
303    }
304    return (*proc) (clientData, interp, objc, objv);
305}
306
307/**
308 * \brief Convert a Tcl_Obj representing the label of a child node into its
309 * integer node id.
310 *
311 * \param clientData Flag indicating if the node is considered before or
312 * after the insertion position.
313 * \param interp Interpreter to send results back to
314 * \param switchName Not used
315 * \param objPtr String representation
316 * \param record Structure record
317 * \param offset Not used
318 * \param flags Not used
319 *
320 * \return The return value is a standard Tcl result.
321 */
322static int
323AxisSwitchProc(ClientData clientData, Tcl_Interp *interp,
324               const char *switchName, Tcl_Obj *objPtr,
325               char *record, int offset, int flags)
326{
327    const char *string = Tcl_GetString(objPtr);
328    if (string[1] == '\0') {
329        FlowSliceAxis *axisPtr = (FlowSliceAxis *)(record + offset);
330        char c;
331        c = tolower((unsigned char)string[0]);
332        if (c == 'x') {
333            *axisPtr = AXIS_X;
334            return TCL_OK;
335        } else if (c == 'y') {
336            *axisPtr = AXIS_Y;
337            return TCL_OK;
338        } else if (c == 'z') {
339            *axisPtr = AXIS_Z;
340            return TCL_OK;
341        }
342        /*FALLTHRU*/
343    }
344    Tcl_AppendResult(interp, "bad axis \"", string,
345                     "\": should be x, y, or z", (char*)NULL);
346    return TCL_ERROR;
347}
348
349/**
350 * \brief Convert a Tcl_Obj representing the label of a list of four color
351 * components in to a RGBA color value.
352 *
353 * \param clientData Flag indicating if the node is considered before or
354 * after the insertion position.
355 * \param interp Interpreter to send results back to
356 * \param switchName Not used
357 * \param objPtr String representation
358 * \param record Structure record
359 * \param offset Not used
360 * \param flags Not used
361 *
362 * \return The return value is a standard Tcl result.
363 */
364static int
365ColorSwitchProc(ClientData clientData, Tcl_Interp *interp,
366                const char *switchName, Tcl_Obj *objPtr,
367                char *record, int offset, int flags)
368{
369    Tcl_Obj **objv;
370    int objc;
371    FlowColor *color = (FlowColor *)(record + offset);
372
373    if (Tcl_ListObjGetElements(interp, objPtr, &objc, &objv) != TCL_OK) {
374        return TCL_ERROR;
375    }
376    if ((objc < 3) || (objc > 4)) {
377        Tcl_AppendResult(interp, "wrong # of elements in color definition",
378                         (char *)NULL);
379        return TCL_ERROR;
380    }
381    float values[4];
382    int i;
383    values[3] = 1.0f;
384    for (i = 0; i < objc; i++) {
385        float value;
386
387        if (GetFloatFromObj(interp, objv[i], &value) != TCL_OK) {
388            return TCL_ERROR;
389        }
390        if ((value < 0.0) || (value > 1.0)) {
391            Tcl_AppendResult(interp, "bad component value in \"",
392                             Tcl_GetString(objPtr), "\": color values must be [0..1]",
393                             (char *)NULL);
394            return TCL_ERROR;
395        }
396        values[i] = value;
397    }
398    color->r = values[0];
399    color->g = values[1];
400    color->b = values[2];
401    color->a = values[3];
402    return TCL_OK;
403}
404
405/**
406 * \brief Convert a Tcl_Obj representing the a 3-D coordinate into a point.
407 *
408 * \param clientData Flag indicating if the node is considered before or
409 * after the insertion position.
410 * \param interp Interpreter to send results back to
411 * \param switchName Not used
412 * \param objPtr String representation
413 * \param record Structure record
414 * \param offset Not used
415 * \param flags Not used
416 *
417 * \return The return value is a standard Tcl result.
418 */
419static int
420PointSwitchProc(ClientData clientData, Tcl_Interp *interp,
421                const char *switchName, Tcl_Obj *objPtr,
422                char *record, int offset, int flags)
423{
424    FlowPoint *point = (FlowPoint *)(record + offset);
425    int objc;
426    Tcl_Obj **objv;
427
428    if (Tcl_ListObjGetElements(interp, objPtr, &objc, &objv) != TCL_OK) {
429        return TCL_ERROR;
430    }
431    if (objc != 3) {
432        Tcl_AppendResult(interp, "wrong # of elements for box coordinates: "
433                         " should be \"x y z\"", (char *)NULL);
434        return TCL_ERROR;
435    }
436    float values[3];
437    int i;
438    for (i = 0; i < objc; i++) {
439        float value;
440
441        if (GetFloatFromObj(interp, objv[i], &value) != TCL_OK) {
442            return TCL_ERROR;
443        }
444        values[i] = value;
445    }
446    point->x = values[0];
447    point->y = values[1];
448    point->z = values[2];
449    return TCL_OK;
450}
451
452/**
453 * \brief Convert a Tcl_Obj representing the a 3-D coordinate into a point.
454 *
455 * \param clientData Flag indicating if the node is considered before or
456 * after the insertion position.
457 * \param interp Interpreter to send results back to
458 * \param switchName Not used
459 * \param objPtr String representation
460 * \param record Structure record
461 * \param offset Not used
462 * \param flags Not used
463 *
464 * \return The return value is a standard Tcl result.
465 */
466static int
467PositionSwitchProc(ClientData clientData, Tcl_Interp *interp,
468                   const char *switchName, Tcl_Obj *objPtr,
469                   char *record, int offset, int flags)
470{
471    FlowPosition *posPtr = (FlowPosition *)(record + offset);
472    const char *string;
473    char *p;
474
475    string = Tcl_GetString(objPtr);
476    p = strrchr((char *)string, '%');
477    if (p == NULL) {
478        float value;
479
480        if (GetFloatFromObj(interp, objPtr, &value) != TCL_OK) {
481            return TCL_ERROR;
482        }
483        posPtr->value = value;
484        posPtr->flags = ABSPOS;
485    } else {
486        double value;
487
488        *p = '\0';
489        if (Tcl_GetDouble(interp, string, &value) != TCL_OK) {
490            return TCL_ERROR;
491        }
492        posPtr->value = (float)value * 0.01;
493        posPtr->flags = RELPOS;
494    }
495    return TCL_OK;
496}
497
498/**
499 * \brief Convert a Tcl_Obj representing the transfer function into a
500 *  TransferFunction pointer. 
501 *
502 * The transfer function must have been previously defined.
503 *
504 * \param clientData Flag indicating if the node is considered before or
505 * after the insertion position.
506 * \param interp Interpreter to send results back to
507 * \param switchName Not used
508 * \param objPtr String representation
509 * \param record Structure record
510 * \param offset Not used
511 * \param flags Not used
512 *
513 * \return The return value is a standard Tcl result.
514 */
515static int
516TransferFunctionSwitchProc(ClientData clientData, Tcl_Interp *interp,
517                           const char *switchName, Tcl_Obj *objPtr,
518                           char *record, int offset, int flags)
519{
520    TransferFunction **funcPtrPtr = (TransferFunction **)(record + offset);
521    TransferFunction *tf = NanoVis::getTransferFunction(Tcl_GetString(objPtr));
522    if (tf == NULL) {
523        Tcl_AppendResult(interp, "transfer function \"", Tcl_GetString(objPtr),
524                         "\" is not defined", (char*)NULL);
525        return TCL_ERROR;
526    }
527    *funcPtrPtr = tf;
528    return TCL_OK;
529}
530
531static int
532FlowConfigureOp(ClientData clientData, Tcl_Interp *interp, int objc,
533                Tcl_Obj *const *objv)
534{
535    Flow *flow = (Flow *)clientData;
536
537    if (flow->parseSwitches(interp, objc - 2, objv + 2) != TCL_OK) {
538        return TCL_ERROR;
539    }
540    if (flow->configure()) {
541        Flow::updatePending = true;
542    }
543    NanoVis::eventuallyRedraw();
544    return TCL_OK;
545}
546
547static int
548FlowParticlesAddOp(ClientData clientData, Tcl_Interp *interp, int objc,
549                   Tcl_Obj *const *objv)
550{
551    Flow *flow = (Flow *)clientData;
552    const char *particlesName = Tcl_GetString(objv[3]);
553    FlowParticles *particles = flow->createParticles(particlesName);
554    if (particles == NULL) {
555        Tcl_AppendResult(interp, "Flow particle injection plane \"",
556                         particlesName,
557                         "\" already exists or could not be created",
558                         (char*)NULL);
559        return TCL_ERROR;
560    }
561    if (particles->parseSwitches(interp, objc - 4, objv + 4) != TCL_OK) {
562        flow->deleteParticles(particlesName);
563        return TCL_ERROR;
564    }
565    particles->configure();
566    Flow::updatePending = true;
567    NanoVis::eventuallyRedraw();
568    Tcl_SetObjResult(interp, objv[3]);
569    return TCL_OK;
570}
571
572static int
573FlowParticlesConfigureOp(ClientData clientData, Tcl_Interp *interp, int objc,
574                         Tcl_Obj *const *objv)
575{
576    Flow *flow = (Flow *)clientData;
577    const char *particlesName = Tcl_GetString(objv[3]);
578    FlowParticles *particles = flow->getParticles(particlesName);
579    if (particles == NULL) {
580        Tcl_AppendResult(interp, "Flow particle injection plane \"",
581                         particlesName, "\" not found",
582                         (char*)NULL);
583        return TCL_ERROR;
584    }
585    if (particles->parseSwitches(interp, objc - 4, objv + 4) != TCL_OK) {
586        return TCL_ERROR;
587    }
588    if (particles->configure()) {
589        Flow::updatePending = true;
590    }
591    NanoVis::eventuallyRedraw();
592    return TCL_OK;
593}
594
595static int
596FlowParticlesDeleteOp(ClientData clientData, Tcl_Interp *interp, int objc,
597                      Tcl_Obj *const *objv)
598{
599    Flow *flow = (Flow *)clientData;
600    for (int i = 3; i < objc; i++) {
601        flow->deleteParticles(Tcl_GetString(objv[i]));
602    }
603    NanoVis::eventuallyRedraw();
604    return TCL_OK;
605}
606
607static int
608FlowParticlesNamesOp(ClientData clientData, Tcl_Interp *interp, int objc,
609                     Tcl_Obj *const *objv)
610{
611    Flow *flow = (Flow *)clientData;
612    Tcl_Obj *listObjPtr;
613    listObjPtr = Tcl_NewListObj(0, (Tcl_Obj **) NULL);
614    std::vector<std::string> names;
615    flow->getParticlesNames(names);
616    for (std::vector<std::string>::iterator itr = names.begin();
617         itr != names.end(); ++itr) {
618        Tcl_Obj *objPtr = Tcl_NewStringObj(itr->c_str(), -1);
619        Tcl_ListObjAppendElement(interp, listObjPtr, objPtr);
620    }
621    Tcl_SetObjResult(interp, listObjPtr);
622    return TCL_OK;
623}
624
625static CmdSpec flowParticlesOps[] = {
626    {"add",        1, FlowParticlesAddOp,        4, 0, "name ?switches?",},
627    {"configure",  1, FlowParticlesConfigureOp,  4, 0, "name ?switches?",},
628    {"delete",     1, FlowParticlesDeleteOp,     4, 0, "name ?name...?"},
629    {"names",      1, FlowParticlesNamesOp,      3, 3, ""},
630};
631
632static int nFlowParticlesOps = NumCmdSpecs(flowParticlesOps);
633
634/**
635 * \brief This procedure is invoked to process commands on behalf of the flow
636 * object.
637 *
638 * Side effects: See the user documentation.
639 *
640 * $flow particles oper $name
641 *
642 * \return A standard Tcl result.
643 */
644static int
645FlowParticlesOp(ClientData clientData, Tcl_Interp *interp, int objc,
646                Tcl_Obj *const *objv)
647{
648    Tcl_ObjCmdProc *proc;
649    proc = GetOpFromObj(interp, nFlowParticlesOps, flowParticlesOps,
650                        CMDSPEC_ARG2, objc, objv, 0);
651    if (proc == NULL) {
652        return TCL_ERROR;
653    }
654    Flow *flow = (Flow *)clientData;
655    Tcl_Preserve(flow);
656    int result;
657    result = (*proc) (clientData, interp, objc, objv);
658    Tcl_Release(flow);
659    return result;
660}
661
662static int
663FlowBoxAddOp(ClientData clientData, Tcl_Interp *interp, int objc,
664             Tcl_Obj *const *objv)
665{
666    Flow *flow = (Flow *)clientData;
667    const char *boxName = Tcl_GetString(objv[3]);
668    FlowBox *box = flow->createBox(boxName);
669    if (box == NULL) {
670        Tcl_AppendResult(interp, "Flow box \"", boxName,
671                         "\" already exists or could not be created",
672                         (char*)NULL);
673        return TCL_ERROR;
674    }
675    if (box->parseSwitches(interp, objc - 4, objv + 4) != TCL_OK) {
676        flow->deleteBox(boxName);
677        return TCL_ERROR;
678    }
679    NanoVis::eventuallyRedraw();
680    Tcl_SetObjResult(interp, objv[3]);
681    return TCL_OK;
682}
683
684static int
685FlowBoxConfigureOp(ClientData clientData, Tcl_Interp *interp, int objc,
686                   Tcl_Obj *const *objv)
687{
688    Flow *flow = (Flow *)clientData;
689    const char *boxName = Tcl_GetString(objv[3]);
690    FlowBox *box = flow->getBox(boxName);
691    if (box == NULL) {
692        Tcl_AppendResult(interp, "Flow box \"", boxName, "\" not found",
693                         (char*)NULL);
694        return TCL_ERROR;
695    }
696    if (box->parseSwitches(interp, objc - 4, objv + 4) != TCL_OK) {
697        return TCL_ERROR;
698    }
699    NanoVis::eventuallyRedraw();
700    return TCL_OK;
701}
702
703static int
704FlowBoxDeleteOp(ClientData clientData, Tcl_Interp *interp, int objc,
705                Tcl_Obj *const *objv)
706{
707    Flow *flow = (Flow *)clientData;
708    for (int i = 3; i < objc; i++) {
709        flow->deleteBox(Tcl_GetString(objv[i]));
710    }
711    NanoVis::eventuallyRedraw();
712    return TCL_OK;
713}
714
715static int
716FlowBoxNamesOp(ClientData clientData, Tcl_Interp *interp, int objc,
717               Tcl_Obj *const *objv)
718{
719    Flow *flow = (Flow *)clientData;
720    Tcl_Obj *listObjPtr = Tcl_NewListObj(0, (Tcl_Obj **) NULL);
721    std::vector<std::string> names;
722    flow->getBoxNames(names);
723    for (std::vector<std::string>::iterator itr = names.begin();
724         itr != names.end(); ++itr) {
725        Tcl_Obj *objPtr = Tcl_NewStringObj(itr->c_str(), -1);
726        Tcl_ListObjAppendElement(interp, listObjPtr, objPtr);
727    }
728    Tcl_SetObjResult(interp, listObjPtr);
729    return TCL_OK;
730}
731
732static CmdSpec flowBoxOps[] = {
733    {"add",        1, FlowBoxAddOp,        4, 0, "name ?switches?",},
734    {"configure",  1, FlowBoxConfigureOp,  4, 0, "name ?switches?",},
735    {"delete",     1, FlowBoxDeleteOp,     4, 0, "name ?name...?"},
736    {"names",      1, FlowBoxNamesOp,      3, 3, ""},
737};
738
739static int nFlowBoxOps = NumCmdSpecs(flowBoxOps);
740
741/**
742 * \brief This procedure is invoked to process commands on behalf of the flow
743 *         object.
744 *
745 * Side effects:  See the user documentation.
746 *
747 * \return A standard Tcl result.
748 */
749static int
750FlowBoxOp(ClientData clientData, Tcl_Interp *interp, int objc,
751          Tcl_Obj *const *objv)
752{
753    Tcl_ObjCmdProc *proc;
754    proc = GetOpFromObj(interp, nFlowBoxOps, flowBoxOps,
755                        CMDSPEC_ARG2, objc, objv, 0);
756    if (proc == NULL) {
757        return TCL_ERROR;
758    }
759    Flow *flow = (Flow *)clientData;
760    Tcl_Preserve(flow);
761    int result;
762    result = (*proc) (clientData, interp, objc, objv);
763    Tcl_Release(flow);
764    return result;
765}
766
767/*
768 * CLIENT COMMAND:
769 *   $flow legend <width> <height>
770 *
771 * Clients use this to generate a legend image for the specified
772 * transfer function.  The legend image is a color gradient from 0
773 * to one, drawn in the given transfer function.  The resulting image
774 * is returned in the size <width> x <height>.
775 */
776static int
777FlowLegendOp(ClientData clientData, Tcl_Interp *interp, int objc,
778             Tcl_Obj *const *objv)
779{
780    Flow *flow = (Flow *)clientData;
781   
782    const char *string = Tcl_GetString(objv[1]);
783    TransferFunction *tf;
784    tf = flow->getTransferFunction();
785    if (tf == NULL) {
786        Tcl_AppendResult(interp, "unknown transfer function \"", string, "\"",
787                         (char*)NULL);
788        return TCL_ERROR;
789    }
790    const char *label;
791    label = Tcl_GetString(objv[0]);
792    int w, h;
793    if ((Tcl_GetIntFromObj(interp, objv[2], &w) != TCL_OK) ||
794        (Tcl_GetIntFromObj(interp, objv[3], &h) != TCL_OK)) {
795        return TCL_ERROR;
796    }
797    if (Flow::updatePending) {
798        NanoVis::setFlowRanges();
799    }
800    NanoVis::renderLegend(tf, Flow::magMin, Flow::magMax, w, h, label);
801    return TCL_OK;
802}
803
804static CmdSpec flowInstOps[] = {
805    {"box",         1, FlowBoxOp,        2, 0, "oper ?args?"},
806    {"configure",   1, FlowConfigureOp,  2, 0, "?switches?"},
807    {"data",        1, FlowDataOp,       2, 0, "oper ?args?"},
808    {"legend",      1, FlowLegendOp,     4, 4, "w h"},
809    {"particles",   1, FlowParticlesOp,  2, 0, "oper ?args?"}
810};
811static int nFlowInstOps = NumCmdSpecs(flowInstOps);
812
813/**
814 * \brief This procedure is invoked to process commands on behalf of the flow
815 * object.
816 *
817 * Side effects: See the user documentation.
818 *
819 * \return A standard Tcl result.
820 */
821int
822nv::FlowInstObjCmd(ClientData clientData, Tcl_Interp *interp, int objc,
823                   Tcl_Obj *const *objv)
824{
825    Tcl_ObjCmdProc *proc;
826    proc = GetOpFromObj(interp, nFlowInstOps, flowInstOps,
827                        CMDSPEC_ARG1, objc, objv, 0);
828    if (proc == NULL) {
829        return TCL_ERROR;
830    }
831    assert(CheckGL(AT));
832    Flow *flow = (Flow *)clientData;
833    Tcl_Preserve(flow);
834    int result = (*proc) (clientData, interp, objc, objv);
835    Tcl_Release(flow);
836    return result;
837}
838
839/**
840 * \brief Deletes the command associated with the flow
841 *
842 * This is called only when the command associated with the flow is destroyed.
843 */
844void
845nv::FlowInstDeleteProc(ClientData clientData)
846{
847    Flow *flow = (Flow *)clientData;
848    NanoVis::deleteFlow(flow->name());
849}
850
851static int
852FlowAddOp(ClientData clientData, Tcl_Interp *interp, int objc,
853          Tcl_Obj *const *objv)
854{
855    const char *name = Tcl_GetString(objv[2]);
856    Tcl_CmdInfo cmdInfo;
857    if (Tcl_GetCommandInfo(interp, name, &cmdInfo)) {
858        Tcl_AppendResult(interp, "an another command \"", name,
859                         "\" already exists.", (char *)NULL);
860        return TCL_ERROR;
861    }
862    Flow *flow = NanoVis::createFlow(interp, name);
863    if (flow == NULL) {
864        Tcl_AppendResult(interp, "Flow \"", name, "\" already exists",
865                         (char*)NULL);
866        return TCL_ERROR;
867    }
868    if (flow->parseSwitches(interp, objc - 3, objv + 3) != TCL_OK) {
869        Tcl_DeleteCommand(interp, flow->name());
870        return TCL_ERROR;
871    }
872    Tcl_SetObjResult(interp, objv[2]);
873    NanoVis::eventuallyRedraw();
874    return TCL_OK;
875}
876
877static int
878FlowDeleteOp(ClientData clientData, Tcl_Interp *interp, int objc,
879             Tcl_Obj *const *objv)
880{
881    for (int i = 2; i < objc; i++) {
882        Flow *flow = NanoVis::getFlow(Tcl_GetString(objv[i]));
883        if (flow != NULL) {
884            Tcl_DeleteCommandFromToken(interp, flow->getCommandToken());
885        }
886    }
887    Flow::updatePending = true;
888    NanoVis::eventuallyRedraw();
889    return TCL_OK;
890}
891
892static int
893FlowExistsOp(ClientData clientData, Tcl_Interp *interp, int objc,
894             Tcl_Obj *const *objv)
895{
896    bool value = false;
897    Flow *flow = NanoVis::getFlow(Tcl_GetString(objv[2]));
898    if (flow != NULL) {
899        value = true;
900    }
901    Tcl_SetBooleanObj(Tcl_GetObjResult(interp), (int)value);
902    return TCL_OK;
903}
904
905/**
906 * \brief flow goto number
907 */
908static int
909FlowGotoOp(ClientData clientData, Tcl_Interp *interp, int objc,
910           Tcl_Obj *const *objv)
911{
912    int nSteps;
913    if (Tcl_GetIntFromObj(interp, objv[2], &nSteps) != TCL_OK) {
914        return TCL_ERROR;
915    }
916    if ((nSteps < 0) || (nSteps > SHRT_MAX)) {
917        Tcl_AppendResult(interp, "flow goto: bad # of steps \"",
918                         Tcl_GetString(objv[2]), "\"", (char *)NULL);
919        return TCL_ERROR;
920    }
921    NanoVis::resetFlows();
922    if (Flow::updatePending) {
923        NanoVis::setFlowRanges();
924    }
925    for (int i = 0; i < nSteps; i++) {
926        NanoVis::licRenderer->convolve();
927        NanoVis::advectFlows();
928    }
929    NanoVis::eventuallyRedraw();
930    return TCL_OK;
931}
932
933static int
934FlowNamesOp(ClientData clientData, Tcl_Interp *interp, int objc,
935            Tcl_Obj *const *objv)
936{
937    Tcl_Obj *listObjPtr;
938    listObjPtr = Tcl_NewListObj(0, (Tcl_Obj **) NULL);
939    for (NanoVis::FlowHashmap::iterator itr = NanoVis::flowTable.begin();
940         itr != NanoVis::flowTable.end(); ++itr) {
941        Flow *flow = itr->second;
942        Tcl_Obj *objPtr = Tcl_NewStringObj(flow->name(), -1);
943        Tcl_ListObjAppendElement(interp, listObjPtr, objPtr);
944    }
945    Tcl_SetObjResult(interp, listObjPtr);
946    return TCL_OK;
947}
948
949static int
950FlowNextOp(ClientData clientData, Tcl_Interp *interp, int objc,
951           Tcl_Obj *const *objv)
952{
953    assert(NanoVis::licRenderer != NULL);
954    if (Flow::updatePending) {
955        NanoVis::setFlowRanges();
956    }
957    NanoVis::licRenderer->convolve();
958    NanoVis::advectFlows();
959    NanoVis::eventuallyRedraw();
960    return TCL_OK;
961}
962
963static int
964FlowResetOp(ClientData clientData, Tcl_Interp *interp, int objc,
965            Tcl_Obj *const *objv)
966{
967    NanoVis::resetFlows();
968    return TCL_OK;
969}
970
971#ifdef HAVE_FFMPEG
972
973/**
974 * \brief Convert a Tcl_Obj representing the video format into its
975 * integer id.
976 *
977 * \param clientData Not used
978 * \param interp Interpreter to send results back to
979 * \param switchName Not used
980 * \param objPtr String representation
981 * \param record Structure record
982 * \param offset Not used
983 * \param flags Not used
984 *
985 * \return The return value is a standard Tcl result.
986 */
987static int
988VideoFormatSwitchProc(ClientData clientData, Tcl_Interp *interp,
989                      const char *switchName, Tcl_Obj *objPtr,
990                      char *record, int offset, int flags)
991{
992    Tcl_Obj **formatObjPtr = (Tcl_Obj **)(record + offset);
993    Tcl_Obj *fmtObjPtr;
994    const char *string;
995    char c;
996    int length;
997
998    string = Tcl_GetStringFromObj(objPtr, &length);
999    c = string[0];
1000    if ((c == 'm') && (length > 1) &&
1001        (strncmp(string, "mpeg", length) == 0)) {
1002        fmtObjPtr =  Tcl_NewStringObj("mpeg1video", 10);
1003    } else if ((c == 't') && (strncmp(string, "theora", length) == 0)) {
1004        fmtObjPtr =  Tcl_NewStringObj("theora", 6);
1005    } else if ((c == 'm') && (length > 1) &&
1006               (strncmp(string, "mov", length) == 0)) {
1007        fmtObjPtr =  Tcl_NewStringObj("mov", 3);
1008    } else {
1009        Tcl_AppendResult(interp, "bad video format \"", string,
1010                         "\": should be mpeg, theora, or mov", (char*)NULL);
1011        return TCL_ERROR;
1012    }
1013    if (*formatObjPtr != NULL) {
1014        Tcl_DecrRefCount(*formatObjPtr);
1015    }
1016    Tcl_IncrRefCount(fmtObjPtr);
1017    *formatObjPtr = fmtObjPtr;
1018    return TCL_OK;
1019}
1020
1021struct FlowVideoSwitches {
1022    float frameRate;         /**< Frame rate */
1023    int bitRate;             /**< Video bitrate */
1024    int width, height;       /**< Dimensions of video frame. */
1025    int numFrames;
1026    Tcl_Obj *formatObjPtr;
1027};
1028
1029static SwitchParseProc VideoFormatSwitchProc;
1030static SwitchCustom videoFormatSwitch = {
1031    VideoFormatSwitchProc, NULL, 0,
1032};
1033
1034static SwitchSpec flowVideoSwitches[] = {
1035    {SWITCH_INT, "-bitrate", "value",
1036     offsetof(FlowVideoSwitches, bitRate), 0},
1037    {SWITCH_CUSTOM, "-format", "string",
1038     offsetof(FlowVideoSwitches, formatObjPtr), 0, 0, &videoFormatSwitch},
1039    {SWITCH_FLOAT, "-framerate", "value",
1040     offsetof(FlowVideoSwitches, frameRate), 0},
1041    {SWITCH_INT, "-height", "integer",
1042     offsetof(FlowVideoSwitches, height), 0},
1043    {SWITCH_INT, "-numframes", "count",
1044     offsetof(FlowVideoSwitches, numFrames), 0},
1045    {SWITCH_INT, "-width", "integer",
1046     offsetof(FlowVideoSwitches, width), 0},
1047    {SWITCH_END}
1048};
1049
1050static bool
1051MakeImageFiles(char *tmpFileName,
1052               int width, int height, int numFrames,
1053               bool *cancelPtr)
1054{
1055    struct pollfd pollResults;
1056    pollResults.fd = nv::g_fdIn;
1057    pollResults.events = POLLIN;
1058#define PENDING_TIMEOUT          10  /* milliseconds. */
1059    int timeout = PENDING_TIMEOUT;
1060
1061    int oldWidth, oldHeight;
1062    oldWidth = NanoVis::winWidth;
1063    oldHeight = NanoVis::winHeight;
1064
1065    if (width != oldWidth ||
1066        height != oldHeight) {
1067        // Resize to the requested size.
1068        NanoVis::resizeOffscreenBuffer(width, height);
1069    }
1070    NanoVis::resetFlows();
1071    *cancelPtr = false;
1072    bool result = true;
1073    size_t length = strlen(tmpFileName);
1074    for (int i = 1; i <= numFrames; i++) {
1075        if (((i & 0xF) == 0) && (poll(&pollResults, 1, timeout) > 0)) {
1076            /* If there's another command on stdin, that means the client is
1077             * trying to cancel this operation. */
1078            *cancelPtr = true;
1079            break;
1080        }
1081
1082        NanoVis::licRenderer->convolve();
1083        NanoVis::advectFlows();
1084
1085        int fboOrig;
1086        glGetIntegerv(GL_FRAMEBUFFER_BINDING_EXT, &fboOrig);
1087
1088        NanoVis::bindOffscreenBuffer();
1089        NanoVis::render();
1090        NanoVis::readScreen();
1091
1092        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboOrig);
1093
1094        sprintf(tmpFileName + length, "/image%d.ppm", i);
1095        result = nv::writePPMFile(tmpFileName, NanoVis::screenBuffer,
1096                                  width, height);
1097        if (!result) {
1098            break;
1099        }
1100    }
1101    if (width != oldWidth ||
1102        height != oldHeight) {
1103        NanoVis::resizeOffscreenBuffer(oldWidth, oldHeight);
1104    }
1105    tmpFileName[length] = '\0';
1106    NanoVis::resetFlows();
1107    return result;
1108}
1109
1110static int
1111MakeMovie(Tcl_Interp *interp, char *tmpFileName, const char *token,
1112          FlowVideoSwitches *switchesPtr)
1113{
1114#ifndef FFMPEG
1115#  define FFMPEG "/usr/bin/ffmpeg"
1116#endif
1117    /* Generate the movie from the frame images by exec-ing ffmpeg */
1118    /* The ffmpeg command is
1119     *   ffmpeg -f image2 -i /var/tmp/xxxxx/image%d.ppm                 \
1120     *      -b bitrate -f framerate /var/tmp/xxxxx/movie.mpeg
1121     */
1122    char cmd[BUFSIZ];
1123    sprintf(cmd, "%s -f image2 -i %s/image%%d.ppm -f %s -b %d -r %f -",
1124            FFMPEG, tmpFileName, Tcl_GetString(switchesPtr->formatObjPtr),
1125            switchesPtr->bitRate, switchesPtr->frameRate);
1126    TRACE("Enter: %s", cmd);
1127    FILE *f;
1128    f = popen(cmd, "r");
1129    if (f == NULL) {
1130        Tcl_AppendResult(interp, "can't run ffmpeg: ",
1131                         Tcl_PosixError(interp), (char *)NULL);
1132        return TCL_ERROR;
1133    }
1134    char *data = NULL;
1135    size_t total = 0;
1136    for (;;) {
1137        ssize_t numRead;
1138        char buffer[BUFSIZ];
1139       
1140        numRead = fread(buffer, sizeof(char), BUFSIZ, f);
1141        total += numRead;
1142        if (numRead == 0) {             // EOF
1143            break;
1144        }
1145        if (numRead < 0) {              // Error
1146            ERROR("Can't read movie data: %s",
1147                  Tcl_PosixError(interp));
1148            Tcl_AppendResult(interp, "can't read movie data: ",
1149                Tcl_PosixError(interp), (char *)NULL);
1150            return TCL_ERROR;
1151        }
1152        data = (char *)realloc(data, total);
1153        if (data == NULL) {
1154            ERROR("Can't append movie data to buffer %d bytes",
1155                  numRead);
1156            Tcl_AppendResult(interp, "can't append movie data to buffer",
1157                             (char *)NULL);
1158            return TCL_ERROR;
1159        }
1160        memcpy(data + (total - numRead), buffer, numRead);
1161    }
1162    if (total == 0) {
1163        ERROR("ffmpeg returned 0 bytes");
1164    }
1165    // Send zero length to client so it can deal with error
1166    sprintf(cmd,"nv>image -type movie -token \"%s\" -bytes %lu\n",
1167            token, total);
1168    // Memory is freed by this call
1169    nv::sendDataToClient(cmd, data, total);
1170    return TCL_OK;
1171}
1172
1173static int
1174FlowVideoOp(ClientData clientData, Tcl_Interp *interp, int objc,
1175            Tcl_Obj *const *objv)
1176{
1177    FlowVideoSwitches switches;
1178    const char *token;
1179
1180    token = Tcl_GetString(objv[2]);
1181    switches.frameRate = 25.0f;                // Default frame rate 25 fps
1182    switches.bitRate = 6000000;                // Default video bit rate.
1183    switches.width = NanoVis::winWidth;
1184    switches.height = NanoVis::winHeight;
1185    switches.numFrames = 100;
1186    switches.formatObjPtr = Tcl_NewStringObj("mpeg1video", 10);
1187    Tcl_IncrRefCount(switches.formatObjPtr);
1188    if (ParseSwitches(interp, flowVideoSwitches,
1189                objc - 3, objv + 3, &switches, SWITCH_DEFAULTS) < 0) {
1190        return TCL_ERROR;
1191    }
1192    if ((switches.width < 0) || (switches.width > SHRT_MAX) ||
1193        (switches.height < 0) || (switches.height > SHRT_MAX)) {
1194        Tcl_AppendResult(interp, "bad dimensions for video", (char *)NULL);
1195        return TCL_ERROR;
1196    }
1197    if ((switches.frameRate < 0.0f) || (switches.frameRate > 30.0f)) {
1198        Tcl_AppendResult(interp, "bad frame rate.", (char *)NULL);
1199        return TCL_ERROR;
1200    }
1201    if (switches.bitRate < 0) {
1202        Tcl_AppendResult(interp, "bad bit rate.", (char *)NULL);
1203        return TCL_ERROR;
1204    }
1205    if (NanoVis::licRenderer == NULL) {
1206        Tcl_AppendResult(interp, "no lic renderer.", (char *)NULL);
1207        return TCL_ERROR;
1208    }
1209    TRACE("FLOW started");
1210
1211    char *tmpFileName;
1212    char nameTemplate[200];
1213    strcpy(nameTemplate,"/var/tmp/flowXXXXXX");
1214    tmpFileName = mkdtemp(nameTemplate);
1215    int result = TCL_OK;
1216    if (tmpFileName == NULL) {
1217        Tcl_AppendResult(interp, "can't create temporary directory \"",
1218                         nameTemplate, "\" for frame image files: ",
1219                         Tcl_PosixError(interp), (char *)NULL);
1220        return TCL_ERROR;
1221    }
1222    size_t length = strlen(tmpFileName);
1223    bool canceled = false;
1224    if (MakeImageFiles(tmpFileName,
1225                       switches.width, switches.height, switches.numFrames,
1226                       &canceled)) {
1227        result = TCL_OK;
1228    } else {
1229        result = TCL_ERROR;
1230    }
1231    if ((result == TCL_OK) && (!canceled)) {
1232        result = MakeMovie(interp, tmpFileName, token, &switches);
1233    }
1234    for (int i = 1; i <= switches.numFrames; i++) {
1235        sprintf(tmpFileName + length, "/image%d.ppm", i);
1236        unlink(tmpFileName);
1237    }
1238    tmpFileName[length] = '\0';
1239    rmdir(tmpFileName);
1240    FreeSwitches(flowVideoSwitches, &switches, 0);
1241    return result;
1242}
1243#else
1244/**
1245 *  Not implemented
1246 */
1247static int
1248FlowVideoOp(ClientData clientData, Tcl_Interp *interp, int objc,
1249            Tcl_Obj *const *objv)
1250{
1251    return TCL_OK;
1252}
1253#endif /* HAVE_FFMPEG */
1254
1255static CmdSpec flowCmdOps[] = {
1256    {"add",      1, FlowAddOp,     3, 0, "name ?option value...?",},
1257    {"delete",   1, FlowDeleteOp,  3, 0, "name ?name...?",},
1258    {"exists",   1, FlowExistsOp,  3, 3, "name",},
1259    {"goto",     1, FlowGotoOp,    3, 3, "nSteps",},
1260    {"names",    1, FlowNamesOp,   2, 2, "",},
1261    {"next",     2, FlowNextOp,    2, 2, "",},
1262    {"reset",    1, FlowResetOp,   2, 2, "",},
1263    {"video",    1, FlowVideoOp,   3, 0, "token ?switches...?",},
1264};
1265static int nFlowCmdOps = NumCmdSpecs(flowCmdOps);
1266
1267static int
1268FlowCmdProc(ClientData clientData, Tcl_Interp *interp, int objc,
1269            Tcl_Obj *const *objv)
1270{
1271    Tcl_ObjCmdProc *proc;
1272
1273    proc = GetOpFromObj(interp, nFlowCmdOps, flowCmdOps,
1274                        CMDSPEC_ARG1, objc, objv, 0);
1275    if (proc == NULL) {
1276        return TCL_ERROR;
1277    }
1278    return (*proc) (clientData, interp, objc, objv);
1279}
1280
1281/**
1282 *\brief This procedure is invoked to initialize the "flow" command.
1283 *
1284 * Side effects:
1285 *    Creates the new command and adds a new entry into a global Tcl
1286 *    associative array.
1287 */
1288void
1289nv::FlowCmdInitProc(Tcl_Interp *interp, ClientData clientData)
1290{
1291    Tcl_CreateObjCommand(interp, "flow", FlowCmdProc, clientData, NULL);
1292}
Note: See TracBrowser for help on using the repository browser.