Skip to main content

Movies, Scenes, States/Frames, Sequence Viewer

Map of four coupled PyMOL subsystems. The engine (packages/engine/, C++ + Python cmd) is unmodified upstream and is reached over the Python bridge. Every claim below is anchored to a file:line that was read. Where the port stands. Movie panel and transport: apps/web/src/features/movie/ (MovieTimeline, TransportBar, MovieEditors, ExportDialog, msetParser.ts, timeline.ts, mvprg.ts) over packages/bridge/tenmol_bridge/panels/movie.py and packages/protocol/src/topics/movie{,_panel}.ts. Scenes: apps/web/src/features/scenes/ over packages/protocol/src/topics/scenes.ts. Sequence viewer: apps/web/src/features/seqview/ over packages/bridge/tenmol_bridge/panels/seqview.py. The clock stays on the backend (§0), and the frame stream arrives on packages/protocol/src/topics/frame.ts.

0. Executive summary of the area

Four coupled subsystems live here:
  1. States vs. frames. Objects have states (coordinate sets). The viewer has frames. By default 1 frame = 1 state; mset defines an arbitrary frame→state map (packages/engine/modules/pymol/moving.py:691, packages/engine/layer1/Movie.cpp:979 MovieFrameToIndex).
  2. Movies. Per-frame command strings (mdo/mappend), per-frame key frames (mview, CViewElem) for camera and per-object matrices, plus playback (mplay/mstop/mtoggle) and export (mpng, movie.produce).
  3. Scenes. Named snapshots of view + colors + reps + enabled-state + frame + message
    • a PNG thumbnail (packages/engine/layer3/MovieScene.h:73, packages/engine/layer3/MovieScene.cpp:173).
  4. Sequence viewer (“Seeker”). An in-viewport text grid built in C++ (packages/engine/layer3/Seeker.cpp:969 SeekerUpdate) and drawn as GL text (packages/engine/layer1/Seq.cpp:259 CSeq::draw). Selection interaction mutates the active selection via generated cmd.select(...) strings (packages/engine/layer3/Seeker.cpp:169, :70).
Two of these (movie panel, sequence viewer) are Ortho blocks drawn inside the GL viewport upstream (packages/engine/layer1/Movie.cpp:1741, packages/engine/layer1/Seq.cpp:259, packages/engine/layer1/Control.cpp:536), plus scene buttons drawn as an overlay on the Scene block (packages/engine/layer1/Scene.cpp:2885 SceneDrawButtons, gated by scene_buttons, packages/engine/layer1/Scene.cpp:3456). All three are React components in the port and are never drawn by the engine.

Why parts of this area are server-rendered raster, not geometry

The molecular scene reaches the browser as geometry, but four features here produce server-rendered raster images:
  • cmd.ray_cmd.render (packages/engine/modules/pymol/viewing.py:1581) writes into G->Scene->Image.
  • cmd.draw_cmd.draw needs a live GL context (packages/engine/modules/pymol/viewing.py:1652, wrapped in _self._call_with_opengl_context at :1660).
  • cmd.mpng / movie.produce render every frame to PNG on disk (packages/engine/modules/pymol/moving.py:366, packages/engine/layer1/Movie.cpp:626 MovieModalPNG, packages/engine/modules/pymol/movie.py:846).
  • Scene thumbnails come from SceneDeferImage into a 220×124 PNG buffer (packages/engine/layer3/MovieScene.cpp:225-233, dims at packages/engine/layer3/MovieScene.h:97-99).
None of those can be produced client-side, so the bridge owns an offscreen GL context (packages/bridge/tenmol_bridge/glcontext/ — CGL, EGL, WGL) and streams encoded bytes (packages/bridge/tenmol_bridge/render/encode.py, render/framestream.py). cmd.draw documents that it “does not work when running in the command-line only mode” (packages/engine/modules/pymol/viewing.py:1630-1632), which is why a context-less bridge falls back to ray. Movie playback is driven by the backend idle loop (packages/engine/layer1/Scene.cpp:2432 SceneIdle, frame pacing at :2453-2480, rock at :2477-2484). The backend stays the single clock and pushes frame-change events; the browser never runs its own playback timer, which is what keeps the two from double-timing.

1. Frames & states — exact semantics

1.1 Frame/state queries

MoviePlaying() returns true also while movie commands are being evaluated (packages/engine/layer1/Movie.cpp:540-551), and returns false when I->Locked.

1.2 SceneSetFrame(G, mode, frame) — the whole navigation vocabulary

packages/engine/layer1/Scene.cpp:2121-2184. Modes: cmd.frame(n, trigger=-1, scene=0) is a separate entry (packages/engine/modules/pymol/moving.py:460) calling _cmd.frame(COb, n-1, trigger). Side effects of a frame change (packages/engine/layer1/Scene.cpp:2185-2210): clamp to [0, NFrame), compute newState = MovieFrameToIndex(...), at frame 0 recall the mmatrix matrix and abort any running animation, set settings frame and state, invalidate selection indicator CGOs and picking, optionally run the frame’s movie command, and set MovieFrameFlag if cache_frames.

1.3 Playback loop

SceneIdle (packages/engine/layer1/Scene.cpp:2432):
  • FPS from movie_fps; fps <= 0 → use movie_delay ms; fps < 0 → full speed (:2453-2464; same logic duplicated in SceneGetFPS packages/engine/layer1/Scene.cpp:342).
  • Adaptive LastFrameAdjust smoothing (:2465-2475).
  • At last frame: if movie_loopSceneSetFrame(G,7,0), else MoviePlay(G,cMovieStop) (:2485-2492).
  • If not playing but ControlRocking(G)SceneUpdateCameraRock every rock_delay ms (:2477-2484).
MoviePlay (packages/engine/layer1/Movie.cpp:555): cMovieToggle=-1, cMovieStop=0, cMoviePlay=1 (packages/engine/layer1/Movie.h:118-120). When not looping and already at the last frame, play/toggle first rewinds via SceneSetFrame(G,7,0) (:561-567, :573-579).

2. mset — the frame↔state program

