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

Last change on this file since 3508 was 3330, checked in by gah, 12 years ago

merge (by hand) with Rappture1.2 branch

File size: 23.2 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    foreach {opt val} $settings {
238        if {![info exists params($opt)]} {
239            error "bad settings \"$opt\": should be [join [lsort [array names params]] {, }]"
240        }
241        set params($opt) $val
242    }
243 
244    set pos [lsearch -exact $dataobj $_dlist]
245
246    if {$pos < 0} {
247        if {![Rappture::library isvalid $dataobj]} {
248            error "bad value \"$dataobj\": should be Rappture::library object"
249        }
250   
251        set emblem [$dataobj get components.molecule.about.emblems]
252        if {$emblem == "" || ![string is boolean $emblem] || !$emblem} {
253            emblems off
254        } else {
255            emblems on
256        }
257
258        lappend _dlist $dataobj
259        set _dobj2raise($dataobj) $params(-raise)
260
261        $_dispatcher event -idle !redraw
262    }
263}
264
265# ----------------------------------------------------------------------
266# USAGE: get
267#
268# Clients use this to query the list of objects being plotted, in
269# order from bottom to top of this result.
270# ----------------------------------------------------------------------
271itcl::body Rappture::MoleculeViewer::get {} {
272    # put the dataobj list in order according to -raise options
273    set dlist $_dlist
274    foreach obj $dlist {
275        if {[info exists _dobj2raise($obj)] && $_dobj2raise($obj)} {
276            set i [lsearch -exact $dlist $obj]
277            if {$i >= 0} {
278                set dlist [lreplace $dlist $i $i]
279                lappend dlist $obj
280            }
281        }
282    }
283    return $dlist
284}
285
286# ----------------------------------------------------------------------
287# USAGE: delete ?<dataobj> <dataobj> ...?
288#
289# Clients use this to delete a dataobj from the plot. If no dataobjs
290# are specified, then all dataobjs are deleted.
291# ----------------------------------------------------------------------
292itcl::body Rappture::MoleculeViewer::delete {args} {
293    if {[llength $args] == 0} {
294        set args $_dlist
295    }
296
297    # delete all specified dataobjs
298    set changed 0
299    foreach dataobj $args {
300        set pos [lsearch -exact $_dlist $dataobj]
301        if {$pos >= 0} {
302            set _dlist [lreplace $_dlist $pos $pos]
303            catch {unset _dobj2raise($dataobj)}
304            set changed 1
305        }
306    }
307
308    # if anything changed, then rebuild the plot
309    if {$changed} {
310        $_dispatcher event -idle !redraw
311    }
312}
313
314# ----------------------------------------------------------------------
315# USAGE: download coming
316# USAGE: download controls <downloadCommand>
317# USAGE: download now
318#
319# Clients use this method to create a downloadable representation
320# of the plot.  Returns a list of the form {ext string}, where
321# "ext" is the file extension (indicating the type of data) and
322# "string" is the data itself.
323# ----------------------------------------------------------------------
324itcl::body Rappture::MoleculeViewer::download {option args} {
325    switch $option {
326        coming {
327            if {[catch {blt::winop snap $itk_component(area) $_download}]} {
328                $_download configure -width 1 -height 1
329                $_download put #000000
330            }
331        }
332        controls {
333            # no controls for this download yet
334            return ""
335        }
336        now {
337            # Get the image data (as base64) and decode it back to binary.
338            # This is better than writing to temporary files.  When we switch
339            # to the BLT picture image it won't be necessary to decode the
340            # image data.
341            set bytes [$_download data -format "jpeg -quality 100"]
342            set bytes [Rappture::encoding::decode -as b64 $bytes]
343            return [list .jpg $bytes]
344        }
345        default {
346            error "bad option \"$option\": should be coming, controls, now"
347        }
348    }
349}
350
351# ----------------------------------------------------------------------
352# USAGE: _clear
353#
354# Used internally to clear the scene whenever it is about to change.
355# ----------------------------------------------------------------------
356itcl::body Rappture::MoleculeViewer::_clear {} {
357    foreach a $_actors {
358        $this-ren RemoveActor $a
359        rename $a ""
360    }
361    set _actors ""
362    catch {unset _label2atom}
363
364    foreach lim {xmin xmax ymin ymax zmin zmax} {
365        set _limits($lim) ""
366    }
367
368    $this-ren ResetCamera
369    $_dispatcher event -now !render
370}
371
372# ----------------------------------------------------------------------
373# USAGE: _redraw
374#
375# Used internally to rebuild the scene whenever options within this
376# widget change.  Destroys all actors and rebuilds them from scratch.
377# ----------------------------------------------------------------------
378itcl::body Rappture::MoleculeViewer::_redraw {} {
379    blt::busy hold $itk_component(hull)
380
381    _clear
382
383    set dev [lindex [get] end]
384    if {"" != $dev} {
385        set lib [Rappture::library standard]
386
387        set counter 0
388        foreach atom [$dev children -type atom components.molecule] {
389            set symbol [$dev get components.molecule.$atom.symbol]
390            set xyz [$dev get components.molecule.$atom.xyz]
391            regsub {,} $xyz {} xyz
392
393            # update overall limits for molecules along all axes
394            foreach axis {x y z} val $xyz {
395                if {"" == $_limits(${axis}min)} {
396                    set _limits(${axis}min) $val
397                    set _limits(${axis}max) $val
398                } else {
399                    if {$val < $_limits(${axis}min)} {
400                        set _limits(${axis}min) $val
401                    }
402                    if {$val > $_limits(${axis}max)} {
403                        set _limits(${axis}max) $val
404                    }
405                }
406            }
407
408            # create an actor for each atom
409            set aname $this-actor[incr counter]
410            vtkActor $aname
411            $aname SetMapper $this-map
412            eval $aname SetPosition $xyz
413            $this-ren AddActor $aname
414
415            set sfac 0.7
416            set scale [$lib get elements.($symbol).scale]
417            if {$scale != ""} {
418                $aname SetScale [expr {$sfac*$scale}]
419            }
420            set color [$lib get elements.($symbol).color]
421            if {$color != ""} {
422                eval [$aname GetProperty] SetColor [_color2rgb $color]
423            }
424
425            lappend _actors $aname
426
427            # create a label for each atom
428            set lname $this-label$counter
429            vtkTextActor $lname
430            $lname SetInput "$counter $symbol"
431            $lname ScaledTextOff
432
433            set tprop [$lname GetTextProperty]
434            $tprop SetJustificationToCentered
435            $tprop SetVerticalJustificationToCentered
436            $tprop ShadowOn
437            $tprop SetColor 1 1 1
438
439            set _label2atom($lname) $aname
440            lappend _actors $lname
441        }
442        if {[$itk_component(labels) cget -relief] == "sunken"} {
443            emblems on
444        }
445        _zoom reset
446    }
447    $this-ren ResetCamera
448    $_dispatcher event -idle !render
449
450    blt::busy release $itk_component(hull)
451}
452
453# ----------------------------------------------------------------------
454# USAGE: _zoom in
455# USAGE: _zoom out
456# USAGE: _zoom reset
457#
458# Called automatically when the user clicks on one of the zoom
459# controls for this widget.  Changes the zoom for the current view.
460# ----------------------------------------------------------------------
461itcl::body Rappture::MoleculeViewer::_zoom {option} {
462    switch -- $option {
463        in {
464            [$this-ren GetActiveCamera] Zoom 1.25
465        }
466        out {
467            [$this-ren GetActiveCamera] Zoom 0.8
468        }
469        reset {
470            $this-ren ResetCamera
471            [$this-ren GetActiveCamera] SetViewAngle 30
472            _3dView 45 45 0
473        }
474    }
475    $_dispatcher event -later !fixsize
476    $_dispatcher event -idle !render
477}
478
479# ----------------------------------------------------------------------
480# USAGE: _move click <x> <y>
481# USAGE: _move drag <x> <y>
482# USAGE: _move release <x> <y>
483#
484# Called automatically when the user clicks/drags/releases in the
485# plot area.  Moves the plot according to the user's actions.
486# ----------------------------------------------------------------------
487itcl::body Rappture::MoleculeViewer::_move {option x y} {
488    switch -- $option {
489        click {
490            blt::busy configure $itk_component(area) -cursor fleur
491            set _click(x) $x
492            set _click(y) $y
493            set _click(theta) $_view(theta)
494            set _click(phi) $_view(phi)
495            set _click(psi) $_view(psi)
496        }
497        drag {
498            if {[array size _click] == 0} {
499                _move click $x $y
500            } else {
501                set w [winfo width $itk_component(renderer)]
502                set h [winfo height $itk_component(renderer)]
503                if {$w <= 0 || $h <= 0} {
504                    return
505                }
506
507                if {[catch {
508                    # this fails sometimes for no apparent reason
509                    set dx [expr {double($x-$_click(x))/$w}]
510                    set dy [expr {double($y-$_click(y))/$h}]
511                }]} {
512                    return
513                }
514
515                #
516                # Rotate the camera in 3D
517                #
518                if {$_view(psi) > 90 || $_view(psi) < -90} {
519                    # when psi is flipped around, theta moves backwards
520                    set dy [expr {-$dy}]
521                }
522                set theta [expr {$_view(theta) - $dy*180}]
523                while {$theta < 0} { set theta [expr {$theta+180}] }
524                while {$theta > 180} { set theta [expr {$theta-180}] }
525                #if {$theta < 2} { set theta 2 }
526                #if {$theta > 178} { set theta 178 }
527
528                if {$theta > 45 && $theta < 135} {
529                    set phi [expr {$_view(phi) - $dx*360}]
530                    while {$phi < 0} { set phi [expr {$phi+360}] }
531                    while {$phi > 360} { set phi [expr {$phi-360}] }
532                    set psi $_view(psi)
533                } else {
534                    set phi $_view(phi)
535                    set psi [expr {$_view(psi) - $dx*360}]
536                    while {$psi < -180} { set psi [expr {$psi+360}] }
537                    while {$psi > 180} { set psi [expr {$psi-360}] }
538                }
539
540                _3dView $theta $phi $psi
541                emblems fixPosition
542                $_dispatcher event -idle !render
543
544                set _click(x) $x
545                set _click(y) $y
546            }
547        }
548        release {
549            _move drag $x $y
550            blt::busy configure $itk_component(area) -cursor left_ptr
551            catch {unset _click}
552        }
553        default {
554            error "bad option \"$option\": should be click, drag, release"
555        }
556    }
557}
558
559# ----------------------------------------------------------------------
560# USAGE: _3dView <theta> <phi> <psi>
561#
562# Used internally to change the position of the camera for 3D data
563# sets.  Sets the camera according to the angles <theta> (angle from
564# the z-axis) and <phi> (angle from the x-axis in the x-y plane).
565# Both angles are in degrees.
566# ----------------------------------------------------------------------
567itcl::body Rappture::MoleculeViewer::_3dView {theta phi psi} {
568    set deg2rad 0.0174532927778
569    set xp [expr {sin($theta*$deg2rad)*cos($phi*$deg2rad)}]
570    set yp [expr {sin($theta*$deg2rad)*sin($phi*$deg2rad)}]
571    set zp [expr {cos($theta*$deg2rad)}]
572
573    set blank 0
574    foreach lim {xmin xmax ymin ymax zmin zmax} {
575        if {"" == $_limits($lim)} {
576            set blank 1
577            break
578        }
579    }
580    if {$blank} {
581        set xm 0
582        set ym 0
583        set zm 0
584    } else {
585        set xm [expr {0.5*($_limits(xmax)+$_limits(xmin))}]
586        set ym [expr {0.5*($_limits(ymax)+$_limits(ymin))}]
587        set zm [expr {0.5*($_limits(zmax)+$_limits(zmin))}]
588    }
589
590    set cam [$this-ren GetActiveCamera]
591    set zoom [$cam GetViewAngle]
592    $cam SetViewAngle 30
593
594    $cam SetFocalPoint $xm $ym $zm
595    $cam SetPosition [expr {$xm-$xp}] [expr {$ym-$yp}] [expr {$zm+$zp}]
596    $cam ComputeViewPlaneNormal
597    $cam SetViewUp 0 0 1  ;# z-dir is up
598    $cam OrthogonalizeViewUp
599    $cam Azimuth $psi
600    $this-ren ResetCamera
601    $cam SetViewAngle $zoom
602
603    # fix up the labels so they sit over the new atom positions
604    emblems fixPosition
605
606    set _view(theta) $theta
607    set _view(phi) $phi
608    set _view(psi) $psi
609}
610
611# ----------------------------------------------------------------------
612# USAGE: emblems on
613# USAGE: emblems off
614# USAGE: emblems toggle
615# USAGE: emblems fixPosition
616#
617# Used internally to turn labels associated with atoms on/off, and to
618# update the positions of the labels so they sit on top of each atom.
619# ----------------------------------------------------------------------
620itcl::body Rappture::MoleculeViewer::emblems {option} {
621    switch -- $option {
622        on {
623            set state 1
624        }
625        off {
626            set state 0
627        }
628        toggle {
629            if {[$itk_component(labels) cget -relief] == "sunken"} {
630                set state 0
631            } else {
632                set state 1
633            }
634        }
635        fixPosition {
636            foreach lname [array names _label2atom] {
637                set aname $_label2atom($lname)
638                set xyz [$aname GetPosition]
639                eval $this-xyzconv SetValue $xyz
640                set xy [$this-xyzconv GetComputedViewportValue $this-ren]
641                eval $lname SetDisplayPosition $xy
642            }
643            return
644        }
645        default {
646            error "bad option \"$option\": should be on, off, toggle, fixPosition"
647        }
648    }
649
650    if {$state} {
651        $itk_component(labels) configure -relief sunken
652        foreach lname [array names _label2atom] {
653            catch {$this-ren AddActor2D $lname}
654        }
655        emblems fixPosition
656    } else {
657        $itk_component(labels) configure -relief raised
658        foreach lname [array names _label2atom] {
659            catch {$this-ren RemoveActor $lname}
660        }
661    }
662    $_dispatcher event -idle !render
663}
664
665# ----------------------------------------------------------------------
666# USAGE: _color2rgb color
667#
668# Used internally to convert a Tk color name into the r,g,b values
669# used in Vtk (scaled 0-1).
670# ----------------------------------------------------------------------
671itcl::body Rappture::MoleculeViewer::_color2rgb {color} {
672    foreach {r g b} [winfo rgb $itk_component(hull) $color] {}
673    set r [expr {$r/65535.0}]
674    set g [expr {$g/65535.0}]
675    set b [expr {$b/65535.0}]
676    return [list $r $g $b]
677}
678
679# ----------------------------------------------------------------------
680# OPTION: -backdrop
681# ----------------------------------------------------------------------
682itcl::configbody Rappture::MoleculeViewer::backdrop {
683    eval $this-ren SetBackground [_color2rgb $itk_option(-backdrop)]
684    $_dispatcher event -idle !render
685}
686
687# ----------------------------------------------------------------------
688# OPTION: -device
689# ----------------------------------------------------------------------
690itcl::configbody Rappture::MoleculeViewer::device {
691    if {"" != $itk_option(-device)
692          && ![Rappture::library isvalid $itk_option(-device)]} {
693        error "bad value \"$itk_option(-device)\": should be Rappture::library object"
694    }
695    delete
696
697    if {"" != $itk_option(-device)} {
698        add $itk_option(-device)
699        set state [$itk_option(-device) get components.molecule.about.emblems]
700        if {$state == "" || ![string is boolean $state] || !$state} {
701            emblems off
702        } else {
703            emblems on
704        }
705    }
706    $_dispatcher event -idle !redraw
707}
Note: See TracBrowser for help on using the repository browser.