source: branches/blt4/gui/scripts/moleculeViewer.tcl @ 1897

Last change on this file since 1897 was 1897, checked in by gah, 14 years ago

re-merge with latest trunk changes

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