cmd.mset(specification, frame=1, freeze=0)packages/engine/modules/pymol/moving.py:691. The entire mini-language is parsed in Python (:733-764) then handed to _cmd.mset(space-separated 0-based state list, start-1, freeze):
  • bare integer N → one frame showing state N.
  • xN → repeat previous state N times total (:743-751); if no previous, uses current state.
  • -N → ramp from previous state to N inclusive, direction auto (:752-760).
  • whitespace normalized, x/- split off adjacent tokens (:734-739).
Examples from the docstring (:709-722): mset 1, mset 1 x10, mset 1 x30 1 -15 15 x30 15 -1. cmd.madd (packages/engine/modules/pymol/moving.py:677) is literally mset(..., frame, freeze) — it appends using the same syntax. C side: MovieSet / MovieAppendSequence (packages/engine/layer1/Movie.cpp:877, :890). Redefining the movie clears existing mdo commands (documented at packages/engine/modules/pymol/moving.py:307-308, :349-351). Frame → state resolution: MovieFrameToIndex (packages/engine/layer1/Movie.cpp:979) — a per-frame ViewElem[frame].state_flag (set by mview store, state=…) overrides the Sequence[] map; otherwise Sequence[frame]; if no movie, frame == state.

3. Movie commands (mdo / mappend)

  • cmd.mdo(frame, command)_cmd.mdo(COb, frame-1, command, 0) — replaces (packages/engine/modules/pymol/moving.py:274, C: MovieSetCommand packages/engine/layer1/Movie.cpp:1074).
  • cmd.mappend(frame, command)_cmd.mdo(COb, frame-1, ";"+command, 1) — appends (packages/engine/modules/pymol/moving.py:323, C: MovieAppendCommand packages/engine/layer1/Movie.cpp:1378).
  • cmd.mdump() prints all defined commands to the feedback stream, format "%5d: %s\n" (packages/engine/modules/pymol/moving.py:81, C: MovieDump packages/engine/layer1/Movie.cpp:378-403). There is no structured getter — see §12 gaps.
  • Execution: MovieDoFrameCommand (packages/engine/layer1/Movie.cpp:1045) — at frame 0 recall mmatrix; if not locked, PParse the command string, then if the frame carries a scene_flag and the scene differs from scene_current_name, MovieSceneRecall(...) with view=false/frame=false (:1058-1065), then SceneFromViewElem.
cmd.mmatrix(action) with clear|store|recall|check_cmd.mmatrix(0..3) (packages/engine/modules/pymol/moving.py:772-816; C MovieMatrix packages/engine/layer1/Movie.cpp:589, constants packages/engine/layer1/Movie.h:161-164). Docstring warns not to mix with mview. cmd.mclear() clears the cached frame images (packages/engine/modules/pymol/moving.py:436; C MovieClearImages packages/engine/layer1/Movie.cpp:1432).

4. Key frames — mview and CViewElem

4.1 Python surface

cmd.mview(action='store', first=0, last=0, power=0.0, bias=-1.0, simple=-1, linear=0.0, object='', wrap=-1, hand=0, window=5, cycles=1, scene='', cut=0.5, quiet=1, auto=-1, state=0, freeze=0)packages/engine/modules/pymol/moving.py:160. Actions (packages/engine/modules/pymol/moving.py:145-156, mirrored in packages/engine/layer1/MViewAction.h:3-14): Python-side pre/post logic worth cloning verbatim:
  • negative first/last are counted from count_frames() (packages/engine/modules/pymol/moving.py:214-219).
  • scene='auto' or None → current scene_current_name; a non-empty scene triggers scene(scene,"recall",animate=0,frame=0) before the store (:221-225).
  • args are 0-based on the wire: first-1, last-1, state-1 (:228-232).
  • auto-reinterpolate: unless freeze, and if auto>0 or (auto<0 and movie_auto_interpolate), actions store/clear/toggle are followed by a second _cmd.mview(action=3, first=-1, last=-1, …) (:233-241).

4.2 CViewElem — what a key frame holds

packages/engine/layer1/View.h:24-63: 4×4 matrix (+flag), pre[3], post[3], front/back clip (+clip_flag), ortho (+flag), state (+state_flag), view_mode (0 relative / 1 absolute), specification_level (0 none, 1 interpolated, 2 key frame), timing (+flag), scene_name lexicon key (+scene_flag), power (+flag), bias (+flag). Interpolation entry point: ViewElemInterpolate(G, first, last, power, bias, simple, linearity, hand, cut) (packages/engine/layer1/View.h:93). Smoothing: ViewElemSmooth(first,last,window,loop) (packages/engine/layer1/View.h:92).

4.3 Frame-range editing (mmove / mcopy / mdelete / minsert)

All four funnel into _cmd.mmodify(COb, action, index, count, target, object, freeze, quiet): Semantics: 0 means “current frame” (get_frame()-1), negative counts back from the end and clamps so frame+count <= cur_len (:520-535, :621-630). count<0 in mdelete means “to the end” (:629-630). C side: MovieViewModify (packages/engine/layer1/Movie.cpp:89), enum ViewElemAction {Delete=-1, Insert=1, Move=2, Copy=3} (packages/engine/layer1/View.h:100-106). When not frozen/localized it calls ExecutiveMotionExtend (packages/engine/layer1/Movie.cpp:157-159).

5. The Movie Panel (in-viewport upstream)

