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

Last change on this file since 1342 was 1342, checked in by gah, 15 years ago

preliminary HQ output from molvisviewer; unexpand tabs; all jpeg generation at 100%

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