source: trunk/gui/scripts/nanovisviewer.tcl @ 2390

Last change on this file since 2390 was 2390, checked in by gah, 13 years ago
File size: 70.0 KB
Line 
1
2# ----------------------------------------------------------------------
3#  COMPONENT: nanovisviewer - 3D volume rendering
4#
5#  This widget performs volume rendering on 3D scalar/vector datasets.
6#  It connects to the Nanovis server running on a rendering farm,
7#  transmits data, and displays the results.
8# ======================================================================
9#  AUTHOR:  Michael McLennan, Purdue University
10#  Copyright (c) 2004-2005  Purdue Research Foundation
11#
12#  See the file "license.terms" for information on usage and
13#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
14# ======================================================================
15package require Itk
16package require BLT
17package require Img
18                                       
19#
20# FIXME:
21#        Need to Add DX readers this client to examine the data before
22#        it's sent to the server.  This will eliminate 90% of the insanity in
23#        computing the limits of all the volumes.  I can rip out all the
24#        "receive data" "send transfer function" event crap.
25#
26#       This means we can compute the transfer function (relative values) and
27#        draw the legend min/max values without waiting for the information to
28#        come from the server.  This will also prevent the flashing that occurs
29#        when a new volume is drawn (using the default transfer function) and
30#        then when the correct transfer function has been sent and linked to
31#        the volume. 
32#
33option add *NanovisViewer.width 4i widgetDefault
34option add *NanovisViewer*cursor crosshair widgetDefault
35option add *NanovisViewer.height 4i widgetDefault
36option add *NanovisViewer.foreground black widgetDefault
37option add *NanovisViewer.controlBackground gray widgetDefault
38option add *NanovisViewer.controlDarkBackground #999999 widgetDefault
39option add *NanovisViewer.plotBackground black widgetDefault
40option add *NanovisViewer.plotForeground white widgetDefault
41option add *NanovisViewer.plotOutline gray widgetDefault
42option add *NanovisViewer.font \
43    -*-helvetica-medium-r-normal-*-12-* widgetDefault
44
45# must use this name -- plugs into Rappture::resources::load
46proc NanovisViewer_init_resources {} {
47    Rappture::resources::register \
48        nanovis_server Rappture::NanovisViewer::SetServerList
49}
50
51itcl::class Rappture::NanovisViewer {
52    inherit Rappture::VisViewer
53
54    itk_option define -plotforeground plotForeground Foreground ""
55    itk_option define -plotbackground plotBackground Background ""
56    itk_option define -plotoutline plotOutline PlotOutline ""
57
58    constructor { hostlist args } {
59        Rappture::VisViewer::constructor $hostlist
60    } {
61        # defined below
62    }
63    destructor {
64        # defined below
65    }
66    public proc SetServerList { namelist } {
67        Rappture::VisViewer::SetServerList "nanovis" $namelist
68    }
69    public method add {dataobj {settings ""}}
70    public method camera {option args}
71    public method delete {args}
72    public method disconnect {}
73    public method download {option args}
74    public method get {args}
75    public method isconnected {}
76    public method limits { tf }
77    public method overmarker { m x }
78    public method sendto { string }
79    public method parameters {title args} {
80        # do nothing
81    }
82    public method rmdupmarker { m x }
83    public method scale {args}
84    public method updatetransferfuncs {}
85
86    protected method Connect {}
87    protected method CurrentVolumes {{what -all}}
88    protected method Disconnect {}
89    protected method DoResize {}
90    protected method FixLegend {}
91    protected method FixSettings {what {value ""}}
92    protected method Pan {option x y}
93    protected method Rebuild {}
94    protected method ReceiveData { args }
95    protected method ReceiveImage { args }
96    protected method ReceiveLegend { tf vmin vmax size }
97    protected method Rotate {option x y}
98    protected method SendCmd {string}
99    protected method SendTransferFuncs {}
100    protected method Slice {option args}
101    protected method SlicerTip {axis}
102    protected method Zoom {option}
103
104    # The following methods are only used by this class.
105    private method AddIsoMarker { x y }
106    private method BuildCameraTab {}
107    private method BuildCutplanesTab {}
108    private method BuildViewTab {}
109    private method BuildVolumeTab {}
110    private method ComputeTransferFunc { tf }
111    private method EventuallyResize { w h }
112    private method EventuallyResizeLegend { }
113    private method NameTransferFunc { dataobj comp }
114    private method PanCamera {}
115    private method ParseLevelsOption { tf levels }
116    private method ParseMarkersOption { tf markers }
117    private method volume { tag name }
118    private method GetVolumeInfo { w }
119
120    private variable _outbuf       ;# buffer for outgoing commands
121
122    private variable _dlist ""     ;# list of data objects
123    private variable _allDataObjs
124    private variable _obj2ovride   ;# maps dataobj => style override
125    private variable _serverVols   ;# contains all the dataobj-component
126                                   ;# to volumes in the server
127    private variable _serverTfs    ;# contains all the transfer functions
128                                   ;# in the server.
129    private variable _recvdVols    ;# list of data objs to send to server
130    private variable _vol2style    ;# maps dataobj-component to transfunc
131    private variable _style2vols   ;# maps tf back to list of
132                                    # dataobj-components using the tf.
133
134    private variable _click        ;# info used for rotate operations
135    private variable _limits       ;# autoscale min/max for all axes
136    private variable _view         ;# view params for 3D view
137    private variable _isomarkers    ;# array of isosurface level values 0..1
138    private common   _settings
139    # Array of transfer functions in server.  If 0 the transfer has been
140    # defined but not loaded.  If 1 the transfer function has been named
141    # and loaded.
142    private variable _activeTfs
143    private variable _first ""     ;# This is the topmost volume.
144    private variable _buffering 0
145
146    # This
147    # indicates which isomarkers and transfer
148    # function to use when changing markers,
149    # opacity, or thickness.
150    common _downloadPopup          ;# download options from popup
151    private common _hardcopy
152    private variable _width 0
153    private variable _height 0
154    private variable _resizePending 0
155    private variable _resizeLegendPending 0
156}
157
158itk::usual NanovisViewer {
159    keep -background -foreground -cursor -font
160    keep -plotbackground -plotforeground
161}
162
163# ----------------------------------------------------------------------
164# CONSTRUCTOR
165# ----------------------------------------------------------------------
166itcl::body Rappture::NanovisViewer::constructor {hostlist args} {
167
168    # Draw legend event
169    $_dispatcher register !legend
170    $_dispatcher dispatch $this !legend "[itcl::code $this FixLegend]; list"
171
172    # Send transfer functions event
173    $_dispatcher register !send_transfunc
174    $_dispatcher dispatch $this !send_transfunc \
175        "[itcl::code $this SendTransferFuncs]; list"
176
177    # Rebuild event
178    $_dispatcher register !rebuild
179    $_dispatcher dispatch $this !rebuild "[itcl::code $this Rebuild]; list"
180
181    # Resize event
182    $_dispatcher register !resize
183    $_dispatcher dispatch $this !resize "[itcl::code $this DoResize]; list"
184
185    set _outbuf ""
186
187    #
188    # Populate parser with commands handle incoming requests
189    #
190    $_parser alias image [itcl::code $this ReceiveImage]
191    $_parser alias legend [itcl::code $this ReceiveLegend]
192    $_parser alias data [itcl::code $this ReceiveData]
193
194    # Initialize the view to some default parameters.
195    array set _view {
196        theta   45
197        phi     45
198        psi     0
199        zoom    1.0
200        pan-x        0
201        pan-y        0
202    }
203    set _limits(vmin) 0.0
204    set _limits(vmax) 1.0
205
206    array set _settings [subst {
207        $this-pan-x                     $_view(pan-x)
208        $this-pan-y                     $_view(pan-y)
209        $this-phi                       $_view(phi)
210        $this-psi                       $_view(psi)
211        $this-theta                     $_view(theta)
212        $this-volume                    1
213        $this-xcutplane                 0
214        $this-xcutposition              0
215        $this-ycutplane                 0
216        $this-ycutposition              0
217        $this-zcutplane                 0
218        $this-zcutposition              0
219        $this-zoom                      $_view(zoom)
220    }]
221
222    itk_component add 3dview {
223        label $itk_component(plotarea).vol -image $_image(plot) \
224            -highlightthickness 0 -borderwidth 0
225    } {
226        usual
227        ignore -highlightthickness -borderwidth  -background
228    }
229
230    set f [$itk_component(main) component controls]
231    itk_component add reset {
232        button $f.reset -borderwidth 1 -padx 1 -pady 1 \
233            -highlightthickness 0 \
234            -image [Rappture::icon reset-view] \
235            -command [itcl::code $this Zoom reset]
236    } {
237        usual
238        ignore -highlightthickness
239    }
240    pack $itk_component(reset) -side top -padx 2 -pady 2
241    Rappture::Tooltip::for $itk_component(reset) "Reset the view to the default zoom level"
242
243    itk_component add zoomin {
244        button $f.zin -borderwidth 1 -padx 1 -pady 1 \
245            -highlightthickness 0 \
246            -image [Rappture::icon zoom-in] \
247            -command [itcl::code $this Zoom in]
248    } {
249        usual
250        ignore -highlightthickness
251    }
252    pack $itk_component(zoomin) -side top -padx 2 -pady 2
253    Rappture::Tooltip::for $itk_component(zoomin) "Zoom in"
254
255    itk_component add zoomout {
256        button $f.zout -borderwidth 1 -padx 1 -pady 1 \
257            -highlightthickness 0 \
258            -image [Rappture::icon zoom-out] \
259            -command [itcl::code $this Zoom out]
260    } {
261        usual
262        ignore -highlightthickness
263    }
264    pack $itk_component(zoomout) -side top -padx 2 -pady 2
265    Rappture::Tooltip::for $itk_component(zoomout) "Zoom out"
266
267    itk_component add volume {
268        Rappture::PushButton $f.volume \
269            -onimage [Rappture::icon volume-on] \
270            -offimage [Rappture::icon volume-off] \
271            -command [itcl::code $this FixSettings volume] \
272            -variable [itcl::scope _settings($this-volume)]
273    }
274    $itk_component(volume) select
275    Rappture::Tooltip::for $itk_component(volume) \
276        "Toggle the volume cloud on/off"
277    pack $itk_component(volume) -padx 2 -pady 2
278
279    BuildViewTab
280    BuildVolumeTab
281    BuildCutplanesTab
282    BuildCameraTab
283
284    # Legend
285
286    set _image(legend) [image create photo]
287    itk_component add legend {
288        canvas $itk_component(plotarea).legend -height 50 -highlightthickness 0
289    } {
290        usual
291        ignore -highlightthickness
292        rename -background -plotbackground plotBackground Background
293    }
294    bind $itk_component(legend) <Configure> \
295        [itcl::code $this EventuallyResizeLegend]
296
297    # Hack around the Tk panewindow.  The problem is that the requested
298    # size of the 3d view isn't set until an image is retrieved from
299    # the server.  So the panewindow uses the tiny size.
300    set w 10000
301    pack forget $itk_component(3dview)
302    blt::table $itk_component(plotarea) \
303        0,0 $itk_component(3dview) -fill both -reqwidth $w \
304        1,0 $itk_component(legend) -fill x
305    blt::table configure $itk_component(plotarea) r1 -resize none
306
307    # Bindings for rotation via mouse
308    bind $itk_component(3dview) <ButtonPress-1> \
309        [itcl::code $this Rotate click %x %y]
310    bind $itk_component(3dview) <B1-Motion> \
311        [itcl::code $this Rotate drag %x %y]
312    bind $itk_component(3dview) <ButtonRelease-1> \
313        [itcl::code $this Rotate release %x %y]
314    bind $itk_component(3dview) <Configure> \
315        [itcl::code $this EventuallyResize %w %h]
316
317    # Bindings for panning via mouse
318    bind $itk_component(3dview) <ButtonPress-2> \
319        [itcl::code $this Pan click %x %y]
320    bind $itk_component(3dview) <B2-Motion> \
321        [itcl::code $this Pan drag %x %y]
322    bind $itk_component(3dview) <ButtonRelease-2> \
323        [itcl::code $this Pan release %x %y]
324
325    # Bindings for panning via keyboard
326    bind $itk_component(3dview) <KeyPress-Left> \
327        [itcl::code $this Pan set -10 0]
328    bind $itk_component(3dview) <KeyPress-Right> \
329        [itcl::code $this Pan set 10 0]
330    bind $itk_component(3dview) <KeyPress-Up> \
331        [itcl::code $this Pan set 0 -10]
332    bind $itk_component(3dview) <KeyPress-Down> \
333        [itcl::code $this Pan set 0 10]
334    bind $itk_component(3dview) <Shift-KeyPress-Left> \
335        [itcl::code $this Pan set -2 0]
336    bind $itk_component(3dview) <Shift-KeyPress-Right> \
337        [itcl::code $this Pan set 2 0]
338    bind $itk_component(3dview) <Shift-KeyPress-Up> \
339        [itcl::code $this Pan set 0 -2]
340    bind $itk_component(3dview) <Shift-KeyPress-Down> \
341        [itcl::code $this Pan set 0 2]
342
343    # Bindings for zoom via keyboard
344    bind $itk_component(3dview) <KeyPress-Prior> \
345        [itcl::code $this Zoom out]
346    bind $itk_component(3dview) <KeyPress-Next> \
347        [itcl::code $this Zoom in]
348
349    bind $itk_component(3dview) <Enter> "focus $itk_component(3dview)"
350
351    if {[string equal "x11" [tk windowingsystem]]} {
352        # Bindings for zoom via mouse
353        bind $itk_component(3dview) <4> [itcl::code $this Zoom out]
354        bind $itk_component(3dview) <5> [itcl::code $this Zoom in]
355    }
356
357    set _image(download) [image create photo]
358
359    eval itk_initialize $args
360
361    Connect
362}
363
364# ----------------------------------------------------------------------
365# DESTRUCTOR
366# ----------------------------------------------------------------------
367itcl::body Rappture::NanovisViewer::destructor {} {
368    $_dispatcher cancel !rebuild
369    $_dispatcher cancel !send_transfunc
370    $_dispatcher cancel !resize
371    image delete $_image(plot)
372    image delete $_image(legend)
373    image delete $_image(download)
374    array unset _settings $this-*
375}
376
377# ----------------------------------------------------------------------
378# USAGE: add <dataobj> ?<settings>?
379#
380# Clients use this to add a data object to the plot.  The optional
381# <settings> are used to configure the plot.  Allowed settings are
382# -color, -brightness, -width, -linestyle, and -raise.
383# ----------------------------------------------------------------------
384itcl::body Rappture::NanovisViewer::add {dataobj {settings ""}} {
385    array set params {
386        -color auto
387        -width 1
388        -linestyle solid
389        -brightness 0
390        -raise 0
391        -description ""
392        -param ""
393    }
394    foreach {opt val} $settings {
395        if {![info exists params($opt)]} {
396            error "bad setting \"$opt\": should be [join [lsort [array names params]] {, }]"
397        }
398        set params($opt) $val
399    }
400    if {$params(-color) == "auto" || $params(-color) == "autoreset"} {
401        # can't handle -autocolors yet
402        set params(-color) black
403    }
404    set pos [lsearch -exact $dataobj $_dlist]
405    if {$pos < 0} {
406        lappend _dlist $dataobj
407        set _allDataObjs($dataobj) 1
408        set _obj2ovride($dataobj-color) $params(-color)
409        set _obj2ovride($dataobj-width) $params(-width)
410        set _obj2ovride($dataobj-raise) $params(-raise)
411        $_dispatcher event -idle !rebuild
412    }
413}
414
415# ----------------------------------------------------------------------
416# USAGE: get ?-objects?
417# USAGE: get ?-image 3dview|legend?
418#
419# Clients use this to query the list of objects being plotted, in
420# order from bottom to top of this result.  The optional "-image"
421# flag can also request the internal images being shown.
422# ----------------------------------------------------------------------
423itcl::body Rappture::NanovisViewer::get {args} {
424    if {[llength $args] == 0} {
425        set args "-objects"
426    }
427
428    set op [lindex $args 0]
429    switch -- $op {
430      -objects {
431        # put the dataobj list in order according to -raise options
432        set dlist $_dlist
433        foreach obj $dlist {
434            if {[info exists _obj2ovride($obj-raise)] && $_obj2ovride($obj-raise)} {
435                set i [lsearch -exact $dlist $obj]
436                if {$i >= 0} {
437                    set dlist [lreplace $dlist $i $i]
438                    lappend dlist $obj
439                }
440            }
441        }
442        return $dlist
443      }
444      -image {
445        if {[llength $args] != 2} {
446            error "wrong # args: should be \"get -image 3dview|legend\""
447        }
448        switch -- [lindex $args end] {
449            3dview {
450                return $_image(plot)
451            }
452            legend {
453                return $_image(legend)
454            }
455            default {
456                error "bad image name \"[lindex $args end]\": should be 3dview or legend"
457            }
458        }
459      }
460      default {
461        error "bad option \"$op\": should be -objects or -image"
462      }
463    }
464}
465
466# ----------------------------------------------------------------------
467# USAGE: delete ?<dataobj1> <dataobj2> ...?
468#
469#       Clients use this to delete a dataobj from the plot.  If no dataobjs
470#       are specified, then all dataobjs are deleted.  No data objects are
471#       deleted.  They are only removed from the display list.
472#
473# ----------------------------------------------------------------------
474itcl::body Rappture::NanovisViewer::delete {args} {
475    if {[llength $args] == 0} {
476        set args $_dlist
477    }
478    # Delete all specified dataobjs
479    set changed 0
480    foreach dataobj $args {
481        set pos [lsearch -exact $_dlist $dataobj]
482        if { $pos >= 0 } {
483            set _dlist [lreplace $_dlist $pos $pos]
484            array unset _limits $dataobj*
485            array unset _obj2ovride $dataobj-*
486            array unset _vol2style $dataobj-*
487            set changed 1
488        }
489    }
490    # If anything changed, then rebuild the plot
491    if {$changed} {
492        $_dispatcher event -idle !rebuild
493    }
494}
495
496# ----------------------------------------------------------------------
497# USAGE: scale ?<data1> <data2> ...?
498#
499# Sets the default limits for the overall plot according to the
500# limits of the data for all of the given <data> objects.  This
501# accounts for all objects--even those not showing on the screen.
502# Because of this, the limits are appropriate for all objects as
503# the user scans through data in the ResultSet viewer.
504# ----------------------------------------------------------------------
505itcl::body Rappture::NanovisViewer::scale {args} {
506    foreach val {xmin xmax ymin ymax zmin zmax vmin vmax} {
507        set _limits($val) ""
508    }
509    foreach obj $args {
510        foreach axis {x y z v} {
511
512            foreach { min max } [$obj limits $axis] break
513
514            if {"" != $min && "" != $max} {
515                if {"" == $_limits(${axis}min)} {
516                    set _limits(${axis}min) $min
517                    set _limits(${axis}max) $max
518                } else {
519                    if {$min < $_limits(${axis}min)} {
520                        set _limits(${axis}min) $min
521                    }
522                    if {$max > $_limits(${axis}max)} {
523                        set _limits(${axis}max) $max
524                    }
525                }
526            }
527        }
528    }
529}
530
531# ----------------------------------------------------------------------
532# USAGE: download coming
533# USAGE: download controls <downloadCommand>
534# USAGE: download now
535#
536# Clients use this method to create a downloadable representation
537# of the plot.  Returns a list of the form {ext string}, where
538# "ext" is the file extension (indicating the type of data) and
539# "string" is the data itself.
540# ----------------------------------------------------------------------
541itcl::body Rappture::NanovisViewer::download {option args} {
542    switch $option {
543        coming {
544            if {[catch {
545                blt::winop snap $itk_component(plotarea) $_image(download)
546            }]} {
547                $_image(download) configure -width 1 -height 1
548                $_image(download) put #000000
549            }
550        }
551        controls {
552            # no controls for this download yet
553            return ""
554        }
555        now {
556            # Get the image data (as base64) and decode it back to binary.
557            # This is better than writing to temporary files.  When we switch
558            # to the BLT picture image it won't be necessary to decode the
559            # image data.
560            if { [image width $_image(plot)] > 0 &&
561                 [image height $_image(plot)] > 0 } {
562                set bytes [$_image(plot) data -format "jpeg -quality 100"]
563                set bytes [Rappture::encoding::decode -as b64 $bytes]
564                return [list .jpg $bytes]
565            }
566            return ""
567        }
568        default {
569            error "bad option \"$option\": should be coming, controls, now"
570        }
571    }
572}
573
574# ----------------------------------------------------------------------
575# USAGE: Connect ?<host:port>,<host:port>...?
576#
577# Clients use this method to establish a connection to a new
578# server, or to reestablish a connection to the previous server.
579# Any existing connection is automatically closed.
580# ----------------------------------------------------------------------
581itcl::body Rappture::NanovisViewer::Connect {} {
582    set _hosts [GetServerList "nanovis"]
583    if { "" == $_hosts } {
584        return 0
585    }
586    set result [VisViewer::Connect $_hosts]
587    if { $result } {
588        set w [winfo width $itk_component(3dview)]
589        set h [winfo height $itk_component(3dview)]
590        EventuallyResize $w $h
591    }
592    return $result
593}
594
595#
596# isconnected --
597#
598#       Indicates if we are currently connected to the visualization server.
599#
600itcl::body Rappture::NanovisViewer::isconnected {} {
601    return [VisViewer::IsConnected]
602}
603
604#
605# disconnect --
606#
607itcl::body Rappture::NanovisViewer::disconnect {} {
608    Disconnect
609}
610
611#
612# Disconnect --
613#
614#       Clients use this method to disconnect from the current rendering
615#       server.
616#
617itcl::body Rappture::NanovisViewer::Disconnect {} {
618    VisViewer::Disconnect
619
620    # disconnected -- no more data sitting on server
621    set _outbuf ""
622    array unset _serverVols
623}
624
625#
626# sendto --
627#
628itcl::body Rappture::NanovisViewer::sendto { bytes } {
629    SendBytes "$bytes\n"
630}
631
632#
633# SendCmd
634#
635#       Send commands off to the rendering server.  If we're currently
636#       sending data objects to the server, buffer the commands to be
637#       sent later.
638#
639itcl::body Rappture::NanovisViewer::SendCmd {string} {
640    if { $_buffering } {
641        append _outbuf $string "\n"
642    } else {
643        foreach line [split $string \n] {
644            SendEcho >>line $line
645        }
646        SendBytes "$string\n"
647    }
648}
649
650
651# ----------------------------------------------------------------------
652# USAGE: SendTransferFuncs
653# ----------------------------------------------------------------------
654itcl::body Rappture::NanovisViewer::SendTransferFuncs {} {
655    if { $_first == "" } {
656        puts stderr "first not set"
657        return
658    }
659    # Insure that the global opacity and thickness settings (in the slider
660    # settings widgets) are used for the active transfer-function.  Update
661    # the values in the _settings varible.
662    set opacity [expr { double($_settings($this-opacity)) * 0.01 }]
663    # Scale values between 0.00001 and 0.01000
664    set thickness [expr {double($_settings($this-thickness)) * 0.0001}]
665
666    foreach vol [CurrentVolumes] {
667        if { ![info exists _serverVols($vol)] || !$_serverVols($vol) } {
668            # The volume hasn't reached the server yet.  How did we get
669            # here?
670            continue
671        }
672        if { ![info exists _vol2style($vol)] } {
673            puts stderr "unknown volume $vol"
674            continue;                        # How does this happen?
675        }
676        set tf $_vol2style($vol)
677        set _settings($this-$tf-opacity) $opacity
678        set _settings($this-$tf-thickness) $thickness
679        ComputeTransferFunc $tf
680        # FIXME: Need to the send information as to what transfer functions
681        #         to update so that we only update the transfer function
682        #         as necessary.  Right now, all transfer functions are
683        #         updated. This makes moving the isomarker slider chunky.
684        if { ![info exists _activeTfs($tf)] || !$_activeTfs($tf) } {
685            set _activeTfs($tf) 1
686        }
687        SendCmd "volume shading transfunc $tf $vol"
688    }
689    FixLegend
690}
691
692# ----------------------------------------------------------------------
693# USAGE: ReceiveImage -bytes <size> -type <type> -token <token>
694#
695# Invoked automatically whenever the "image" command comes in from
696# the rendering server.  Indicates that binary image data with the
697# specified <size> will follow.
698# ----------------------------------------------------------------------
699itcl::body Rappture::NanovisViewer::ReceiveImage { args } {
700    array set info {
701        -token "???"
702        -bytes 0
703        -type image
704    }
705    array set info $args
706    set bytes [ReceiveBytes $info(-bytes)]
707    ReceiveEcho <<line "<read $info(-bytes) bytes"
708    if { $info(-type) == "image" } {
709        ReceiveEcho "for [image width $_image(plot)]x[image height $_image(plot)] image>"       
710        $_image(plot) configure -data $bytes
711    } elseif { $info(type) == "print" } {
712        set tag $this-print-$info(-token)
713        set _hardcopy($tag) $bytes
714    }
715}
716
717#
718# ReceiveLegend --
719#
720#       The procedure is the response from the render server to each "legend"
721#       command.  The server sends back a "legend" command invoked our
722#       the slave interpreter.  The purpose is to collect data of the image
723#       representing the legend in the canvas.  In addition, the isomarkers
724#       of the active transfer function are displayed.
725#
726#       I don't know is this is the right place to display the isomarkers.
727#       I don't know all the different paths used to draw the plot. There's
728#       "Rebuild", "add", etc.
729#
730itcl::body Rappture::NanovisViewer::ReceiveLegend { tf vmin vmax size } {
731    if { ![isconnected] } {
732        return
733    }
734    set bytes [ReceiveBytes $size]
735    $_image(legend) configure -data $bytes
736    ReceiveEcho <<line "<read $size bytes for [image width $_image(legend)]x[image height $_image(legend)] legend>"
737
738    set c $itk_component(legend)
739    set w [winfo width $c]
740    set h [winfo height $c]
741    set lx 10
742    set ly [expr {$h - 1}]
743    if {"" == [$c find withtag transfunc]} {
744        $c create image 10 10 -anchor nw \
745            -image $_image(legend) -tags transfunc
746        $c create text $lx $ly -anchor sw \
747            -fill $itk_option(-plotforeground) -tags "limits vmin"
748        $c create text [expr {$w-$lx}] $ly -anchor se \
749            -fill $itk_option(-plotforeground) -tags "limits vmax"
750        $c lower transfunc
751        $c bind transfunc <ButtonRelease-1> \
752            [itcl::code $this AddIsoMarker %x %y]
753    }
754    # Display the markers used by the active transfer function.
755
756    array set limits [limits $tf]
757    $c itemconfigure vmin -text [format %.2g $limits(min)]
758    $c coords vmin $lx $ly
759
760    $c itemconfigure vmax -text [format %.2g $limits(max)]
761    $c coords vmax [expr {$w-$lx}] $ly
762
763    if { [info exists _isomarkers($tf)] } {
764        foreach m $_isomarkers($tf) {
765            $m visible yes
766        }
767    }
768}
769
770#
771# ReceiveData --
772#
773#       The procedure is the response from the render server to each "data
774#       follows" command.  The server sends back a "data" command invoked our
775#       the slave interpreter.  The purpose is to collect the min/max of the
776#       volume sent to the render server.  Since the client (nanovisviewer)
777#       doesn't parse 3D data formats, we rely on the server (nanovis) to
778#       tell us what the limits are.  Once we've received the limits to all
779#       the data we've sent (tracked by _recvdVols) we can then determine
780#       what the transfer functions are for these volumes.
781#
782#
783#       Note: There is a considerable tradeoff in having the server report
784#             back what the data limits are.  It means that much of the code
785#             having to do with transfer-functions has to wait for the data
786#             to come back, since the isomarkers are calculated based upon
787#             the data limits.  The client code is much messier because of
788#             this.  The alternative is to parse any of the 3D formats on the
789#             client side.
790#
791itcl::body Rappture::NanovisViewer::ReceiveData { args } {
792    if { ![isconnected] } {
793        return
794    }
795    # Arguments from server are name value pairs. Stuff them in an array.
796    array set info $args
797
798    set tag $info(tag)
799    set parts [split $tag -]
800
801    #
802    # Volumes don't exist until we're told about them.
803    #
804    set dataobj [lindex $parts 0]
805    set _serverVols($tag) 1
806    if { $_settings($this-volume) && $dataobj == $_first } {
807        SendCmd "volume state 1 $tag"
808    }
809    set _limits($tag-min) $info(min);  # Minimum value of the volume.
810    set _limits($tag-max) $info(max);  # Maximum value of the volume.
811    set _limits(vmin)      $info(vmin); # Overall minimum value.
812    set _limits(vmax)      $info(vmax); # Overall maximum value.
813
814    unset _recvdVols($tag)
815    if { [array size _recvdVols] == 0 } {
816        # The active transfer function is by default the first component of
817        # the first data object.  This assumes that the data is always
818        # successfully transferred.
819        updatetransferfuncs
820    }
821}
822
823# ----------------------------------------------------------------------
824# USAGE: Rebuild
825#
826# Called automatically whenever something changes that affects the
827# data in the widget.  Clears any existing data and rebuilds the
828# widget to display new data.
829# ----------------------------------------------------------------------
830itcl::body Rappture::NanovisViewer::Rebuild {} {
831
832    # Turn on buffering of commands to the server.  We don't want to
833    # be preempted by a server disconnect/reconnect (which automatically
834    # generates a new call to Rebuild).   
835    set _buffering 1
836
837    # Hide all the isomarkers. Can't remove them. Have to remember the
838    # settings since the user may have created/deleted/moved markers.
839
840    foreach tf [array names _isomarkers] {
841        foreach m $_isomarkers($tf) {
842            $m visible no
843        }
844    }
845
846    set w [winfo width $itk_component(3dview)]
847    set h [winfo height $itk_component(3dview)]
848    EventuallyResize $w $h
849
850    foreach dataobj [get] {
851        foreach comp [$dataobj components] {
852            set vol $dataobj-$comp
853            if { ![info exists _serverVols($vol)] } {
854                # Send the data as one huge base64-encoded mess -- yuck!
855                set data [$dataobj values $comp]
856                set nbytes [string length $data]
857                append _outbuf "volume data follows $nbytes $vol\n"
858                append _outbuf $data
859                set _recvdVols($vol) 1
860                set _serverVols($vol) 0
861            }
862            NameTransferFunc $dataobj $comp
863        }
864    }
865    #
866    # Reset the camera and other view parameters
867    #
868
869    set _settings($this-theta) $_view(theta)
870    set _settings($this-phi)   $_view(phi)
871    set _settings($this-psi)   $_view(psi)
872    set _settings($this-pan-x) $_view(pan-x)
873    set _settings($this-pan-y) $_view(pan-y)
874    set _settings($this-zoom)  $_view(zoom)
875
876    set xyz [Euler2XYZ $_view(theta) $_view(phi) $_view(psi)]
877    SendCmd "camera angle $xyz"
878    PanCamera
879    SendCmd "camera zoom $_view(zoom)"
880    FixSettings light
881    FixSettings transp
882    FixSettings isosurface
883    FixSettings grid
884    FixSettings axes
885    FixSettings outline
886
887    # nothing to send -- activate the proper ivol
888    SendCmd "volume state 0"
889    set _first [lindex [get] 0]
890    if {"" != $_first} {
891        set axis [$_first hints updir]
892        if { "" != $axis } {
893            SendCmd "up $axis"
894        }
895        set location [$_first hints camera]
896        if { $location != "" } {
897            array set _view $location
898        }
899        set vols [array names _serverVols $_first-*]
900        if { $vols != "" } {
901            SendCmd "volume state 1 $vols"
902        }
903    }
904    # If the first volume already exists on the server, then make sure we
905    # display the proper transfer function in the legend.
906    set comp [lindex [$_first components] 0]
907    if { [info exists _serverVols($_first-$comp)] } {
908        updatetransferfuncs
909    }
910
911    # Sync the state of slicers
912    set vols [CurrentVolumes -cutplanes]
913    foreach axis {x y z} {
914        SendCmd "cutplane state $_settings($this-${axis}cutplane) $axis $vols"
915        set pos [expr {0.01*$_settings($this-${axis}cutposition)}]
916        set vol [lindex $vols 0]
917        SendCmd "cutplane position $pos $axis $vol"
918    }
919    SendCmd "volume data state $_settings($this-volume) $vols"
920    set _buffering 0;                        # Turn off buffering.
921    # Actually write the commands to the server socket.  If it fails, we don't
922    # care.  We're finished here.
923    blt::busy hold $itk_component(hull)
924    SendBytes $_outbuf;                       
925    blt::busy release $itk_component(hull)
926    set _outbuf "";                        # Clear the buffer.               
927}
928
929# ----------------------------------------------------------------------
930# USAGE: CurrentVolumes ?-cutplanes?
931#
932# Returns a list of volume server IDs for the current volume being
933# displayed.  This is normally a single ID, but it might be a list
934# of IDs if the current data object has multiple components.
935# ----------------------------------------------------------------------
936itcl::body Rappture::NanovisViewer::CurrentVolumes {{what -all}} {
937    set rlist ""
938    if { $_first == "" } {
939        return
940    }
941    foreach comp [$_first components] {
942        set vol $_first-$comp
943        if { [info exists _serverVols($vol)] && $_serverVols($vol) } {
944            array set style {
945                -cutplanes 1
946            }
947            array set style [lindex [$_first components -style $comp] 0]
948            if { $what != "-cutplanes" || $style(-cutplanes) } {
949                lappend rlist $vol
950            }
951        }
952    }
953    return $rlist
954}
955
956# ----------------------------------------------------------------------
957# USAGE: Zoom in
958# USAGE: Zoom out
959# USAGE: Zoom reset
960#
961# Called automatically when the user clicks on one of the zoom
962# controls for this widget.  Changes the zoom for the current view.
963# ----------------------------------------------------------------------
964itcl::body Rappture::NanovisViewer::Zoom {option} {
965    switch -- $option {
966        "in" {
967            set _view(zoom) [expr {$_view(zoom)*1.25}]
968            set _settings($this-zoom) $_view(zoom)
969        }
970        "out" {
971            set _view(zoom) [expr {$_view(zoom)*0.8}]
972            set _settings($this-zoom) $_view(zoom)
973        }
974        "reset" {
975            array set _view {
976                theta   45
977                phi     45
978                psi     0
979                zoom        1.0
980                pan-x        0
981                pan-y        0
982            }
983            if { $_first != "" } {
984                set location [$_first hints camera]
985                if { $location != "" } {
986                    array set _view $location
987                }
988            }
989            set xyz [Euler2XYZ $_view(theta) $_view(phi) $_view(psi)]
990            SendCmd "camera angle $xyz"
991            PanCamera
992            set _settings($this-theta) $_view(theta)
993            set _settings($this-phi)   $_view(phi)
994            set _settings($this-psi)   $_view(psi)
995            set _settings($this-pan-x) $_view(pan-x)
996            set _settings($this-pan-y) $_view(pan-y)
997            set _settings($this-zoom)  $_view(zoom)
998        }
999    }
1000    SendCmd "camera zoom $_view(zoom)"
1001}
1002
1003itcl::body Rappture::NanovisViewer::PanCamera {} {
1004    #set x [expr ($_view(pan-x)) / $_limits(xrange)]
1005    #set y [expr ($_view(pan-y)) / $_limits(yrange)]
1006    set x $_view(pan-x)
1007    set y $_view(pan-y)
1008    SendCmd "camera pan $x $y"
1009}
1010
1011
1012# ----------------------------------------------------------------------
1013# USAGE: Rotate click <x> <y>
1014# USAGE: Rotate drag <x> <y>
1015# USAGE: Rotate release <x> <y>
1016#
1017# Called automatically when the user clicks/drags/releases in the
1018# plot area.  Moves the plot according to the user's actions.
1019# ----------------------------------------------------------------------
1020itcl::body Rappture::NanovisViewer::Rotate {option x y} {
1021    switch -- $option {
1022        click {
1023            $itk_component(3dview) configure -cursor fleur
1024            set _click(x) $x
1025            set _click(y) $y
1026            set _click(theta) $_view(theta)
1027            set _click(phi) $_view(phi)
1028        }
1029        drag {
1030            if {[array size _click] == 0} {
1031                Rotate click $x $y
1032            } else {
1033                set w [winfo width $itk_component(3dview)]
1034                set h [winfo height $itk_component(3dview)]
1035                if {$w <= 0 || $h <= 0} {
1036                    return
1037                }
1038
1039                if {[catch {
1040                    # this fails sometimes for no apparent reason
1041                    set dx [expr {double($x-$_click(x))/$w}]
1042                    set dy [expr {double($y-$_click(y))/$h}]
1043                }]} {
1044                    return
1045                }
1046
1047                #
1048                # Rotate the camera in 3D
1049                #
1050                if {$_view(psi) > 90 || $_view(psi) < -90} {
1051                    # when psi is flipped around, theta moves backwards
1052                    set dy [expr {-$dy}]
1053                }
1054                set theta [expr {$_view(theta) - $dy*180}]
1055                while {$theta < 0} { set theta [expr {$theta+180}] }
1056                while {$theta > 180} { set theta [expr {$theta-180}] }
1057
1058                if {abs($theta) >= 30 && abs($theta) <= 160} {
1059                    set phi [expr {$_view(phi) - $dx*360}]
1060                    while {$phi < 0} { set phi [expr {$phi+360}] }
1061                    while {$phi > 360} { set phi [expr {$phi-360}] }
1062                    set psi $_view(psi)
1063                } else {
1064                    set phi $_view(phi)
1065                    set psi [expr {$_view(psi) - $dx*360}]
1066                    while {$psi < -180} { set psi [expr {$psi+360}] }
1067                    while {$psi > 180} { set psi [expr {$psi-360}] }
1068                }
1069
1070                set _view(theta)        $theta
1071                set _view(phi)          $phi
1072                set _view(psi)          $psi
1073                set xyz [Euler2XYZ $theta $phi $psi]
1074                set _settings($this-theta) $_view(theta)
1075                set _settings($this-phi)   $_view(phi)
1076                set _settings($this-psi)   $_view(psi)
1077                SendCmd "camera angle $xyz"
1078                set _click(x) $x
1079                set _click(y) $y
1080            }
1081        }
1082        release {
1083            Rotate drag $x $y
1084            $itk_component(3dview) configure -cursor ""
1085            catch {unset _click}
1086        }
1087        default {
1088            error "bad option \"$option\": should be click, drag, release"
1089        }
1090    }
1091}
1092
1093# ----------------------------------------------------------------------
1094# USAGE: $this Pan click x y
1095#        $this Pan drag x y
1096#        $this Pan release x y
1097#
1098# Called automatically when the user clicks on one of the zoom
1099# controls for this widget.  Changes the zoom for the current view.
1100# ----------------------------------------------------------------------
1101itcl::body Rappture::NanovisViewer::Pan {option x y} {
1102    # Experimental stuff
1103    set w [winfo width $itk_component(3dview)]
1104    set h [winfo height $itk_component(3dview)]
1105    if { $option == "set" } {
1106        set x [expr $x / double($w)]
1107        set y [expr $y / double($h)]
1108        set _view(pan-x) [expr $_view(pan-x) + $x]
1109        set _view(pan-y) [expr $_view(pan-y) + $y]
1110        PanCamera
1111        set _settings($this-pan-x) $_view(pan-x)
1112        set _settings($this-pan-y) $_view(pan-y)
1113        return
1114    }
1115    if { $option == "click" } {
1116        set _click(x) $x
1117        set _click(y) $y
1118        $itk_component(3dview) configure -cursor hand1
1119    }
1120    if { $option == "drag" || $option == "release" } {
1121        set dx [expr ($_click(x) - $x)/double($w)]
1122        set dy [expr ($_click(y) - $y)/double($h)]
1123        set _click(x) $x
1124        set _click(y) $y
1125        set _view(pan-x) [expr $_view(pan-x) - $dx]
1126        set _view(pan-y) [expr $_view(pan-y) - $dy]
1127        PanCamera
1128        set _settings($this-pan-x) $_view(pan-x)
1129        set _settings($this-pan-y) $_view(pan-y)
1130    }
1131    if { $option == "release" } {
1132        $itk_component(3dview) configure -cursor ""
1133    }
1134}
1135
1136# ----------------------------------------------------------------------
1137# USAGE: FixSettings <what> ?<value>?
1138#
1139# Used internally to update rendering settings whenever parameters
1140# change in the popup settings panel.  Sends the new settings off
1141# to the back end.
1142# ----------------------------------------------------------------------
1143itcl::body Rappture::NanovisViewer::FixSettings {what {value ""}} {
1144    switch -- $what {
1145        light {
1146            if {[isconnected]} {
1147                set val $_settings($this-light)
1148                set sval [expr {0.1*$val}]
1149                SendCmd "volume shading diffuse $sval"
1150                set sval [expr {sqrt($val+1.0)}]
1151                SendCmd "volume shading specular $sval"
1152            }
1153        }
1154        transp {
1155            if {[isconnected]} {
1156                set val $_settings($this-transp)
1157                set sval [expr {0.2*$val+1}]
1158                SendCmd "volume shading opacity $sval"
1159            }
1160        }
1161        opacity {
1162            if {[isconnected] && [array size _activeTfs] > 0 } {
1163                set val $_settings($this-opacity)
1164                set sval [expr { 0.01 * double($val) }]
1165                foreach tf [array names _activeTfs] {
1166                    set _settings($this-$tf-opacity) $sval
1167                    set _activeTfs($tf) 0
1168                }
1169                updatetransferfuncs
1170            }
1171        }
1172
1173        thickness {
1174            if {[isconnected] && [array names _activeTfs] > 0 } {
1175                set val $_settings($this-thickness)
1176                # Scale values between 0.00001 and 0.01000
1177                set sval [expr {0.0001*double($val)}]
1178                foreach tf [array names _activeTfs] {
1179                    set _settings($this-$tf-thickness) $sval
1180                    set _activeTfs($tf) 0
1181                }
1182                updatetransferfuncs
1183            }
1184        }
1185        "outline" {
1186            if {[isconnected]} {
1187                SendCmd "volume outline state $_settings($this-outline)"
1188            }
1189        }
1190        "isosurface" {
1191            if {[isconnected]} {
1192                SendCmd "volume shading isosurface $_settings($this-isosurface)"
1193            }
1194        }
1195        "grid" {
1196            if { [isconnected] } {
1197                SendCmd "grid visible $_settings($this-grid)"
1198            }
1199        }
1200        "axes" {
1201            if { [isconnected] } {
1202                SendCmd "axis visible $_settings($this-axes)"
1203            }
1204        }
1205        "legend" {
1206            if { $_settings($this-legend) } {
1207                blt::table $itk_component(plotarea) \
1208                    0,0 $itk_component(3dview) -fill both \
1209                    1,0 $itk_component(legend) -fill x
1210                blt::table configure $itk_component(plotarea) r1 -resize none
1211            } else {
1212                blt::table forget $itk_component(legend)
1213            }
1214        }
1215        "volume" {
1216            if { [isconnected] } {
1217                set vols [CurrentVolumes -cutplanes]
1218                SendCmd "volume data state $_settings($this-volume) $vols"
1219            }
1220        }
1221        "xcutplane" - "ycutplane" - "zcutplane" {
1222            set axis [string range $what 0 0]
1223            set bool $_settings($this-$what)
1224            if { [isconnected] } {
1225                set vols [CurrentVolumes -cutplanes]
1226                set vol [lindex $vols 0]
1227                SendCmd "cutplane state $bool $axis $vol"
1228            }
1229            if { $bool } {
1230                $itk_component(${axis}CutScale) configure -state normal \
1231                    -troughcolor white
1232            } else {
1233                $itk_component(${axis}CutScale) configure -state disabled \
1234                    -troughcolor grey82
1235            }
1236        }
1237        default {
1238            error "don't know how to fix $what"
1239        }
1240    }
1241}
1242
1243# ----------------------------------------------------------------------
1244# USAGE: FixLegend
1245#
1246# Used internally to update the legend area whenever it changes size
1247# or when the field changes.  Asks the server to send a new legend
1248# for the current field.
1249# ----------------------------------------------------------------------
1250itcl::body Rappture::NanovisViewer::FixLegend {} {
1251    set _resizeLegendPending 0
1252    set lineht [font metrics $itk_option(-font) -linespace]
1253    set w [expr {$_width-20}]
1254    set h [expr {[winfo height $itk_component(legend)]-20-$lineht}]
1255    if {$w > 0 && $h > 0 && [array names _activeTfs] > 0 && $_first != "" } {
1256        set vol [lindex [CurrentVolumes] 0]
1257        if { [info exists _vol2style($vol)] } {
1258            SendCmd "legend $_vol2style($vol) $w $h"
1259        }
1260    } else {
1261        # Can't do this as this will remove the items associated with the
1262        # isomarkers.
1263       
1264        #$itk_component(legend) delete all
1265    }
1266}
1267
1268#
1269# NameTransferFunc --
1270#
1271#       Creates a transfer function name based on the <style> settings in the
1272#       library run.xml file. This placeholder will be used later to create
1273#       and send the actual transfer function once the data info has been sent
1274#       to us by the render server. [We won't know the volume limits until the
1275#       server parses the 3D data and sends back the limits via ReceiveData.]
1276#
1277#       FIXME: The current way we generate transfer-function names completely
1278#              ignores the -markers option.  The problem is that we are forced
1279#              to compute the name from an increasing complex set of values:
1280#              color, levels, marker, opacity.  I think we're stuck doing it
1281#              now.
1282#
1283itcl::body Rappture::NanovisViewer::NameTransferFunc { dataobj comp } {
1284    array set style {
1285        -color rainbow
1286        -levels 6
1287        -opacity 1.0
1288    }
1289    array set style [lindex [$dataobj components -style $comp] 0]
1290    set tf "$style(-color):$style(-levels):$style(-opacity)"
1291    set _vol2style($dataobj-$comp) $tf
1292    lappend _style2vols($tf) $dataobj-$comp
1293    return $tf
1294}
1295
1296#
1297# ComputeTransferFunc --
1298#
1299#   Computes and sends the transfer function to the render server.  It's
1300#   assumed that the volume data limits are known and that the global
1301#   transfer-functions slider values have be setup.  Both parts are
1302#   needed to compute the relative value (location) of the marker, and
1303#   the alpha map of the transfer function.
1304#
1305itcl::body Rappture::NanovisViewer::ComputeTransferFunc { tf } {
1306    array set style {
1307        -color rainbow
1308        -levels 6
1309        -opacity 1.0
1310    }
1311    foreach {dataobj comp} [split $_style2vols($tf) -] break
1312    array set style [lindex [$dataobj components -style $comp] 0]
1313
1314
1315    # We have to parse the style attributes for a volume using this
1316    # transfer-function *once*.  This sets up the initial isomarkers for the
1317    # transfer function.  The user may add/delete markers, so we have to
1318    # maintain a list of markers for each transfer-function.  We use the one
1319    # of the volumes (the first in the list) using the transfer-function as a
1320    # reference.
1321    #
1322    # FIXME: The current way we generate transfer-function names completely
1323    #        ignores the -markers option.  The problem is that we are forced
1324    #        to compute the name from an increasing complex set of values:
1325    #        color, levels, marker, opacity.  I think the cow's out of the
1326    #        barn on this one.
1327
1328    if { ![info exists _isomarkers($tf)] } {
1329        # Have to defer creation of isomarkers until we have data limits
1330        if { [info exists style(-markers)] } {
1331            ParseMarkersOption $tf $style(-markers)
1332        } else {
1333            ParseLevelsOption $tf $style(-levels)
1334        }
1335    }
1336    if {$style(-color) == "rainbow"} {
1337        set style(-color) "white:yellow:green:cyan:blue:magenta"
1338    }
1339    set clist [split $style(-color) :]
1340    set cmap "0.0 [Color2RGB white] "
1341    for {set i 0} {$i < [llength $clist]} {incr i} {
1342        set x [expr {double($i+1)/([llength $clist]+1)}]
1343        set color [lindex $clist $i]
1344        append cmap "$x [Color2RGB $color] "
1345    }
1346    append cmap "1.0 [Color2RGB $color]"
1347
1348    set tag $this-$tf
1349    if { ![info exists _settings($tag-opacity)] } {
1350        set _settings($tag-opacity) $style(-opacity)
1351    }
1352    set max $_settings($tag-opacity)
1353
1354    set isovalues {}
1355    foreach m $_isomarkers($tf) {
1356        lappend isovalues [$m relval]
1357    }
1358    # Sort the isovalues
1359    set isovalues [lsort -real $isovalues]
1360
1361    if { ![info exists _settings($tag-thickness)]} {
1362        set _settings($tag-thickness) 0.05
1363    }
1364    set delta $_settings($tag-thickness)
1365
1366    set first [lindex $isovalues 0]
1367    set last [lindex $isovalues end]
1368    set wmap ""
1369    if { $first == "" || $first != 0.0 } {
1370        lappend wmap 0.0 0.0
1371    }
1372    foreach x $isovalues {
1373        set x1 [expr {$x-$delta-0.00001}]
1374        set x2 [expr {$x-$delta}]
1375        set x3 [expr {$x+$delta}]
1376        set x4 [expr {$x+$delta+0.00001}]
1377        if { $x1 < 0.0 } {
1378            set x1 0.0
1379        } elseif { $x1 > 1.0 } {
1380            set x1 1.0
1381        }
1382        if { $x2 < 0.0 } {
1383            set x2 0.0
1384        } elseif { $x2 > 1.0 } {
1385            set x2 1.0
1386        }
1387        if { $x3 < 0.0 } {
1388            set x3 0.0
1389        } elseif { $x3 > 1.0 } {
1390            set x3 1.0
1391        }
1392        if { $x4 < 0.0 } {
1393            set x4 0.0
1394        } elseif { $x4 > 1.0 } {
1395            set x4 1.0
1396        }
1397        # add spikes in the middle
1398        lappend wmap $x1 0.0
1399        lappend wmap $x2 $max
1400        lappend wmap $x3 $max
1401        lappend wmap $x4 0.0
1402    }
1403    if { $last == "" || $last != 1.0 } {
1404        lappend wmap 1.0 0.0
1405    }
1406    SendCmd "transfunc define $tf { $cmap } { $wmap }"
1407}
1408
1409# ----------------------------------------------------------------------
1410# CONFIGURATION OPTION: -plotbackground
1411# ----------------------------------------------------------------------
1412itcl::configbody Rappture::NanovisViewer::plotbackground {
1413    if { [isconnected] } {
1414        foreach {r g b} [Color2RGB $itk_option(-plotbackground)] break
1415        #fix this!
1416        #SendCmd "color background $r $g $b"
1417    }
1418}
1419
1420# ----------------------------------------------------------------------
1421# CONFIGURATION OPTION: -plotforeground
1422# ----------------------------------------------------------------------
1423itcl::configbody Rappture::NanovisViewer::plotforeground {
1424    if { [isconnected] } {
1425        foreach {r g b} [Color2RGB $itk_option(-plotforeground)] break
1426        #fix this!
1427        #SendCmd "color background $r $g $b"
1428    }
1429}
1430
1431# ----------------------------------------------------------------------
1432# CONFIGURATION OPTION: -plotoutline
1433# ----------------------------------------------------------------------
1434itcl::configbody Rappture::NanovisViewer::plotoutline {
1435    # Must check if we are connected because this routine is called from the
1436    # class body when the -plotoutline itk_option is defined.  At that point
1437    # the NanovisViewer class constructor hasn't been called, so we can't
1438    # start sending commands to visualization server.
1439    if { [isconnected] } {
1440        if {"" == $itk_option(-plotoutline)} {
1441            SendCmd "volume outline state off"
1442        } else {
1443            SendCmd "volume outline state on"
1444            SendCmd "volume outline color [Color2RGB $itk_option(-plotoutline)]"
1445        }
1446    }
1447}
1448
1449#
1450# The -levels option takes a single value that represents the number
1451# of evenly distributed markers based on the current data range. Each
1452# marker is a relative value from 0.0 to 1.0.
1453#
1454itcl::body Rappture::NanovisViewer::ParseLevelsOption { tf levels } {
1455    set c $itk_component(legend)
1456    regsub -all "," $levels " " levels
1457    if {[string is int $levels]} {
1458        for {set i 1} { $i <= $levels } {incr i} {
1459            set x [expr {double($i)/($levels+1)}]
1460            set m [Rappture::IsoMarker \#auto $c $this $tf]
1461            $m relval $x
1462            lappend _isomarkers($tf) $m
1463        }
1464    } else {
1465        foreach x $levels {
1466            set m [Rappture::IsoMarker \#auto $c $this $tf]
1467            $m relval $x
1468            lappend _isomarkers($tf) $m
1469        }
1470    }
1471}
1472
1473#
1474# The -markers option takes a list of zero or more values (the values
1475# may be separated either by spaces or commas) that have the following
1476# format:
1477#
1478#   N%  Percent of current total data range.  Converted to
1479#       to a relative value between 0.0 and 1.0.
1480#   N   Absolute value of marker.  If the marker is outside of
1481#       the current range, it will be displayed on the outer
1482#       edge of the legends, but it range it represents will
1483#       not be seen.
1484#
1485itcl::body Rappture::NanovisViewer::ParseMarkersOption { tf markers } {
1486    set c $itk_component(legend)
1487    regsub -all "," $markers " " markers
1488    foreach marker $markers {
1489        set n [scan $marker "%g%s" value suffix]
1490        if { $n == 2 && $suffix == "%" } {
1491            # ${n}% : Set relative value.
1492            set value [expr {$value * 0.01}]
1493            set m [Rappture::IsoMarker \#auto $c $this $tf]
1494            $m relval $value
1495            lappend _isomarkers($tf) $m
1496        } else {
1497            # ${n} : Set absolute value.
1498            set m [Rappture::IsoMarker \#auto $c $this $tf]
1499            $m absval $value
1500            lappend _isomarkers($tf) $m
1501        }
1502    }
1503}
1504
1505# ----------------------------------------------------------------------
1506# USAGE: UndateTransferFuncs
1507# ----------------------------------------------------------------------
1508itcl::body Rappture::NanovisViewer::updatetransferfuncs {} {
1509    $_dispatcher event -idle !send_transfunc
1510}
1511
1512itcl::body Rappture::NanovisViewer::AddIsoMarker { x y } {
1513    if { $_first == "" } {
1514        error "active transfer function isn't set"
1515    }
1516    set vol [lindex [CurrentVolumes] 0]
1517    set tf $_vol2style($vol)
1518    set c $itk_component(legend)
1519    set m [Rappture::IsoMarker \#auto $c $this $tf]
1520    set w [winfo width $c]
1521    $m relval [expr {double($x-10)/($w-20)}]
1522    lappend _isomarkers($tf) $m
1523    updatetransferfuncs
1524    return 1
1525}
1526
1527itcl::body Rappture::NanovisViewer::rmdupmarker { marker x } {
1528    set tf [$marker transferfunc]
1529    set bool 0
1530    if { [info exists _isomarkers($tf)] } {
1531        set list {}
1532        set marker [namespace tail $marker]
1533        foreach m $_isomarkers($tf) {
1534            set sx [$m screenpos]
1535            if { $m != $marker } {
1536                if { $x >= ($sx-3) && $x <= ($sx+3) } {
1537                    $marker relval [$m relval]
1538                    itcl::delete object $m
1539                    bell
1540                    set bool 1
1541                    continue
1542                }
1543            }
1544            lappend list $m
1545        }
1546        set _isomarkers($tf) $list
1547        updatetransferfuncs
1548    }
1549    return $bool
1550}
1551
1552itcl::body Rappture::NanovisViewer::overmarker { marker x } {
1553    set tf [$marker transferfunc]
1554    if { [info exists _isomarkers($tf)] } {
1555        set marker [namespace tail $marker]
1556        foreach m $_isomarkers($tf) {
1557            set sx [$m screenpos]
1558            if { $m != $marker } {
1559                set bool [expr { $x >= ($sx-3) && $x <= ($sx+3) }]
1560                $m activate $bool
1561            }
1562        }
1563    }
1564    return ""
1565}
1566
1567itcl::body Rappture::NanovisViewer::limits { tf } {
1568    set _limits(min) 0.0
1569    set _limits(max) 1.0
1570    if { ![info exists _style2vols($tf)] } {
1571        return [array get _limits]
1572    }
1573    set min ""; set max ""
1574    foreach vol $_style2vols($tf) {
1575        if { ![info exists _serverVols($vol)] } {
1576            continue
1577        }
1578        if { ![info exists _limits($vol-min)] } {
1579            continue
1580        }
1581        if { $min == "" || $min > $_limits($vol-min) } {
1582            set min $_limits($vol-min)
1583        }
1584        if { $max == "" || $max < $_limits($vol-max) } {
1585            set max $_limits($vol-max)
1586        }
1587    }
1588    if { $min != "" } {
1589        set _limits(min) $min
1590    }
1591    if { $max != "" } {
1592        set _limits(max) $max
1593    }
1594    return [array get _limits]
1595}
1596
1597
1598itcl::body Rappture::NanovisViewer::BuildViewTab {} {
1599    foreach { key value } {
1600        grid                0
1601        axes                1
1602        outline                1
1603        volume                1
1604        legend                1
1605        particles        1
1606        lic                1
1607    } {
1608        set _settings($this-$key) $value
1609    }
1610
1611    set fg [option get $itk_component(hull) font Font]
1612    #set bfg [option get $itk_component(hull) boldFont Font]
1613
1614    set inner [$itk_component(main) insert end \
1615        -title "View Settings" \
1616        -icon [Rappture::icon wrench]]
1617    $inner configure -borderwidth 4
1618
1619    set ::Rappture::NanovisViewer::_settings($this-isosurface) 0
1620    checkbutton $inner.isosurface \
1621        -text "Isosurface shading" \
1622        -variable [itcl::scope _settings($this-isosurface)] \
1623        -command [itcl::code $this FixSettings isosurface] \
1624        -font "Arial 9"
1625
1626    checkbutton $inner.axes \
1627        -text "Axes" \
1628        -variable [itcl::scope _settings($this-axes)] \
1629        -command [itcl::code $this FixSettings axes] \
1630        -font "Arial 9"
1631
1632    checkbutton $inner.grid \
1633        -text "Grid" \
1634        -variable [itcl::scope _settings($this-grid)] \
1635        -command [itcl::code $this FixSettings grid] \
1636        -font "Arial 9"
1637
1638    checkbutton $inner.outline \
1639        -text "Outline" \
1640        -variable [itcl::scope _settings($this-outline)] \
1641        -command [itcl::code $this FixSettings outline] \
1642        -font "Arial 9"
1643
1644    checkbutton $inner.legend \
1645        -text "Legend" \
1646        -variable [itcl::scope _settings($this-legend)] \
1647        -command [itcl::code $this FixSettings legend] \
1648        -font "Arial 9"
1649
1650    checkbutton $inner.volume \
1651        -text "Volume" \
1652        -variable [itcl::scope _settings($this-volume)] \
1653        -command [itcl::code $this FixSettings volume] \
1654        -font "Arial 9"
1655
1656    blt::table $inner \
1657        0,0 $inner.axes  -columnspan 2 -anchor w \
1658        1,0 $inner.grid  -columnspan 2 -anchor w \
1659        2,0 $inner.outline  -columnspan 2 -anchor w \
1660        3,0 $inner.volume  -columnspan 2 -anchor w \
1661        4,0 $inner.legend  -columnspan 2 -anchor w
1662
1663    if 0 {
1664    bind $inner <Map> [itcl::code $this GetVolumeInfo $inner]
1665    }
1666    blt::table configure $inner r* -resize none
1667    blt::table configure $inner r5 -resize expand
1668}
1669
1670itcl::body Rappture::NanovisViewer::BuildVolumeTab {} {
1671    foreach { key value } {
1672        light                40
1673        transp                50
1674        opacity                100
1675        thickness        350
1676    } {
1677        set _settings($this-$key) $value
1678    }
1679
1680    set inner [$itk_component(main) insert end \
1681        -title "Volume Settings" \
1682        -icon [Rappture::icon volume-on]]
1683    $inner configure -borderwidth 4
1684
1685    set fg [option get $itk_component(hull) font Font]
1686    #set bfg [option get $itk_component(hull) boldFont Font]
1687
1688    checkbutton $inner.vol -text "Show volume" -font $fg \
1689        -variable [itcl::scope _settings($this-volume)] \
1690        -command [itcl::code $this FixSettings volume]
1691    label $inner.shading -text "Shading:" -font $fg
1692
1693    label $inner.dim -text "Dim" -font $fg
1694    ::scale $inner.light -from 0 -to 100 -orient horizontal \
1695        -variable [itcl::scope _settings($this-light)] \
1696        -width 10 \
1697        -showvalue off -command [itcl::code $this FixSettings light]
1698    label $inner.bright -text "Bright" -font $fg
1699
1700    label $inner.fog -text "Fog" -font $fg
1701    ::scale $inner.transp -from 0 -to 100 -orient horizontal \
1702        -variable [itcl::scope _settings($this-transp)] \
1703        -width 10 \
1704        -showvalue off -command [itcl::code $this FixSettings transp]
1705    label $inner.plastic -text "Plastic" -font $fg
1706
1707    label $inner.clear -text "Clear" -font $fg
1708    ::scale $inner.opacity -from 0 -to 100 -orient horizontal \
1709        -variable [itcl::scope _settings($this-opacity)] \
1710        -width 10 \
1711        -showvalue off -command [itcl::code $this FixSettings opacity]
1712    label $inner.opaque -text "Opaque" -font $fg
1713
1714    label $inner.thin -text "Thin" -font $fg
1715    ::scale $inner.thickness -from 0 -to 1000 -orient horizontal \
1716        -variable [itcl::scope _settings($this-thickness)] \
1717        -width 10 \
1718        -showvalue off -command [itcl::code $this FixSettings thickness]
1719    label $inner.thick -text "Thick" -font $fg
1720
1721    blt::table $inner \
1722        0,0 $inner.vol -columnspan 4 -anchor w -pady 2 \
1723        1,0 $inner.shading -columnspan 4 -anchor w -pady {10 2} \
1724        2,0 $inner.dim -anchor e -pady 2 \
1725        2,1 $inner.light -columnspan 2 -pady 2 -fill x \
1726        2,3 $inner.bright -anchor w -pady 2 \
1727        3,0 $inner.fog -anchor e -pady 2 \
1728        3,1 $inner.transp -columnspan 2 -pady 2 -fill x \
1729        3,3 $inner.plastic -anchor w -pady 2 \
1730        4,0 $inner.clear -anchor e -pady 2 \
1731        4,1 $inner.opacity -columnspan 2 -pady 2 -fill x\
1732        4,3 $inner.opaque -anchor w -pady 2 \
1733        5,0 $inner.thin -anchor e -pady 2 \
1734        5,1 $inner.thickness -columnspan 2 -pady 2 -fill x\
1735        5,3 $inner.thick -anchor w -pady 2
1736
1737    blt::table configure $inner c0 c1 c3 r* -resize none
1738    blt::table configure $inner r6 -resize expand
1739}
1740
1741itcl::body Rappture::NanovisViewer::BuildCutplanesTab {} {
1742    set inner [$itk_component(main) insert end \
1743        -title "Cutplane Settings" \
1744        -icon [Rappture::icon cutbutton]]
1745    $inner configure -borderwidth 4
1746
1747    # X-value slicer...
1748    itk_component add xCutButton {
1749        Rappture::PushButton $inner.xbutton \
1750            -onimage [Rappture::icon x-cutplane] \
1751            -offimage [Rappture::icon x-cutplane] \
1752            -command [itcl::code $this FixSettings xcutplane] \
1753            -variable [itcl::scope _settings($this-xcutplane)]
1754    }
1755    Rappture::Tooltip::for $itk_component(xCutButton) \
1756        "Toggle the X cut plane on/off"
1757
1758    itk_component add xCutScale {
1759        ::scale $inner.xval -from 100 -to 0 \
1760            -width 10 -orient vertical -showvalue off \
1761            -borderwidth 1 -highlightthickness 0 \
1762            -command [itcl::code $this Slice move x] \
1763            -variable [itcl::scope _settings($this-xcutposition)]
1764    } {
1765        usual
1766        ignore -borderwidth -highlightthickness
1767    }
1768    # Set the default cutplane value before disabling the scale.
1769    $itk_component(xCutScale) set 50
1770    $itk_component(xCutScale) configure -state disabled
1771    Rappture::Tooltip::for $itk_component(xCutScale) \
1772        "@[itcl::code $this SlicerTip x]"
1773
1774    # Y-value slicer...
1775    itk_component add yCutButton {
1776        Rappture::PushButton $inner.ybutton \
1777            -onimage [Rappture::icon y-cutplane] \
1778            -offimage [Rappture::icon y-cutplane] \
1779            -command [itcl::code $this FixSettings ycutplane] \
1780            -variable [itcl::scope _settings($this-ycutplane)]
1781    }
1782    Rappture::Tooltip::for $itk_component(yCutButton) \
1783        "Toggle the Y cut plane on/off"
1784
1785    itk_component add yCutScale {
1786        ::scale $inner.yval -from 100 -to 0 \
1787            -width 10 -orient vertical -showvalue off \
1788            -borderwidth 1 -highlightthickness 0 \
1789            -command [itcl::code $this Slice move y] \
1790            -variable [itcl::scope _settings($this-ycutposition)]
1791    } {
1792        usual
1793        ignore -borderwidth -highlightthickness
1794    }
1795    Rappture::Tooltip::for $itk_component(yCutScale) \
1796        "@[itcl::code $this SlicerTip y]"
1797    # Set the default cutplane value before disabling the scale.
1798    $itk_component(yCutScale) set 50
1799    $itk_component(yCutScale) configure -state disabled
1800
1801    # Z-value slicer...
1802    itk_component add zCutButton {
1803        Rappture::PushButton $inner.zbutton \
1804            -onimage [Rappture::icon z-cutplane] \
1805            -offimage [Rappture::icon z-cutplane] \
1806            -command [itcl::code $this FixSettings zcutplane] \
1807            -variable [itcl::scope _settings($this-zcutplane)]
1808    }
1809    Rappture::Tooltip::for $itk_component(zCutButton) \
1810        "Toggle the Z cut plane on/off"
1811
1812    itk_component add zCutScale {
1813        ::scale $inner.zval -from 100 -to 0 \
1814            -width 10 -orient vertical -showvalue off \
1815            -borderwidth 1 -highlightthickness 0 \
1816            -command [itcl::code $this Slice move z] \
1817            -variable [itcl::scope _settings($this-zcutposition)]
1818    } {
1819        usual
1820        ignore -borderwidth -highlightthickness
1821    }
1822    $itk_component(zCutScale) set 50
1823    $itk_component(zCutScale) configure -state disabled
1824    #$itk_component(zCutScale) configure -state disabled
1825    Rappture::Tooltip::for $itk_component(zCutScale) \
1826        "@[itcl::code $this SlicerTip z]"
1827
1828    blt::table $inner \
1829        1,1 $itk_component(xCutButton) \
1830        1,2 $itk_component(yCutButton) \
1831        1,3 $itk_component(zCutButton) \
1832        0,1 $itk_component(xCutScale) \
1833        0,2 $itk_component(yCutScale) \
1834        0,3 $itk_component(zCutScale) \
1835
1836    blt::table configure $inner r0 r1 c* -resize none
1837    blt::table configure $inner r2 c4 -resize expand
1838    blt::table configure $inner c0 -width 2
1839    blt::table configure $inner c1 c2 c3 -padx 2
1840}
1841
1842itcl::body Rappture::NanovisViewer::BuildCameraTab {} {
1843    set inner [$itk_component(main) insert end \
1844        -title "Camera Settings" \
1845        -icon [Rappture::icon camera]]
1846    $inner configure -borderwidth 4
1847
1848    set labels { phi theta psi pan-x pan-y zoom }
1849    set row 0
1850    foreach tag $labels {
1851        label $inner.${tag}label -text $tag -font "Arial 9"
1852        entry $inner.${tag} -font "Arial 9"  -bg white \
1853            -textvariable [itcl::scope _settings($this-$tag)]
1854        bind $inner.${tag} <KeyPress-Return> \
1855            [itcl::code $this camera set ${tag}]
1856        blt::table $inner \
1857            $row,0 $inner.${tag}label -anchor e -pady 2 \
1858            $row,1 $inner.${tag} -anchor w -pady 2
1859        blt::table configure $inner r$row -resize none
1860        incr row
1861    }
1862    blt::table configure $inner c0 c1 -resize none
1863    blt::table configure $inner c2 -resize expand
1864    blt::table configure $inner r$row -resize expand
1865}
1866
1867
1868# ----------------------------------------------------------------------
1869# USAGE: Slice move x|y|z <newval>
1870#
1871# Called automatically when the user drags the slider to move the
1872# cut plane that slices 3D data.  Gets the current value from the
1873# slider and moves the cut plane to the appropriate point in the
1874# data set.
1875# ----------------------------------------------------------------------
1876itcl::body Rappture::NanovisViewer::Slice {option args} {
1877    switch -- $option {
1878        move {
1879            if {[llength $args] != 2} {
1880                error "wrong # args: should be \"Slice move x|y|z newval\""
1881            }
1882            set axis [lindex $args 0]
1883            set newval [lindex $args 1]
1884
1885            set newpos [expr {0.01*$newval}]
1886            set vols [CurrentVolumes -cutplanes]
1887            set vol [lindex $vols 0]
1888            SendCmd "cutplane position $newpos $axis $vol"
1889        }
1890        default {
1891            error "bad option \"$option\": should be axis, move, or volume"
1892        }
1893    }
1894}
1895
1896# ----------------------------------------------------------------------
1897# USAGE: SlicerTip <axis>
1898#
1899# Used internally to generate a tooltip for the x/y/z slicer controls.
1900# Returns a message that includes the current slicer value.
1901# ----------------------------------------------------------------------
1902itcl::body Rappture::NanovisViewer::SlicerTip {axis} {
1903    set val [$itk_component(${axis}CutScale) get]
1904#    set val [expr {0.01*($val-50)
1905#        *($_limits(${axis}max)-$_limits(${axis}min))
1906#          + 0.5*($_limits(${axis}max)+$_limits(${axis}min))}]
1907    return "Move the [string toupper $axis] cut plane.\nCurrently:  $axis = $val%"
1908}
1909
1910
1911itcl::body Rappture::NanovisViewer::DoResize {} {
1912    SendCmd "screen $_width $_height"
1913    set _resizePending 0
1914}
1915
1916itcl::body Rappture::NanovisViewer::EventuallyResize { w h } {
1917    set _width $w
1918    set _height $h
1919    if { !$_resizePending } {
1920        $_dispatcher event -idle !resize
1921        set _resizePending 1
1922    }
1923}
1924
1925itcl::body Rappture::NanovisViewer::EventuallyResizeLegend {} {
1926    if { !$_resizeLegendPending } {
1927        $_dispatcher event -idle !legend
1928        set _resizeLegendPending 1
1929    }
1930}
1931
1932
1933#  camera --
1934#
1935itcl::body Rappture::NanovisViewer::camera {option args} {
1936    switch -- $option {
1937        "show" {
1938            puts [array get _view]
1939        }
1940        "set" {
1941            set who [lindex $args 0]
1942            set x $_settings($this-$who)
1943            set code [catch { string is double $x } result]
1944            if { $code != 0 || !$result } {
1945                set _settings($this-$who) $_view($who)
1946                return
1947            }
1948            switch -- $who {
1949                "pan-x" - "pan-y" {
1950                    set _view($who) $_settings($this-$who)
1951                    PanCamera
1952                }
1953                "phi" - "theta" - "psi" {
1954                    set _view($who) $_settings($this-$who)
1955                    set xyz [Euler2XYZ $_view(theta) $_view(phi) $_view(psi)]
1956                    SendCmd "camera angle $xyz"
1957                }
1958                "zoom" {
1959                    set _view($who) $_settings($this-$who)
1960                    SendCmd "camera zoom $_view(zoom)"
1961                }
1962            }
1963        }
1964    }
1965}
1966
1967itcl::body Rappture::NanovisViewer::GetVolumeInfo { w } {
1968    set flowobj ""
1969    foreach key [array names _obj2flow] {
1970        set flowobj $_obj2flow($key)
1971        break
1972    }
1973    if { $flowobj == "" } {
1974        return
1975    }
1976    if { [winfo exists $w.frame] } {
1977        destroy $w.frame
1978    }
1979    set inner [frame $w.frame]
1980    blt::table $w \
1981        5,0 $inner -fill both -columnspan 2 -anchor nw
1982    array set hints [$dataobj hints]
1983
1984    label $inner.volumes -text "Volumes" -font "Arial 9 bold"
1985    blt::table $inner \
1986        1,0 $inner.volumes  -anchor w \
1987    blt::table configure $inner c0 c1 -resize none
1988    blt::table configure $inner c2 -resize expand
1989
1990    set row 3
1991    set volumes [get]
1992    if { [llength $volumes] > 0 } {
1993        blt::table $inner $row,0 $inner.volumes  -anchor w
1994        incr row
1995    }
1996    foreach vol $volumes {
1997        array unset info
1998        array set info $vol
1999        set name $info(name)
2000        if { ![info exists _settings($this-volume-$name)] } {
2001            set _settings($this-volume-$name) $info(hide)
2002        }
2003        checkbutton $inner.vol$row -text $info(label) \
2004            -variable [itcl::scope _settings($this-volume-$name)] \
2005            -onvalue 0 -offvalue 1 \
2006            -command [itcl::code $this volume $key $name] \
2007            -font "Arial 9"
2008        Rappture::Tooltip::for $inner.vol$row $info(description)
2009        blt::table $inner $row,0 $inner.vol$row -anchor w
2010        if { !$_settings($this-volume-$name) } {
2011            $inner.vol$row select
2012        }
2013        incr row
2014    }
2015    blt::table configure $inner r* -resize none
2016    blt::table configure $inner r$row -resize expand
2017    blt::table configure $inner c3 -resize expand
2018    event generate [winfo parent [winfo parent $w]] <Configure>
2019}
2020
2021itcl::body Rappture::NanovisViewer::volume { tag name } {
2022    set bool $_settings($this-volume-$name)
2023    SendCmd "volume statue $bool $name"
2024}
2025
Note: See TracBrowser for help on using the repository browser.