CMovie is an Ortho Block (packages/engine/layer1/Movie.h:52). Height: MovieGetPanelHeight (packages/engine/layer1/Movie.cpp:1701) = movie_panel_row_height × ExecutiveCountMotions(G), or a single row when presentation is on (:1716-1721); zero when movie_panel==0 or there is nothing to show (:1703-1711). Rows come from ExecutiveMotionDraw (packages/engine/layer3/Executive.cpp:697): one row for the global camera (cExecAll, drawn by MovieDrawViewElemViewElemDraw(..., "camera", …), packages/engine/layer1/Movie.cpp:1728-1734) and one row per object that has motions (ObjectDrawViewElem, packages/engine/layer3/Executive.cpp:727). Row count = ExecutiveCountMotions (packages/engine/layer3/Executive.cpp:664), which falls back to 1 when SceneGetNFrame(G) > 1 (:684-685). Per-frame cell painting (packages/engine/layer1/View.cpp:158-260): color by specification_level — level 1 (interpolated) draws a thin center bar (bar_color {0.3,0.3,0.6} with key_color top edge and bot_color bottom edge, :189-232); level 2 (key frame) draws a full-height block in key_color {0.4,0.4,0.8} (:233-248). top_color {0.6,0.6,1.0} and bot_color {0.2,0.2,0.4} are the remaining palette entries (:172-175). X↔frame mapping: ViewElemXtoFrame(rect, frames, x, nearest) (packages/engine/layer1/View.h:98, wrapper MovieXtoFrame packages/engine/layer1/Movie.cpp:72). A horizontal ScrollBar doubles as the frame slider (packages/engine/layer1/Movie.cpp:1775-1793); dragging it issues SceneSetFrame(G, 7, value) (:1781-1784, and on click :1535). When there are no ViewElems at all the row is labelled "states" (packages/engine/layer1/Movie.cpp:1842-1844).

5.1 Complete mouse grammar of the panel

CMovie::click (packages/engine/layer1/Movie.cpp:1488), ::drag (:1574), ::release (:1609). tmpRect.right -= LabelIndent where LabelIndent = 8*8 dip unless presentation (packages/engine/layer1/Movie.cpp:1864-1868). Drag feedback rectangles (packages/engine/layer1/Movie.cpp:1797-1840): white outline on the source frame + grey filled box on the target; oblate = white outline + grey fill over the range; ins/del = green fill when inserting, red fill when deleting. Drag is abandoned if the pointer leaves ±50px vertically (:1580).

5.2 Panel context menus

Activated by ExecutiveMotionMenuActivate (packages/engine/layer3/Executive.cpp:737), which resolves which row was hit and passes the 1-based frame number as a string. camera_motion (packages/engine/modules/pymol/menu.py:108-124):
  • storecmd.mview("store",first=F)
  • store with scene → submenu of up to 40 scene names, each cmd.mview("store",scene=NAME,first=F) (packages/engine/modules/pymol/menu.py:54-59)
  • store with state → submenu current (state=-1), 1, n_state, plus 8 evenly spaced states (packages/engine/modules/pymol/menu.py:62-80)
  • clearcmd.mview("clear",first=F)
  • reset camera motionscmd.mview("reset")
  • purge entire moviecmd.mset()
  • smooth key framesa little / more (window=15) / a lot (window=30) (packages/engine/modules/pymol/menu.py:103-106)
  • interpolate, reinterpolate, uninterpolate
obj_motion (packages/engine/modules/pymol/menu.py:126-143): drag, store, store with state, reset (cmd.reset(object=…)), clear, reset object motions, purge object motions (mview("purge",object=…)), smooth key frames, interpolate / reinterpolate / uninterpolate, all with object="…".

6. Movie menu (menu bar) — full enumeration

Source: packages/engine/modules/pymol/_gui.py:234-376 (toolkit-independent; consumed by packages/engine/modules/pmg_qt/pymol_qt_gui.py:353 and packages/engine/modules/pmg_tk/skins/normal/__init__.py:1072).
Also in the File menu: Export Movie As ▸ MPEG… / Quicktime… / ─── / PNG Images… (packages/engine/modules/pymol/_gui.py:105-110).

6.1 “Last program” bookkeeping

PyMOLDesktopGUI.mvprg(command=None) (packages/engine/modules/pymol/_gui.py:958-968): remembers movie_start = cmd.get_movie_length() + 1 and movie_command = command % movie_start, then cmd.do(movie_command). Calling with None re-runs the stored command (“Update Last Program”). mvprg_remove_last (:950-956) issues cmd.mdelete(-1, self.movie_start). This is client-side state — it must live in the React store, not the bridge.

7. Movie programs (pymol.movie) — full API

packages/engine/modules/pymol/movie.py. FPS helper: get_movie_fps returns movie_fps or 30 if ≤0 (:26).

7.1 Legacy mdo-based generators (write per-frame commands)

7.2 Key-frame based generators (the ones the Movie menu uses)

All take start=0 meaning “append at get_movie_length()+1”. Internal helpers: _rock (:490, uses sweep_angle, power=-1 keys), _nutate_sub (:517), _nutate (:543).

7.3 Export

cmd.mpng(prefix, first=0, last=0, preserve=0, modal=0, mode=-1, quiet=1, width=0, height=0)packages/engine/modules/pymol/moving.py:366. mode: 2=ray, 1=draw, 0=normal, -1=check ray_trace_frames/draw_frames (:392-393). Asserts mode in (-1,0,1,2) (:427). Routes through _self._call_with_opengl_context unless ray (:431-434). C side MoviePNG (packages/engine/layer1/Movie.cpp:819) drives the modal loop MovieModalPNG (packages/engine/layer1/Movie.cpp:626) with stages in CMovieModal (packages/engine/layer1/Movie.h:22-50). movie.produce(filename, mode='', first=0, last=0, preserve=0, encoder='', quality=-1, quiet=1, width=0, height=0)packages/engine/modules/pymol/movie.py:846:
  • mode shortcut dict normal|draw|ray0|1|2 (:815-821).
  • Encoder autodetect: .mpeg/.mpgmpeg_encode, else ffmpeg, else convert, else raise (:915-925).
  • mpeg_encode uses .ppm frames; others .png and force opaque_background (:928-936).
  • mp4/mov/webm: dimensions forced even, aspect-preserving from the viewport (:938-951).
  • Temp dir is <basename>.tmp; preserve<1 deletes it at the end (:906-963, :812-813).
  • mpng writes mov%04d.png (_prefix="mov", :656), then _encode (:687) waits for all files (polling get_modal_draw(), :701) and shells out.
  • ffmpeg branches: GIF uses a two-pass palettegen/paletteuse (:765-771); webm uses libvpx-vp9 with crf = 65 - quality/2 (:778-781); otherwise crf 10/15/20 by quality and -pix_fmt yuv420p (:782-785).
  • convert uses -delay 100/fps (:794-802).
  • mpeg_encode maps quality to 1 + (100-q)*29/100 and snaps fps to [23.976,24,25,29.97,30,50,59.94,60] with a warning (:732-743).
  • Sets keep_alive during export and unsets it afterwards (:971, :811).
  • A _watch thread prints byte counts while encoding (:658-685).

