source: branches/1.3/gui/scripts/moleculeViewer.tcl @ 4449

Last change on this file since 4449 was 3844, checked in by ldelgass, 11 years ago

Sync with trunk. Branch now differs only from trunk by r3722 (branch is version
1.3, trunk is version 1.4)

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.