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

Last change on this file since 4730 was 3813, checked in by ldelgass, 11 years ago

Fix bug in 'add' method of viewer widgets: list search for existing dataobj
entry was wrong (list and pattern transposed), causing potential duplicate
entries in dataobj list.

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