7.4 Export dialog (Qt)

packages/engine/modules/pmg_qt/file_dialogs.py:691 file_save_mpeg(parent, _preselect=None), form packages/engine/modules/pmg_qt/forms/movieexport.ui: Fields: input_width, input_height (QSpinBox, seeded from get_viewport(), file_dialogs.py:795-797); preset buttons button_720p, button_480p, button_360p which set height and clamp aspect to ≤16:9 (:788-793, :808-811); input_encoder combo — "", ffmpeg, mpeg_encode, convert (movieexport.ui:161-177); input_quality spinbox seeded from movie_quality, disabled for ""/convert (:725, :798); input_ray checkbox seeded from ray_trace_frames (:799-800); format radios format_png/mp4/mpg/mov/gif enabled per encoder support matrix (:702-707); button_ok. _preselect='png' hides the format group (:737-739); _preselect='mov' forces ffmpeg (:747-750). Run: png → cmd.mpng(fname, width, height, mode=2 if ray else 1, quiet=0, modal=-1) (:772-777); otherwise cmd.movie.produce(fname, width, height, quality, mode, encoder, quiet=0) (:781-785). Legacy Tk equivalent: a Pmw “Movie Settings” dialog with Encoding Quality / Ray Trace Frames / Width / Height (packages/engine/modules/pmg_tk/skins/normal/__init__.py:964-987).

8. Scenes

8.1 cmd.scene — the one entry point

packages/engine/modules/pymol/viewing.py:1034:
Accepted actions (viewing.py:56-60): store, recall, clear, insert_before, insert_after, next, previous, start, update, rename, delete, order, sort, first, append. Python normalization before the C call (viewing.py:1086-1115):
  • key='auto' + action='recall'action='next' (that’s why bare scene advances).
  • action='update' preserves the existing message via _scene_get_current_message (viewing.py:1007, which reads the Message wizard that carries a from_scene attr).
  • Deprecated aliases: cleardelete; append/updatestore.
  • Presentation auto-quit: if the same next/previous action repeats while presentation and presentation_auto_quit are on and scene_current_name is empty, try chain_session() (loads the next numbered .pse/.psw, viewing.py:935-959), else cmd.quit().
  • The C call is wrapped in _self._call_with_opengl_context (viewing.py:1125) because storing a scene grabs a thumbnail.

8.2 C implementation — MovieSceneFunc

packages/engine/layer3/MovieScene.cpp:755:
  • insert_before / insert_after → remembers scene_current_name, rewrites action to store, then reorders with MovieSceneOrderBeforeAfter (:772-777, :814-815, :733).
  • next / previousMovieSceneGetNextKey then recall (:779-783). Wrap behavior: if scene_loop is off and we run off either end, returns ""; an empty scene_current_name forces looping (:700-728).
  • start → first key in order, then recall (:784-788).
  • key == "auto"scene_current_name (:789-791).
  • recall with key=="*"MovieScenePrintOrder (:794-795).
  • recall with an empty key → clear scene_current_name, ExecutiveSetObjVisib(G,"*",false) (blank screen), clear the message (:797-801).
  • store, delete, rename, order, sort (sorted natural order), first (move to top) (:808-826).
  • Always sets scenes_changed=true and fires side effects — this is the GUI refresh signal the React scene panel should subscribe to (:832-834).
Store (packages/engine/layer3/MovieScene.cpp:173): key ""/"new"getUniqueKey(); new keys are appended to order; SceneSetNames refreshes the scene-button list; scene_current_name is set. storemask bits STORE_VIEW|ACTIVE|COLOR|REP|FRAME|THUMBNAIL (packages/engine/layer3/MovieScene.h:27-34). Stores: message, SceneGetView, SceneGetFrame, a 220×124 PNG thumbnail via SceneDeferImage (:225-233), per-atom {color, visRep} keyed by unique_id for atoms of enabled objects only (:236-252), and per-object {color, visRep} with the enabled bit packed into bit 0 of visRep (:254-265). Recall (packages/engine/layer3/MovieScene.cpp:458, impl :485): each recall_* flag is ANDed with the stored storemask (:491-495); atom color/rep restored by unique_id (:506-528); objects invalidated with cRepInvVisib/cRepInvColor (:571); camera restored with SceneSetView(G, view, true, animate, 1) where animate == -1 resolves to get_scene_animation_duration (:576-579). get_scene_animation_duration (:429-438): scene_animation (-1 → fall back to animation), 0 → no animation, else scene_animation_duration. Frame recall (MovieSceneRecallFrame, :404-424): if the movie is playing use SceneSetFrame mode 10 (seek to this scene’s frame); if the frame is unchanged do nothing; otherwise honor scene_frame_mode (0, or -1 with a movie defined ⇒ don’t change the frame). Note it round-trips through Python cmd.set_frame to avoid a PBlock deadlock (:421-423) — relevant if we ever run the bridge on a non-main thread. Order (packages/engine/layer3/MovieScene.cpp:88/:95, Python cmd.scene_order packages/engine/modules/pymol/viewing.py:961): args names (space-separated string or list), sort (bool), locationtop|current|bottom (viewing.py:997-1005). Duplicate keys are an error (MovieScene.cpp:126-128); invalid location is an error (:132-135).

8.3 Scene helper cmds

