source: trunk/gui/scripts/moleculeViewer.tcl @ 738

Last change on this file since 738 was 730, checked in by mmc, 17 years ago

Fix for support ticket #1434 "reset zoom button does not work on CNTBands".
Fixed the zoom reset so that it resets the view angle back to the default
30 deg.

Also, fixed all numeric gauges so that they won't accept "nan" or "inf"
as numbers.

File size: 23.0 KB
Line 
1# ----------------------------------------------------------------------
2#  COMPONENT: MoleculeViewer - view a molecule in 3D
3#
4#  This widget brings up a 3D representation of a molecule, which you
5#  can rotate.  It extracts atoms and bonds from the Rappture XML
6#  representation for a <molecule>.
7# ======================================================================
8#  AUTHOR:  Michael McLennan, Purdue University
9#  Copyright (c) 2004-2005  Purdue Research Foundation
10#
11#  See the file "license.terms" for information on usage and
12#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
13# ======================================================================
14package require Itk
15package require vtk
16package require vtkinteraction
17package require BLT
18package require Img
19
20option add *MoleculeViewer.width 3i widgetDefault
21option add *MoleculeViewer.height 3i widgetDefault
22option add *MoleculeViewer.backdrop black widgetDefault
23
24itcl::class Rappture::MoleculeViewer {
25    inherit itk::Widget
26
27    itk_option define -backdrop backdrop Backdrop "black"
28    itk_option define -device device Device ""
29
30    constructor {tool args} { # defined below }
31    destructor { # defined below }
32
33    public method add {dataobj {settings ""}}
34    public method get {}
35    public method delete {args}
36
37    public method emblems {option}
38    public method download {option args}
39
40    protected method _clear {}
41    protected method _redraw {}
42    protected method _zoom {option}
43    protected method _move {option x y}
44    protected method _3dView {theta phi psi}
45    protected method _color2rgb {color}
46
47    private variable _dispatcher "" ;# dispatcher for !events
48
49    private variable _tool ""    ;# tool containing this viewer
50    private variable _dlist ""   ;# list of dataobj objects
51    private variable _dobj2raise ;# maps dataobj => raise flag
52    private variable _actors ""  ;# list of actors in renderer
53    private variable _label2atom ;# maps 2D text actor => underlying atom
54    private variable _view       ;# view params for 3D view
55    private variable _limits     ;# limits of x/y/z axes
56    private variable _click      ;# info used for _move operations
57    private variable _download "";# snapshot for download
58}
59                                                                               
60itk::usual MoleculeViewer {
61}
62
63# ----------------------------------------------------------------------
64# CONSTRUCTOR
65# ----------------------------------------------------------------------
66itcl::body Rappture::MoleculeViewer::constructor {tool args} {
67    set _tool $tool
68
69    Rappture::dispatcher _dispatcher
70    $_dispatcher register !redraw
71    $_dispatcher dispatch $this !redraw "[itcl::code $this _redraw]; list"
72    $_dispatcher register !render
73    $_dispatcher dispatch $this !render "$this-renWin Render; list"
74    $_dispatcher register !fixsize
75    $_dispatcher dispatch $this !fixsize \
76        "[itcl::code $this emblems fixPosition]; list"
77
78    itk_option add hull.width hull.height
79    pack propagate $itk_component(hull) no
80
81    vtkRenderWindow $this-renWin
82    vtkRenderer $this-ren
83    $this-renWin AddRenderer $this-ren
84
85    vtkRenderWindowInteractor $this-int
86    $this-int SetRenderWindow $this-renWin
87
88    vtkSphereSource $this-sphere
89    $this-sphere SetRadius 1.0
90    $this-sphere SetThetaResolution 18
91    $this-sphere SetPhiResolution 18
92
93    vtkPolyDataMapper $this-map
94    $this-map SetInput [$this-sphere GetOutput]
95
96    vtkCoordinate $this-xyzconv
97    $this-xyzconv SetCoordinateSystemToWorld
98
99    set _view(theta) 0
100    set _view(phi) 0
101    set _view(psi) 0
102
103    itk_component add controls {
104        frame $itk_interior.cntls
105    } {
106        usual
107        rename -background -controlbackground controlBackground Background
108    }
109    pack $itk_component(controls) -side right -fill y
110
111    itk_component add reset {
112        button $itk_component(controls).reset \
113            -borderwidth 1 -padx 1 -pady 1 \
114            -bitmap [Rappture::icon reset] \
115            -command [itcl::code $this _zoom reset]
116    } {
117        usual
118        ignore -borderwidth
119        rename -highlightbackground -controlbackground controlBackground Background
120    }
121    pack $itk_component(reset) -padx 4 -pady 4
122    Rappture::Tooltip::for $itk_component(reset) "Reset the view to the default zoom level"
123
124    itk_component add zoomin {
125        button $itk_component(controls).zin \
126            -borderwidth 1 -padx 1 -pady 1 \
127            -bitmap [Rappture::icon zoomin] \
128            -command [itcl::code $this _zoom in]
129    } {
130        usual
131        ignore -borderwidth
132        rename -highlightbackground -controlbackground controlBackground Background
133    }
134    pack $itk_component(zoomin) -padx 4 -pady 4
135    Rappture::Tooltip::for $itk_component(zoomin) "Zoom in"
136
137    itk_component add zoomout {
138        button $itk_component(controls).zout \
139            -borderwidth 1 -padx 1 -pady 1 \
140            -bitmap [Rappture::icon zoomout] \
141            -command [itcl::code $this _zoom out]
142    } {
143        usual
144        ignore -borderwidth
145        rename -highlightbackground -controlbackground controlBackground Background
146    }
147    pack $itk_component(zoomout) -padx 4 -pady 4
148    Rappture::Tooltip::for $itk_component(zoomout) "Zoom out"
149
150    itk_component add labels {
151        label $itk_component(controls).labels \
152            -borderwidth 1 -padx 1 -pady 1 \
153            -bitmap [Rappture::icon atoms]
154    } {
155        usual
156        ignore -borderwidth
157        rename -highlightbackground -controlbackground controlBackground Background
158    }
159    pack $itk_component(labels) -padx 4 -pady 8 -ipadx 1 -ipady 1
160    Rappture::Tooltip::for $itk_component(labels) "Show/hide the labels on atoms"
161    bind $itk_component(labels) <ButtonPress> \
162        [itcl::code $this emblems toggle]
163
164    #
165    # RENDERING AREA
166    #
167    itk_component add area {
168        frame $itk_interior.area
169    }
170    pack $itk_component(area) -expand yes -fill both
171    bind $itk_component(area) <Configure> \
172        [list $_dispatcher event -idle !fixsize]
173
174    itk_component add renderer {
175        vtkTkRenderWidget $itk_component(area).ren -rw $this-renWin
176    } {
177    }
178    pack $itk_component(renderer) -expand yes -fill both
179
180    eval itk_initialize $args
181
182    # prevent interactions -- use our own
183    blt::busy hold $itk_component(area) -cursor left_ptr
184    bind $itk_component(area)_Busy <ButtonPress> \
185        [itcl::code $this _move click %x %y]
186    bind $itk_component(area)_Busy <B1-Motion> \
187        [itcl::code $this _move drag %x %y]
188    bind $itk_component(area)_Busy <ButtonRelease> \
189        [itcl::code $this _move release %x %y]
190
191    emblems on
192
193    # create a photo for download snapshots
194    set _download [image create photo]
195}
196
197# ----------------------------------------------------------------------
198# DESTRUCTOR
199# ----------------------------------------------------------------------
200itcl::body Rappture::MoleculeViewer::destructor {} {
201    rename $this-renWin ""
202    rename $this-ren ""
203    rename $this-int ""
204    rename $this-sphere ""
205    rename $this-map ""
206    rename $this-xyzconv ""
207
208    image delete $_download
209}
210
211# ----------------------------------------------------------------------
212# USAGE: add <dataobj> ?<settings>?
213#
214# Clients use this to add a data object to the plot.  The optional
215# <settings> are used to configure the plot.  Allowed settings are
216# -color, -brightness, -width, -linestyle, and -raise. Only
217# -brightness and -raise do anything.
218# ----------------------------------------------------------------------
219itcl::body Rappture::MoleculeViewer::add {dataobj {settings ""}} {
220    array set params {
221        -color auto
222        -brightness 0
223        -width 1
224        -raise 0
225        -linestyle solid
226        -description ""
227    }
228    foreach {opt val} $settings {
229        if {![info exists params($opt)]} {
230            error "bad settings \"$opt\": should be [join [lsort [array names params]] {, }]"
231        }
232        set params($opt) $val
233    }
234 
235    set pos [lsearch -exact $dataobj $_dlist]
236
237    if {$pos < 0} {
238        if {![Rappture::library isvalid $dataobj]} {
239            error "bad value \"$dataobj\": should be Rappture::library object"
240        }
241   
242        set emblem [$dataobj get components.molecule.about.emblems]
243        if {$emblem == "" || ![string is boolean $emblem] || !$emblem} {
244            emblems off
245        } else {
246            emblems on
247        }
248
249        lappend _dlist $dataobj
250        set _dobj2raise($dataobj) $params(-raise)
251
252        $_dispatcher event -idle !redraw
253    }
254}
255
256# ----------------------------------------------------------------------
257# USAGE: get
258#
259# Clients use this to query the list of objects being plotted, in
260# order from bottom to top of this result.
261# ----------------------------------------------------------------------
262itcl::body Rappture::MoleculeViewer::get {} {
263    # put the dataobj list in order according to -raise options
264    set dlist $_dlist
265    foreach obj $dlist {
266        if {[info exists _dobj2raise($obj)] && $_dobj2raise($obj)} {
267            set i [lsearch -exact $dlist $obj]
268            if {$i >= 0} {
269                set dlist [lreplace $dlist $i $i]
270                lappend dlist $obj
271            }
272        }
273    }
274    return $dlist
275}
276
277# ----------------------------------------------------------------------
278# USAGE: delete ?<dataobj> <dataobj> ...?
279#
280# Clients use this to delete a dataobj from the plot. If no dataobjs
281# are specified, then all dataobjs are deleted.
282# ----------------------------------------------------------------------
283itcl::body Rappture::MoleculeViewer::delete {args} {
284    if {[llength $args] == 0} {
285        set args $_dlist
286    }
287
288    # delete all specified dataobjs
289    set changed 0
290    foreach dataobj $args {
291        set pos [lsearch -exact $_dlist $dataobj]
292        if {$pos >= 0} {
293            set _dlist [lreplace $_dlist $pos $pos]
294            catch {unset _dobj2raise($dataobj)}
295            set changed 1
296        }
297    }
298
299    # if anything changed, then rebuild the plot
300    if {$changed} {
301        $_dispatcher event -idle !redraw
302    }
303}
304
305# ----------------------------------------------------------------------
306# USAGE: download coming
307# USAGE: download controls <downloadCommand>
308# USAGE: download now
309#
310# Clients use this method to create a downloadable representation
311# of the plot.  Returns a list of the form {ext string}, where
312# "ext" is the file extension (indicating the type of data) and
313# "string" is the data itself.
314# ----------------------------------------------------------------------
315itcl::body Rappture::MoleculeViewer::download {option args} {
316    switch $option {
317        coming {
318            if {[catch {blt::winop snap $itk_component(area) $_download}]} {
319                $_download configure -width 1 -height 1
320                $_download put #000000
321            }
322        }
323        controls {
324            # no controls for this download yet
325            return ""
326        }
327        now {
328            #
329            # Hack alert!  Need data in binary format,
330            # so we'll save to a file and read it back.
331            #
332            set tmpfile /tmp/image[pid].jpg
333            $_download write $tmpfile -format jpeg
334            set fid [open $tmpfile r]
335            fconfigure $fid -encoding binary -translation binary
336            set bytes [read $fid]
337            close $fid
338            file delete -force $tmpfile
339
340            return [list .jpg $bytes]
341        }
342        default {
343            error "bad option \"$option\": should be coming, controls, now"
344        }
345    }
346}
347
348# ----------------------------------------------------------------------
349# USAGE: _clear
350#
351# Used internally to clear the scene whenever it is about to change.
352# ----------------------------------------------------------------------
353itcl::body Rappture::MoleculeViewer::_clear {} {
354    foreach a $_actors {
355        $this-ren RemoveActor $a
356        rename $a ""
357    }
358    set _actors ""
359    catch {unset _label2atom}
360
361    foreach lim {xmin xmax ymin ymax zmin zmax} {
362        set _limits($lim) ""
363    }
364
365    $this-ren ResetCamera
366    $_dispatcher event -now !render
367}
368
369# ----------------------------------------------------------------------
370# USAGE: _redraw
371#
372# Used internally to rebuild the scene whenever options within this
373# widget change.  Destroys all actors and rebuilds them from scratch.
374# ----------------------------------------------------------------------
375itcl::body Rappture::MoleculeViewer::_redraw {} {
376    blt::busy hold $itk_component(hull); update
377
378    _clear
379
380    set dev [lindex [get] end]
381    if {"" != $dev} {
382        set lib [Rappture::library standard]
383
384        set counter 0
385        foreach atom [$dev children -type atom components.molecule] {
386            set symbol [$dev get components.molecule.$atom.symbol]
387            set xyz [$dev get components.molecule.$atom.xyz]
388            regsub {,} $xyz {} xyz
389
390            # update overall limits for molecules along all axes
391            foreach axis {x y z} val $xyz {
392                if {"" == $_limits(${axis}min)} {
393                    set _limits(${axis}min) $val
394                    set _limits(${axis}max) $val
395                } else {
396                    if {$val < $_limits(${axis}min)} {
397                        set _limits(${axis}min) $val
398                    }
399                    if {$val > $_limits(${axis}max)} {
400                        set _limits(${axis}max) $val
401                    }
402                }
403            }
404
405            # create an actor for each atom
406            set aname $this-actor[incr counter]
407            vtkActor $aname
408            $aname SetMapper $this-map
409            eval $aname SetPosition $xyz
410            $this-ren AddActor $aname
411
412            set sfac 0.7
413            set scale [$lib get elements.($symbol).scale]
414            if {$scale != ""} {
415                $aname SetScale [expr {$sfac*$scale}]
416            }
417            set color [$lib get elements.($symbol).color]
418            if {$color != ""} {
419                eval [$aname GetProperty] SetColor [_color2rgb $color]
420            }
421
422            lappend _actors $aname
423
424            # create a label for each atom
425            set lname $this-label$counter
426            vtkTextActor $lname
427            $lname SetInput "$counter $symbol"
428            $lname ScaledTextOff
429
430            set tprop [$lname GetTextProperty]
431            $tprop SetJustificationToCentered
432            $tprop SetVerticalJustificationToCentered
433            $tprop ShadowOn
434            $tprop SetColor 1 1 1
435
436            set _label2atom($lname) $aname
437            lappend _actors $lname
438        }
439        if {[$itk_component(labels) cget -relief] == "sunken"} {
440            emblems on
441        }
442        _zoom reset
443    }
444    $this-ren ResetCamera
445    $_dispatcher event -idle !render
446
447    blt::busy release $itk_component(hull)
448}
449
450# ----------------------------------------------------------------------
451# USAGE: _zoom in
452# USAGE: _zoom out
453# USAGE: _zoom reset
454#
455# Called automatically when the user clicks on one of the zoom
456# controls for this widget.  Changes the zoom for the current view.
457# ----------------------------------------------------------------------
458itcl::body Rappture::MoleculeViewer::_zoom {option} {
459    switch -- $option {
460        in {
461            [$this-ren GetActiveCamera] Zoom 1.25
462        }
463        out {
464            [$this-ren GetActiveCamera] Zoom 0.8
465        }
466        reset {
467            $this-ren ResetCamera
468            [$this-ren GetActiveCamera] SetViewAngle 30
469            _3dView 45 45 0
470        }
471    }
472    $_dispatcher event -later !fixsize
473    $_dispatcher event -idle !render
474}
475
476# ----------------------------------------------------------------------
477# USAGE: _move click <x> <y>
478# USAGE: _move drag <x> <y>
479# USAGE: _move release <x> <y>
480#
481# Called automatically when the user clicks/drags/releases in the
482# plot area.  Moves the plot according to the user's actions.
483# ----------------------------------------------------------------------
484itcl::body Rappture::MoleculeViewer::_move {option x y} {
485    switch -- $option {
486        click {
487            blt::busy configure $itk_component(area) -cursor fleur
488            set _click(x) $x
489            set _click(y) $y
490            set _click(theta) $_view(theta)
491            set _click(phi) $_view(phi)
492            set _click(psi) $_view(psi)
493        }
494        drag {
495            if {[array size _click] == 0} {
496                _move click $x $y
497            } else {
498                set w [winfo width $itk_component(renderer)]
499                set h [winfo height $itk_component(renderer)]
500                if {$w <= 0 || $h <= 0} {
501                    return
502                }
503
504                if {[catch {
505                    # this fails sometimes for no apparent reason
506                    set dx [expr {double($x-$_click(x))/$w}]
507                    set dy [expr {double($y-$_click(y))/$h}]
508                }]} {
509                    return
510                }
511
512                #
513                # Rotate the camera in 3D
514                #
515                if {$_view(psi) > 90 || $_view(psi) < -90} {
516                    # when psi is flipped around, theta moves backwards
517                    set dy [expr {-$dy}]
518                }
519                set theta [expr {$_view(theta) - $dy*180}]
520                while {$theta < 0} { set theta [expr {$theta+180}] }
521                while {$theta > 180} { set theta [expr {$theta-180}] }
522                #if {$theta < 2} { set theta 2 }
523                #if {$theta > 178} { set theta 178 }
524
525                if {$theta > 45 && $theta < 135} {
526                    set phi [expr {$_view(phi) - $dx*360}]
527                    while {$phi < 0} { set phi [expr {$phi+360}] }
528                    while {$phi > 360} { set phi [expr {$phi-360}] }
529                    set psi $_view(psi)
530                } else {
531                    set phi $_view(phi)
532                    set psi [expr {$_view(psi) - $dx*360}]
533                    while {$psi < -180} { set psi [expr {$psi+360}] }
534                    while {$psi > 180} { set psi [expr {$psi-360}] }
535                }
536
537                _3dView $theta $phi $psi
538                emblems fixPosition
539                $_dispatcher event -idle !render
540
541                set _click(x) $x
542                set _click(y) $y
543            }
544        }
545        release {
546            _move drag $x $y
547            blt::busy configure $itk_component(area) -cursor left_ptr
548            catch {unset _click}
549        }
550        default {
551            error "bad option \"$option\": should be click, drag, release"
552        }
553    }
554}
555
556# ----------------------------------------------------------------------
557# USAGE: _3dView <theta> <phi> <psi>
558#
559# Used internally to change the position of the camera for 3D data
560# sets.  Sets the camera according to the angles <theta> (angle from
561# the z-axis) and <phi> (angle from the x-axis in the x-y plane).
562# Both angles are in degrees.
563# ----------------------------------------------------------------------
564itcl::body Rappture::MoleculeViewer::_3dView {theta phi psi} {
565    set deg2rad 0.0174532927778
566    set xp [expr {sin($theta*$deg2rad)*cos($phi*$deg2rad)}]
567    set yp [expr {sin($theta*$deg2rad)*sin($phi*$deg2rad)}]
568    set zp [expr {cos($theta*$deg2rad)}]
569
570    set blank 0
571    foreach lim {xmin xmax ymin ymax zmin zmax} {
572        if {"" == $_limits($lim)} {
573            set blank 1
574            break
575        }
576    }
577    if {$blank} {
578        set xm 0
579        set ym 0
580        set zm 0
581    } else {
582        set xm [expr {0.5*($_limits(xmax)+$_limits(xmin))}]
583        set ym [expr {0.5*($_limits(ymax)+$_limits(ymin))}]
584        set zm [expr {0.5*($_limits(zmax)+$_limits(zmin))}]
585    }
586
587    set cam [$this-ren GetActiveCamera]
588    set zoom [$cam GetViewAngle]
589    $cam SetViewAngle 30
590
591    $cam SetFocalPoint $xm $ym $zm
592    $cam SetPosition [expr {$xm-$xp}] [expr {$ym-$yp}] [expr {$zm+$zp}]
593    $cam ComputeViewPlaneNormal
594    $cam SetViewUp 0 0 1  ;# z-dir is up
595    $cam OrthogonalizeViewUp
596    $cam Azimuth $psi
597    $this-ren ResetCamera
598    $cam SetViewAngle $zoom
599
600    # fix up the labels so they sit over the new atom positions
601    emblems fixPosition
602
603    set _view(theta) $theta
604    set _view(phi) $phi
605    set _view(psi) $psi
606}
607
608# ----------------------------------------------------------------------
609# USAGE: emblems on
610# USAGE: emblems off
611# USAGE: emblems toggle
612# USAGE: emblems fixPosition
613#
614# Used internally to turn labels associated with atoms on/off, and to
615# update the positions of the labels so they sit on top of each atom.
616# ----------------------------------------------------------------------
617itcl::body Rappture::MoleculeViewer::emblems {option} {
618    switch -- $option {
619        on {
620            set state 1
621        }
622        off {
623            set state 0
624        }
625        toggle {
626            if {[$itk_component(labels) cget -relief] == "sunken"} {
627                set state 0
628            } else {
629                set state 1
630            }
631        }
632        fixPosition {
633            foreach lname [array names _label2atom] {
634                set aname $_label2atom($lname)
635                set xyz [$aname GetPosition]
636                eval $this-xyzconv SetValue $xyz
637                set xy [$this-xyzconv GetComputedViewportValue $this-ren]
638                eval $lname SetDisplayPosition $xy
639            }
640            return
641        }
642        default {
643            error "bad option \"$option\": should be on, off, toggle, fixPosition"
644        }
645    }
646
647    if {$state} {
648        $itk_component(labels) configure -relief sunken
649        foreach lname [array names _label2atom] {
650            catch {$this-ren AddActor2D $lname}
651        }
652        emblems fixPosition
653    } else {
654        $itk_component(labels) configure -relief raised
655        foreach lname [array names _label2atom] {
656            catch {$this-ren RemoveActor $lname}
657        }
658    }
659    $_dispatcher event -idle !render
660}
661
662# ----------------------------------------------------------------------
663# USAGE: _color2rgb color
664#
665# Used internally to convert a Tk color name into the r,g,b values
666# used in Vtk (scaled 0-1).
667# ----------------------------------------------------------------------
668itcl::body Rappture::MoleculeViewer::_color2rgb {color} {
669    foreach {r g b} [winfo rgb $itk_component(hull) $color] {}
670    set r [expr {$r/65535.0}]
671    set g [expr {$g/65535.0}]
672    set b [expr {$b/65535.0}]
673    return [list $r $g $b]
674}
675
676# ----------------------------------------------------------------------
677# OPTION: -backdrop
678# ----------------------------------------------------------------------
679itcl::configbody Rappture::MoleculeViewer::backdrop {
680    eval $this-ren SetBackground [_color2rgb $itk_option(-backdrop)]
681    $_dispatcher event -idle !render
682}
683
684# ----------------------------------------------------------------------
685# OPTION: -device
686# ----------------------------------------------------------------------
687itcl::configbody Rappture::MoleculeViewer::device {
688    if {"" != $itk_option(-device)
689          && ![Rappture::library isvalid $itk_option(-device)]} {
690        error "bad value \"$itk_option(-device)\": should be Rappture::library object"
691    }
692    delete
693
694    if {"" != $itk_option(-device)} {
695        add $itk_option(-device)
696        set state [$itk_option(-device) get components.molecule.about.emblems]
697        if {$state == "" || ![string is boolean $state] || !$state} {
698            emblems off
699        } else {
700            emblems on
701        }
702    }
703    $_dispatcher event -idle !redraw
704}
Note: See TracBrowser for help on using the repository browser.