cmd.scene_recall_message (packages/engine/modules/pymol/viewing.py:1013) is INTERNAL: it drives the message wizard, tagging it from_scene = 1; an empty message tears the wizard down. In React this becomes a scene-message overlay component, not a wizard. Legacy compatibility (session loading only): _legacy_scene (viewing.py:1132), session_restore_scenes (:1199), _convert_legacy_scene (:1232). Old scenes stored get_view()/get_vis()/get_frame()/get_colorection() plus _scene_<key>_<rep> selections over rep_list (viewing.py:52-54).

8.4 Scene menu (menu bar)

packages/engine/modules/pymol/_gui.py:775-805:

8.5 Scene Panel dialog (Qt)

packages/engine/modules/pmg_qt/scene_bin_gui.py:29 ScenePanel(QWidget):
  • Title “Scene Panel”, 365×700 (:45, :52).
  • Instruction label “Double click selected thumbnail to \nload into Workspace.” (:64-66).
  • Add Scene button → cmd.scene('new','append',quiet=0) then scroll to bottom (:253-260).
  • Table columns enum NAME=0, IMAGE=1, MESSAGE=2, ACTIONS=3 (:16-20), but the “condensed” layout shows only 2 columns ['Name','Scene Preview'] (:213-215), row height 100 (:130).
  • Vertical headers are glyphs used as drag handles for reordering (:231-242), reorder → cmd.scene_order(' '.join(names)) (:379-387).
  • Rename by editing the NAME cell → cmd.scene(old,'rename',new_key=new); names with spaces and blank names are rejected with a printed error (:360-377).
  • Update Scenecmd.scene(name,'update') + refresh thumbnail (:262-274).
  • Delete Scenecmd.scene(name,'clear') (:276-290).
  • Double-click row → cmd.scene(name,'recall') (:292-299).
  • Delete/Update buttons enabled only with a selection (:351-358).
  • Known placeholders: message and actions columns are hard-coded strings 'This is a base message' / 'Rock, zoom, something' (:170-171) — the React version should use cmd.get_scene_message / cmd.set_scene_message instead.
  • Repopulates on paint/focus events via an event filter (:102-113) — replace with the scenes_changed event.

8.6 Scene buttons (in-viewport overlay)

SceneDrawButtons (packages/engine/layer1/Scene.cpp:2885), enabled by scene_buttons (packages/engine/layer1/Scene.cpp:3456). Names come from SceneSetNames (:2870) into CScene::SceneVec of SceneElem{name, rect, drawn} (packages/engine/layer1/SceneElem.h:7-12, packages/engine/layer1/SceneDef.h:145). Layout: internal_gui_control_size line height (:2901), 8dip char width (:2896), a vertical scrollbar when entries exceed the visible rows (:2924-2946), name truncated to max_char (:3014-3016). Colors: pressed {0.7,0.7,0.7}, current scene {0.5,0.5,0.5}, others {0.25,0.25,0.25} (:2891-2893, :3029-3038). Mouse (packages/engine/layer1/SceneMouse.cpp:179 SceneClickSceneButton):
  • Left press → PressMode=1; on release over the same button → cmd.scene('NAME') (SceneMouse.cpp:1097-1104).
  • Middle press → rapid browse: recalls immediately on press, animate=0 when Ctrl is held (:196-212); continues to recall while dragging over other buttons (:1105-1114).
  • Right press → PressMode=3; on release over the same button opens the scene_menu popup (:1117-1126).
  • Dragging with PressMode=4 reorders: cmd.scene_order([a,b]) or cmd.scene_order([name], location='top') when dropped on the first slot (SceneMouse.cpp:1274-1300).
scene_menu (packages/engine/modules/pymol/menu.py:1842-1850): header Scene <name>, renamecmd.wizard("renaming", name, mode="scene"), updatecmd.scene(name,"update"), deletecmd.scene(name,"delete").

9. Camera: view get/set, interpolate, turn/move/zoom/orient/clip, rock

9.1 View vector

cmd.get_view(output=1, quiet=1)packages/engine/modules/pymol/viewing.py:634. _cmd.get_view returns 25 floats; Python slices to 18: r[0:3]+r[4:7]+r[8:11]+r[16:25] (:731). Layout documented at :661-678:
  • 0–8: column-major 3×3 model→camera rotation
  • 9–11: origin of rotation relative to camera (camera space)
  • 12–14: origin of rotation (model space)
  • 15: front plane distance
  • 16: rear plane distance
  • 17: orthoscopic flag (sign) and field of view when abs(value) > 1
  • camera looks down −Z, +X left, +Y down.
output modes: 0 = print, 1 = don’t, 2 = force print even while logging, 3 = return a formatted set_view (...) string (:651-657, :723-730). When logging is on it writes the matrix to the log file (:695-712). cmd.set_view(view, animate=0, quiet=1, hand=1)packages/engine/modules/pymol/viewing.py:734. Accepts a string (parsed by safe_list_eval) or a sequence; must be exactly 18 floats or it raises “bad view argument; should be a sequence of 18 floats” (:764-769). It re-expands to the 25-float form with an embedded 4×4 (:772-780). cmd.view(key, action='recall'|'store'|'clear', animate=-1)packages/engine/modules/pymol/viewing.py:783. This is a pure-Python dictionary pymol._view_dict + _view_dict_sc shortcut (:819-850); key='*' lists or clears all (:821-830). Saved into sessions by session_save_views/session_restore_views (:1187, :1192). F1–F12 fall back to views when no scene matches (see §11).

9.2 Camera animation

SceneSetView(G, view, quiet, animate, hand) interpolates through ani_elem key frames using ViewElemInterpolate(..., 2.0F, 1.0F, true, 0.0F, hand, 0.0F) (packages/engine/layer1/Scene.cpp:411-427), with timing stamps for start and start+duration.

9.3 Camera commands

9.4 Rock / spin / nutate (live, not movie)

cmd.rock(mode=-1)_cmd.rockControlRock(G, mode) (packages/engine/modules/pymol/viewing.py:1360, packages/engine/layer1/Control.cpp:415-439):
  • -2 = query only (returns the current rock setting without touching it)
  • -1 = toggle (default), 0 = off, 1 = on
  • turning on restarts the sweep timer; any non--2 mode restarts the frame timer. cmd.ray uses the -2 query then rock(0) to stop rocking before rendering (packages/engine/modules/pymol/viewing.py:1738-1739).
SceneUpdateCameraRock (packages/engine/layer1/Scene.cpp:2373-2427) — sweep_mode:
  • 0 = Y-axis rock, 1 = X-axis rock, 2 = Z-axis rock (“useless!”, :2405)
  • 3 = nutate (combined X/Y sinusoid with a π/2 phase shift, amplitude ramped in over the first half period, :2411-2425)
  • sweep_angle <= 0 degenerates into a continuous spin at 10*sweep_speed/0.75 deg-ish per render second (:2390-2392).
  • Phase from sweep_phase, speed from sweep_speed, tick rate from rock_delay ms.
ControlIdling (packages/engine/layer1/Control.cpp:397-403) keeps the idle loop alive while any of sdof / movie playing / rock / sculpting is active.

10. Internal GUI control bar (9 buttons, in-viewport)

CControl block, NButton = 9 (packages/engine/layer1/Control.h/packages/engine/layer1/Control.cpp:62), hit test which_button = (NButton * x) / control_width (packages/engine/layer1/Control.cpp:243-252). Release actions (packages/engine/layer1/Control.cpp:290-380): Double-clicking the left margin collapses/restores internal_gui_width to cControlMinWidth (packages/engine/layer1/Control.cpp:449-464); dragging it resizes. External Qt quick-button rows (packages/engine/modules/pmg_qt/pymol_qt_gui.py:223-253) — the movie row is |<, <, Stop, Play, >, >|, MClear bound to cmd.rewind/backward/mstop/mplay/ forward/ending/mclear; the row above has Unpick, Deselect, Rock (cmd.rock), Get View; the top row has Reset, Zoom (animate=1.0), Orient (animate=1.0), Draw/Ray. Legacy Tk equivalents at packages/engine/modules/pmg_tk/skins/normal/__init__.py:266-281.

11. Keyboard bindings in this area

packages/engine/modules/pymol/shortcut_dict.py (defaults; user-overridable through cmd.set_key, packages/engine/modules/pymol/controlling.py:746-757): Bare F1–F12 have no default binding; _special (packages/engine/modules/pymol/internal.py:447-484) first checks explicit set_key mappings, then tries cmd.scene against get_scene_list(), then cmd.view against pymol._view_dict_sc, including a prefix auto-completion pass with key + '-' (:469-480). Special key codes are at packages/engine/modules/pymol/internal.py:398-424; modifier prefixes '', SHFT, CTRL, CTSH, ALT (:395-401). Spacebar (packages/engine/layer1/Ortho.cpp:855-874): only when the command line is empty. In presentation mode plain space = cmd.scene('','next'), Shift+space = rewind;mplay. Otherwise plain space = mtoggle, Shift+space = rewind;mplay.

12. Sequence viewer (“Seeker”)

12.1 What it is today

Two C++ pieces:
  • CSeq — the Ortho block: layout, scrollbar, hit-testing, GL text drawing (packages/engine/layer1/Seq.h:73, packages/engine/layer1/Seq.cpp:259 CSeq::draw). Metrics: LineHeight = 13, CharWidth = 8, ScrollBarWidth = 16, ScrollBarMargin = 2, CharMargin = 2 (packages/engine/layer1/Seq.h:84-88), all scaled by DIP2PIXEL. Total height = LineHeight*NRow + 4 (+ scrollbar) (packages/engine/layer1/Seq.cpp:190-201).
  • CSeeker — the data model + interaction handler (packages/engine/layer3/Seeker.cpp:38), installed via SeqSetHandler at the end of SeekerUpdate (packages/engine/layer3/Seeker.cpp:1948).
Refresh protocol: SeqChanged marks “rebuild” and SeqDirty marks “recompute selection highlight” (packages/engine/layer1/Seq.cpp:136-148); SeqUpdate runs SeekerUpdate then Handler->refresh (packages/engine/layer1/Seq.cpp:88-102).

12.2 Row/column data model

CSeqRow (packages/engine/layer1/Seq.h:42-57): txt (one flat char buffer for the row), col[], fill[], char2col[] (character-offset → 1-based column index), atom_lists[] (a packed, -1-terminated list of atom indices per column), name (object name), color (object color), label_flag, column_label_flag, ext_len, title_width. CSeqCol (packages/engine/layer1/Seq.h:25-37): start/stop (slice into txt), offset (aligned character column), atom_at (index into atom_lists), inverse (selected — drives the inverted-video highlight, packages/engine/layer1/Seq.cpp:465-482), unaligned, spacer, state, color (PyMOL color index), tag (alignment tag), is_abbr, hint_no_space. So the wire payload for a React sequence viewer is, per object row: &#123; object, objectColor, isLabelRow, cells: [&#123; text, offset, colorIndex|rgb, selected, spacer, unaligned, state, tag, atomIndices &#125;] &#125;, plus the label rows. No such cmd API exists today — see §14.

12.3 Display modes (seq_view_format)

SeekerUpdate (packages/engine/layer3/Seeker.cpp:969), codes read from seq_view_format (:1016), overridden to 4 for discrete objects when seq_view_discrete_by_state (:1017-1020): Menu labels (packages/engine/modules/pymol/_gui.py:379-387): Residue Codes (0), Residue Names (1), Chain Identifiers (3), Atom Names (2), States (4).

12.4 Label modes (seq_view_label_mode, global, default 2)

packages/engine/layer3/Seeker.cpp:987, :1028-1101:
  • 2 — All Residue Numbers: a dedicated label row above every object row.
  • 1 — Top Sequence Only: a single label row for the first object only.
  • 0 — Object Names Only: no label rows; the object name /name occupies the left-hand column of the sequence row itself (column_label_flag, :1079-1092).
  • 3 — No Labels: a zero-length spacer column (:1093-1101).
Label rows also carry /segi/chain/ breadcrumbs, re-emitted whenever the segment (:1147-1182) or chain (:1183-1215) changes. Residue-number labels are laid out in a third pass (packages/engine/layer3/Seeker.cpp:1820-1914): drawn every seq_view_label_spacing residues (default 5) offset by seq_view_label_start (default 1) (:1842-1843, :1858-1862), forced at sequence gaps (:1866-1867), forced if more than 2*div were skipped (:1868-1869), never twice for the same residue (:1871-1872), and suppressed if they would collide with a fixed label (:1895-1904). In atom-name mode the label is resn + ` + resi (:1878-1887).

12.5 Gaps (seq_view_gap_mode, global, default 1)

packages/engine/layer3/Seeker.cpp:1009, :1230-1258. GapMode::NONE / ALL(1) / SINGLE(2). Gaps are only inserted between atoms in the same chain, both polymer, and only when no alignment is active (:1232-1235). Gap count = resv - last_resv - 1; SINGLE clamps to 1 (:1238-1240). More than MAXCONSECUTIVEGAPS = 9 (:983) collapses to "---...---" (mode 0) / "---...--- " (mode 1) (:1275-1277, :1346-1348). Gap columns get color = seq_view_fill_color and spacer = true (:1250-1251). Menu labels: No Gaps (0), All Gaps (1), Single Gap (2) (packages/engine/modules/pymol/_gui.py:401-405).

12.6 Alignment mode

When an alignment is active (ExecutiveGetActiveAlignmentSele, packages/engine/layer3/Executive.cpp:3375, resolved from seq_view_alignment or the first enabled alignment object, :3391-3409), the second pass lines rows up by tag instead of by column index (packages/engine/layer3/Seeker.cpp:1584-1818). Tags come from SeekerFindTag (:928-967), which prefers the guide atom for residue-level modes. seq_view_unaligned_mode (0–5) decides staggering (:1590-1599) and how unaligned residues are colored (packages/engine/layer1/Seq.cpp:320-339, :423-455): modes 1/4 average the residue color with the background, 2/5 average with seq_view_unaligned_color, 3 leaves the color alone; otherwise the unaligned color is used flat. seq_view_fill_char (default -) fills alignment gaps in seq_view_fill_color (packages/engine/layer1/Seq.cpp:316, :341-346, :488-505).

12.7 Coloring

Per-column color (packages/engine/layer3/Seeker.cpp:1313-1318 and mirrored in the other code paths):
  • atom not present in the current state → seq_view_fill_color
  • seq_view_color < 0 (default -1) → SeekerFindColor (:908-926): prefer the guide atom’s color, else the last carbon’s color, else the first atom’s color
  • otherwise the explicit seq_view_color. Row label text uses seq_view_label_color (default front) (packages/engine/layer1/Seq.cpp:269-271). Background is bg_rgb, or bg_rgb_top/bg_rgb_bottom when bg_gradient and depending on seq_view_location (packages/engine/layer1/Seq.cpp:273-281).

12.8 Placement / overlay

seq_view_location — 0 = top, 1 = bottom (packages/engine/layer1/Ortho.cpp:2181, :2418). seq_view_overlay — draw over the 3D scene instead of reserving space (packages/engine/layer1/Seq.cpp:282-289, packages/engine/layer1/Ortho.cpp:2425-2437, packages/engine/layer5/main.cpp:817, :1500). The whole viewer is gated per object by the seq_view setting and the object being enabled, and _-prefixed objects are skipped when hide_underscore_names (packages/engine/layer3/Seeker.cpp:991-993). Max 50 rows (max_row = 50, packages/engine/layer3/Seeker.cpp:980, :1013).

12.9 Interaction — the full grammar

Hit test: SeqFindRowCol (packages/engine/layer1/Seq.cpp:36-86) maps (x,y) → (row, col) via char2col, accounting for NSkip (horizontal scroll) and label_flag rows (which are not clickable). During a drag the row is pinned to LastRow (packages/engine/layer1/Seq.cpp:158, :175). SeekerClick (packages/engine/layer3/Seeker.cpp:317): Selection algebra (SeekerSelectionToggle, packages/engine/layer3/Seeker.cpp:169-246; range variant :70-167):
  • The active selection name comes from ExecutiveGetActiveSeleName(create_new=true) (packages/engine/layer3/Executive.cpp:3420), auto-numbered selNN when auto_number_selections (:3436-3443).
  • sele_mode_kw = SceneGetSeleModeKeyword (packages/engine/layer1/Scene.cpp:504), one of "" \| byresi \| bychain \| bysegi \| byobject \| bymol \| bca. indexed by mouse_selection_mode (packages/engine/layer1/Scene.cpp:460-468).
  • Include: ((KW(?sele)) or KW(?tmp)); Exclude: ((KW(?sele)) and not KW(?tmp)); Fresh: KW(?tmp) (packages/engine/layer3/Seeker.cpp:130-140, :202-221).
  • Emits an equivalent cmd.select("<name>","<expr>",enable=1) line to the log (:146-149, :227-230) — this is exactly the API the React component should call.
  • auto_show_selectionsExecutiveSetObjVisib(sele, 1) (:161-162, :241-242).
Highlight sync: SeekerRefresh (packages/engine/layer3/Seeker.cpp:475-525) recomputes col->inverse for every column by testing SelectorIsMember(atInfo[at].selEntry, sele) against the active selection (falling back to a selection literally named _seeker_hilight, :484). Drag box overlay: CSeqHandler::box_active / box_row / box_start_col / box_stop_col (packages/engine/layer1/Seq.h:67-70), drawn as a line loop with spacer trimming (packages/engine/layer1/Seq.cpp:510-563). The scrollbar itself renders a mini-map of the selection: colored ticks per contiguous inverse run, one horizontal band per non-label row (packages/engine/layer1/Seq.cpp:564-696).

13. Settings that this area owns

From packages/engine/layer1/SettingInfo.h (id, name, scope, default): Movie: single_image 15 g 0 (:99) · movie_delay 16 g 30.0 (:100) · ray_trace_frames 30 g 0 (:114) · cache_frames 31 g 0 (:115) · all_states 49 object 0 (:133) · static_singletons 82 object 1 (:166) · movie_loop 299 object 1 (:388) · movie_fps 550 g 30.0 (:650) · movie_animate_by_frame 565 g 0 (:665) · movie_rock 572 g -1 (:672) · keep_alive 607 g 0 (:707) · show_frame_rate 617 g 0 (:717) · movie_panel 618 g 1 (:718) · movie_auto_store 620 object -1 (:720) · movie_auto_interpolate 621 object 1 (:721) · movie_panel_row_height 622 g 15 (:722) · movie_quality 634 g 90 (:734) · frame 194 g 1 (:279) · state 193 object 1 (:278). Also draw_frames (referenced by the menu, packages/engine/modules/pymol/_gui.py:369, and by cmd.mpng mode resolution packages/engine/modules/pymol/moving.py:392-393). Rock / sweep: sweep_angle 26 g 20.0 (:110) · sweep_speed 27 g 0.75 (:111) · rock_delay 56 g 30.0 (:140) · sweep_mode 401 g 0 (:496) · sweep_phase 402 g 0.0 (:497). Scene: animation 388 g 1 (:483) · animation_duration 389 g 0.75 (:484) · scene_animation 390 g -1 (:485) · scene_current_name 396 g "" (:491) · presentation 397 g 0 (:492) · presentation_mode 398 g 1 (:493) · scene_loop 400 g 0 (:495) · scene_restart_movie_delay 403 g 1 (:498) · mouse_restart_movie_delay 404 g 0 (:499) · scene_animation_duration 411 g 2.25 (:506) · presentation_auto_quit 415 g 1 (:510) · presentation_auto_start 417 g 1 (:512) · scene_buttons_mode 598 unused 1 (:698) · scene_buttons 599 g 1 (:699) · scene_frame_mode 623 g -1 (:723) · scenes_changed (change signal, packages/engine/layer3/MovieScene.cpp:833). Sequence viewer: seq_view 353 object 0 (:448) · seq_view_label_spacing 355 object 5 (:450) · seq_view_label_start 356 object 1 (:451) · seq_view_format 357 object 0 (:452) · seq_view_location 358 g 0 (:453) · seq_view_overlay 359 g 0 (:454) · seq_view_color 362 ostate “-1” (:457) · seq_view_label_mode 363 g 2 (:458) · seq_view_discrete_by_state 410 object 1 (:505) · seq_view_alignment 513 g "" (:613) · seq_view_unaligned_mode 514 g 0 (:614) · seq_view_unaligned_color 515 g “-1” (:615) · seq_view_fill_char 516 g ”-” (:616) · seq_view_fill_color 517 g “104” (:617) · seq_view_label_color 518 g “front” (:618) · seq_view_gap_mode 767 g 1 (:877). Related: mouse_selection_mode 354 g 1 (:449), auto_show_selections, hide_underscore_names. Display menu entries that map to these: packages/engine/modules/pymol/_gui.py:378-406.

14. What upstream does not expose, and what the bridge added

Four things this area needs have no upstream Python API. Each is now supplied by a bridge module that installs extra callables onto the cmd namespace, so the client reaches them the same way it reaches any other cmd.* symbol.
  1. No structured movie-panel data upstream. cmd.mdump only prints (packages/engine/layer1/Movie.cpp:378-403). MovieGetSpecLevel(G, frame) (packages/engine/layer1/Movie.cpp:163) is C-only; it is not in the _cmd method table (packages/engine/layer4/Cmd.cpp:6549-6562). Supplied as cmd.get_movie_panel / get_movie_status / get_movie_key_frames (packages/bridge/tenmol_bridge/panels/movie.py, EXPORTS), typed in packages/protocol/src/topics/movie_panel.ts, consumed by apps/web/src/features/movie/movieSource.ts.
  2. No structured sequence-viewer data upstream. SeekerUpdate writes only into G->Seq->Row (packages/engine/layer3/Seeker.cpp:1947). cmd.get_fastastr (packages/engine/modules/pymol/exporting.py:170) gives sequences but no colors, no per-cell atom indices, no selection state, no gaps, no alignment offsets, and it only covers polymers (:198). cmd.get_seq_align_str exists (packages/engine/layer4/Cmd.cpp:6490) but is an alignment export, not the viewer model. The §12.2 payload is rebuilt from cmd queries in packages/bridge/tenmol_bridge/panels/seqview.py (entry point tenmol_seqview), consumed by apps/web/src/features/seqview/source.ts.
  3. No scene metadata bundle upstream. Qt makes N round-trips (get_scene_list + get_scene_thumbnail per scene, scene_bin_gui.py:169-192). Supplied as cmd.get_scene_panel + cmd.get_scene_thumbnail_png (panels/movie.py), backed by the existing MovieSceneGetThumbnail / MovieSceneGetMessage (packages/engine/layer3/MovieScene.h:170-173).
  4. No event stream upstream. Everything is polled or push-from-C via OrthoDirty. The bridge publishes frame, view, settings, objects, feedback and the rest as protocol topics (packages/protocol/src/topics/), with scene changes reaching the client through scenes.ts and playback state through movie.ts.
Menus are Python data, not C. get_menudata (packages/engine/modules/pymol/_gui.py:55) and packages/engine/modules/pymol/menu.py return plain nested lists of ('command'|'menu'|'check'|'radio'|'separator', label, payload), which the port consumes nearly verbatim (compare the Qt walker at packages/engine/modules/pmg_qt/pymol_qt_gui.py:298-342 with apps/web/src/features/menubar/menuSource.ts).

15. Where each surface lives now

Paths are relative to apps/web/src/ unless stated otherwise. | Key handler | shortcut_dict.py, internal.py:447 | forward to cmd.do, with F1–F12 scene/view fallback replicated client-side | Rendering notes for the timeline and sequence viewer: both are dense 1-D grids (hundreds to tens of thousands of cells). Use a canvas/WebGL2 layer with an offscreen color-index texture for hit-testing rather than one DOM node per cell — the existing C code already thinks in terms of “column index → pixel offset” (CSeqCol::offset, ViewElemXtoFrame), so the same math ports directly.