Skip to main content

Settings and colors

Map of PyMOL’s setting table, its change-notification machinery, and the colour/ramp system. Every claim is anchored to a file:line in packages/engine/, which is unmodified upstream. Where the port stands. Introspection and values: packages/bridge/tenmol_bridge/panels/settings.py (catalogue(), values(), drain()), typed in packages/protocol/src/topics/settings.ts, consumed by apps/web/src/features/settings/ (service.ts, SettingsPanel.tsx, AdvancedSettingsTable.tsx, SettingMenu.tsx, LightingPanel.tsx, atomSettings.ts) and stored in packages/stores/src/settings.ts. Colours and ramps: packages/bridge/tenmol_bridge/panels/colors.py and apps/web/src/features/colors/.

PART A — THE SETTINGS SYSTEM

A1. The setting table is a static C table, generated twice from one header

packages/engine/layer1/SettingInfo.h is included twice (packages/engine/layer1/Setting.h:242 for the enum, packages/engine/layer1/Setting.cpp:76-78 with SETTINGINFO_IMPLEMENTATION for the data table). Record macros — packages/engine/layer1/SettingInfo.h:15-23: Type constants are mirrored in Python at packages/engine/modules/pymol/setting.py:19-26 and defined in packages/engine/layer1/Setting.h:113-120. cSetting_tuple = -1 is a pseudo-type used only as an argument to _cmd.get_setting_of_type to request (type, value) tuples. Exact counts (computed from packages/engine/layer1/SettingInfo.h, 798 REC_* rows, indices 0..797):
  • 798 total records; index range 0 .. 797; cSetting_INIT = 798 (packages/engine/layer1/SettingInfo.h:913).
  • By type: 219 boolean, 203 int, 294 float, 15 float3, 34 color, 32 string, 1 blank (REC__( 83, ) at packages/engine/layer1/SettingInfo.h:167).
  • By level: 454 global, 231 object-state (ostate), 44 object, 27 atom, 17 atom-state (astate), 6 bond, 18 unused (+1 blank).
  • 779 settings are visible to Python. SettingGetSettingIndices() (packages/engine/layer1/Setting.cpp:3227-3245) skips every record whose level is cSettingLevel_unused, so the 18 retired settings + 1 blank never appear in index_dict.
  • packages/engine/modules/pymol/setting.py:48 then injects one legacy alias, ray_shadows -> ray_shadow, into index_dict after name_list was captured at setting.py:44. Net effect: len(setting.get_name_list()) == 779, len(setting.index_dict) == 780.
  • Indices are frozen forever (they are serialized into .pse sessions) — packages/engine/layer1/SettingInfo.h:5-6 “WARNING: do not delete or change indices”; regression-tested at packages/engine/testing/tests/api/setting.py:5-8 (collada_geometry_mode == 742).
  • The last three added settings are 795 salt_bridge_distance, 796 use_tessellation_shaders, 797 cell_color (packages/engine/layer1/SettingInfo.h:895-908).
No documentation strings live in the C table. There is a packages/engine/data/setting_help.csv (875 rows, "name","description","type","default","flag") but grep over the entire tree finds zero consumers — no Python, no C++, no CMake reference. It is dead data in this fork, but it is the only per-setting help text that exists and is the obvious source for tooltips in the web UI.

A2. Levels form a non-linear lattice

packages/engine/layer1/Setting.h:325-334 defines the enum; packages/engine/layer1/Setting.cpp:64-74 defines the mask table:
Comment at packages/engine/layer1/Setting.cpp:55-63 states the hierarchy: global < object < object-state, and object-state < atom < atom-state, object-state < bond < bond-state. Atom and bond branches are siblings, not ordered.
  • SettingLevelGetName(index) → level name string (packages/engine/layer1/Setting.cpp:83-85).
  • SettingLevelCheckMask / SettingLevelCheck (packages/engine/layer1/Setting.cpp:88-96) validate that a setting may be written at a requested level.
  • Writing at a wrong level is a warning, not an error: packages/engine/layer3/Executive.cpp:12279-12286 prints " Setting-Warning: '%s' is a %s-level setting" and still performs the write (which then silently has no effect). packages/engine/modules/pymol/setting.py:230-247 documents the per-atom whitelist and setting.py:148-155 the per-bond whitelist as prose only.
  • alter/alter_state enforce hard: packages/engine/layer1/P.cpp:523-528 (astate) and P.cpp:531-536 (atom) raise TypeError.

A3. Runtime introspection surface exposed to Python

C entry points registered in packages/engine/layer4/Cmd.cpp:6492-6496: get_setting_level is exported by C but never wrapped in packages/engine/modules/pymol/setting.py — it is reachable only as cmd._cmd.get_setting_level(i). This is the single most important gap for the web client: the level is what decides whether a setting can be edited globally, per-object, per-state or per-atom, and the Python setting module currently discards it. Python wrappers in packages/engine/modules/pymol/setting.py (re-exported into cmd at packages/engine/modules/pymol/api.py:415-432): Value coercion rules — packages/engine/modules/pymol/setting.py:83-112:
  • boolean: any float parses; non-zero → 1; otherwise boolean_dict (setting.py:50-59) accepts true/false/on/off/1/0/1.0/0.0 with Shortcut abbreviation.
  • int/float: strings that look boolean are converted first, then int()/float().
  • float3: list/tuple passthrough; string with commas goes through cmd.safe_eval (packages/engine/modules/pymol/constants.py:106-117), otherwise whitespace split; always returns 3 floats.
  • color: str(value) — resolution happens in C (SettingSet_color, packages/engine/layer1/Setting.cpp:1740-1770).
  • string: outermost matching quotes stripped.

A4. Read/write semantics

Resolution order. _SettingGetFirstDefined (packages/engine/layer1/Setting.cpp:3273-3282): set1 (most specific) → set2G->Setting (global). ExecutiveGetSettingOfType (packages/engine/layer3/Executive.cpp:7995-8021) resolves object → object CSetting, state >= 0 → object-state CSetting, and errors object "%s" not found / object "%s" lacks state %d. Return shapesSettingGetTuple (packages/engine/layer1/Setting.cpp:1448-1479):
  • boolean/int/color → (type, int)
  • float → (type, float) via pretty_f2d (7 significant digits, packages/engine/testing/tests/api/setting.py:49-55)
  • float3 → (type, (f,f,f))
  • string → (type, str)
  • blank → None
SettingGetPyObject (packages/engine/layer1/Setting.cpp:1405-1443) is different again: color settings return an RGB triple, not an index (Setting.cpp:1428-1437), and only when index > 0. Text representationSettingGetTextPtr (packages/engine/layer1/Setting.cpp:1183-1237): boolean → "on"/"off"; int → %d; float → %1.5f; float3 → "[ %1.5f, %1.5f, %1.5f ]"; color → atomic|object|front|back|default for the negative specials, else the color’s name; string → the raw string. This is exactly what cmd.get() returns and what the advanced settings table displays for float/float3/color (packages/engine/modules/pmg_qt/advanced_settings_gui.py:63-64). Min/max. Only 30 records carry min/max, and only int settings are actually clamped, and only for global writes: packages/engine/layer1/Setting.cpp:1890-1911rec.type == cSetting_int && rec.hasMinMax() && !(sele && sele[0]), printing " Setting-Warning: %s range = [%d,%d]; setting to %d.". The two float ranges present (openvr_gui_fov 0–89, openvr_gui_alpha 0–1) are declared but never enforced. The clamped int settings are: stick_quality 3–100, sphere_quality 0–4, logging 0–2, secondary_structure 1–4, cartoon_cylindrical_helices 0–2, stereo_mode 1–13, cgo_sphere_quality 0–4, pdb_reformat_names_mode 0–4, mesh_type 0–1, sphere_mode -1–11, matrix_mode -1–2, light_count 1–10, grid_mode 0–3, state_counter_mode -1–2, nb_spheres_quality 0–4, nb_spheres_use_shader 0–2, cartoon_nucleic_acid_as_cylinders 0–3, anaglyph_mode 0–4, bg_image_mode 0–3, label_connector_mode 0–4, label_relative_mode 0–2, valence_zero_mode 0–2, auto_show_classified -1–3, internal_gui_name_color_mode 0–2, openvr_gui_use_alpha 0–2, openvr_gui_use_backdrop 0–2, openvr_gui_overlay 0–2, isosurface_algorithm 0–2 (packages/engine/layer1/SettingInfo.h, rows 46, 87, 131, 157, 180, 188, 189, 326, 335, 421, 438, 455, 577, 667, 689, 690, 693, 706, 713, 721, 727, 753, 754, 768, 771, 776, 777, 787). Side effects. SettingGenerateSideEffects (packages/engine/layer1/Setting.cpp:1872-…) is a giant switch over the setting index that invalidates reps, reloads shaders, rebuilds scenes, etc. (cSetting_stereoSceneUpdateStereo + shader reload; cSetting_pick_surface → invalidate surface color; cSetting_grid_mode → reset uniforms + ExecutiveUpdateSceneMembers; …). It also warns " Setting-Warning: '%s' is no longer used" for unused-level records (packages/engine/layer1/Setting.cpp:1876-1887). Implication for the web client: after any set, geometry that three.js already holds may be stale; the bridge must emit a geometry-invalidation event, it cannot assume settings are cosmetic.

A5. Change notification — get_setting_updates is destructive and single-consumer

SettingGetUpdateList (packages/engine/layer1/Setting.cpp:1121-1147) walks all cSetting_INIT records, collects every index whose changed flag is set, and resets the flag while iterating. With a name argument it targets one object’s state-level CSetting instead of globals. The Qt GUI is the only consumer today: packages/engine/modules/pmg_qt/pymol_qt_gui.py:952-956 polls it inside update_feedback() on a 500 ms QTimer (pymol_qt_gui.py:391-393, restarted at line 958) and dispatches to self.setting_callbacks[index] (a defaultdict(list) built at pymol_qt_gui.py:111). Menu actions register themselves there at pymol_qt_gui.py:332-334 (radio) and pymol_qt_gui.py:1065-1066 (SettingAction). One hardcoded callback exists: index 440 = session_file → window title (pymol_qt_gui.py:114-116). The Tk path does the same via pmg_tk/Setting.py:145-150 (Setting.refresh() over active_dict), backed by PymolVar/ListVarItem/ColorVar two-way Tk variables (pmg_tk/Setting.py:28-100). Consequence: if the bridge polls get_setting_updates() and any other component (a plugin, the Qt window if still alive) also polls it, updates are lost — the flag is cleared by whoever reads first. The bridge must own this call exclusively.

A6. Atom-, atom-state- and bond-level settings

Accessed only through the iterate family via the s namespace object (wrapper.SettingWrapper, packages/engine/layer1/P.cpp:393):
  • s[key] read — packages/engine/layer1/P.cpp:455-486. Key may be an int index or a name string (get_and_check_setting_index, P.cpp:425-443, raises LookupError for unknown). Lookup cascade: atom-state → atom → object-state/object/global.
  • s[key] = v write — packages/engine/layer1/P.cpp:502-539; read-only outside alter/alter_state (P.cpp:512-516), and level-checked as noted in A2.
  • iter(s)packages/engine/layer1/P.cpp:581-606SettingUniqueGetIndicesAsPyList (packages/engine/layer1/Setting.cpp:3249-3268): yields the indices actually defined on that atom/atom-state. This is the only way to enumerate per-atom overrides, and it returns numbers, not names.
  • Storage is the CSettingUnique sparse map keyed by unique_id (packages/engine/layer1/Setting.h:34-50; packages/engine/layer2/AtomInfo.cpp:74-95; packages/engine/layer2/CoordSet.cpp:1704-1719).
  • Bond level is read back via cmd.get_bond (packages/engine/modules/pymol/setting.py:449-514, C at packages/engine/layer3/Executive.cpp:11843, level gate at Executive.cpp:11969).
  • unset_deep docstring (packages/engine/modules/pymol/setting.py:519-526) documents the escape hatch for atom-state settings: alter_state 1, *, del s[728] (728 = label_screen_point).

A7. Session / defaults lifecycle

  • Blacklist for .pse persistence — packages/engine/layer1/Setting.cpp:627-660: unused-level plus antialias_shader, ati_bugs, cache_max, cgo_shader_ub_*, colored_feedback, cylinder_shader_ff_workaround, defer_updates, fast_idle, fetch_path, internal_feedback, internal_gui, internal_prompt, logging, max_threads, mouse_grid, mouse_scale, nb_spheres_use_shader, no_idle, nvidia_bugs, presentation, precomputed_lighting, render_as_cylinders, security, session_changed, session_file, …. Used by SettingAsPyList (Setting.cpp:956-975) and SettingFromPyList (Setting.cpp:1024).
  • SettingInitGlobal (packages/engine/layer1/Setting.cpp, after line 1180) resets from SettingInfo, honours reset_gui (skips internal_gui, internal_gui_width), forces volume_mode=0 in open source, and applies command-line overrides (auto_show_lines/spheres/nonbonded, auto_zoom, presentation, defer_builds_mode, presentation_auto_quit, internal_feedback, stereo_mode).
  • cmd.reinitialize(what) with reinit_code (packages/engine/modules/pymol/commanding.py:350-356): everything=0, settings=1, store_defaults=2, original_settings=3, purge_defaults=4. Surfaced in the File menu (packages/engine/modules/pymol/_gui.py:128-131).
  • SettingStoreDefault / SettingPurgeDefault / SettingRestoreDefault (packages/engine/layer1/Setting.h:161-162, packages/engine/layer1/Setting.cpp:1528-1573).

PART B — THE QT SETTING UI (what becomes React)

B1. Menu data is toolkit-independent already

packages/engine/modules/pymol/_gui.py:55-58 PyMOLDesktopGUI.get_menudata(cmd) returns a pure nested list; both the Qt window (packages/engine/modules/pmg_qt/pymol_qt_gui.py:353-357) and the Tk skin (packages/engine/modules/pmg_tk/skins/normal/__init__.py:1072-1075) consume it. Item grammar (pymol_qt_gui.py:294-343): SettingAction (packages/engine/modules/pmg_qt/pymol_qt_gui.py:1041-1080) builds a checkable action for types 1,2,3,5,6 (bool, int, float, color, str) and prints TODO for float3 (type 4). It writes with cmd.set(index, value, log=1, quiet=0). Radio items write cmd.set(name, value, log=1, quiet=0) (pymol_qt_gui.py:328-330) and group by setting name via actiongroups (pymol_qt_gui.py:321-327).

B2. Full enumeration — the Setting menu (packages/engine/modules/pymol/_gui.py:492-773)

Top of menu (_gui.py:493-496):
  • Edit All…settings_edit_all_dialog (advanced table, B4)
  • Keyboard Shortcuts…shortcut_menu_edit_dialog
  • Colors…edit_colors_dialog (B10)
  • separator
Label (_gui.py:497-542)
  • Size radios on label_size: 10/14/18/24/36/48/72 Point; separator; 0.3/0.5/1/2/4 Angstrom (negated values)
  • Font radios on label_font_id: Sans 5, Sans Oblique 6, Sans Bold 7, Sans Bold Oblique 8, Serif 9, Serif Oblique 17, Serif Bold 10, Serif Bold Oblique 18, Mono 11, Mono Oblique 12, Mono Bold 13, Mono Bold Oblique 14, Gentium Roman 15, Gentium Italic 16
  • Color radios on label_color: Front −6, Back −7
  • check Show Connectorslabel_connector
  • Background Color radios on label_bg_color: None −1, Back −7, Front −6
Lines & Sticks (_gui.py:543-580)
  • check Ball and Stickstick_ball 1
  • Ball and Stick Ratio radios stick_ball_ratio: 1.0, 1.5, VDW (−1.0)
  • Zero Order Bonds radios valence_zero_mode: Hide 0, Dashed 1, Solid 2
  • Zero Order Stick Scale radios valence_zero_scale: 0.1, 0.2, 0.3, 1.0
  • Stick Radius radios stick_radius: .1, .2, .25
  • Stick Hydrogen Scale radios stick_h_scale: .4, 1.
  • Line Width radios line_width: 1.0, 1.49, 3.0
  • check Lines As Cylindersline_as_cylinders 1
Cartoon (_gui.py:581-612)
  • Rings and Bases: cartoon_ring_mode 1/2/3/4/0 (Filled Round / Filled Flat / Filled with Border / Spheres / Base Ladders); cartoon_ring_finder 1/2/3/4 (Bases and Sugars / Bases Only / Non-protein Rings / All Rings); cartoon_ring_transparency 0.5 / −1
  • checks: Side Chain Helper cartoon_side_chain_helper, Round Helices cartoon_round_helices, Fancy Helices cartoon_fancy_helices, Cylindrical Helices cartoon_cylindrical_helices, Flat Sheets cartoon_flat_sheets, Fancy Sheets cartoon_fancy_sheets, Smooth Loops cartoon_smooth_loops, Discrete Colors cartoon_discrete_colors, Highlight Color cartoon_highlight_color on=104 off=−1
  • Sampling radios cartoon_sampling: −1 (Atom count dependent), 2, 7, 14
  • Gap Cutoff radios cartoon_gap_cutoff: 0, 5, 10, 20
Ribbon (_gui.py:613-627)
  • checks ribbon_side_chain_helper, ribbon_trace_atoms
  • radios ribbon_as_cylinders 0 (As Lines) / 1 (As Cylinders)
  • Cylinder Radius radios ribbon_radius: 0. (Match Line Width), .2, .5, 1. Angstrom
Surface (_gui.py:628-660)
  • Color radios surface_color: White 0, Light Gray 4236, Gray 25, Default (Atomic) −1
  • radios surface_type: Dot 1, Wireframe 2, Solid 0
  • radios surface_cavity_mode: 1 Cavities and Pockets Only, 2 Culled, 0 Exterior (Normal)
  • Cavity Detection Radius radios surface_cavity_radius: 7 Angstrom; −3,−4,−5,−6,−8,−10,−20 Solvent Radii
  • Cavity Detection Cutoff radios surface_cavity_cutoff: −1..−5 Solvent Radii
  • check Solvent Accessible surface_solvent
  • checks Smooth Edges surface_smooth_edges, Edge Proximity surface_proximity
  • radios surface_mode: Ignore None 1, Ignore HETATMs 0, Ignore Hydrogens 2, Ignore Unsurfaced 3
Volume (_gui.py:661-667)
  • check Pre-integrated Rendering volume_mode
  • Number of Layers radios volume_layers: 100, 256, 500, 1000
Transparency (_gui.py:668-689)
  • four submenus from transparency_menu() (_gui.py:66-71) on transparency, sphere_transparency, cartoon_transparency, stick_transparency, each radios Off 0.0 / 20% / 40% / 50% / 60% / 80%
  • composite commands setting (transparency_mode, backface_cull, two_sided_lighting): Uni-Layer (2,1,0), Multi-Layer (1,0,1), Multi-Layer (Real-time OIT) (3,0,−1), Fast and Ugly (0,1,0)
  • check Angle-dependent ray_transparency_oblique 1.0
Rendering (_gui.py:690-742)
  • check OpenGL 2.0 Shaders use_shaders
  • check Antialias (Ray Tracing) antialias
  • Antialias (Real Time) radios antialias_shader: off 0, FXAA 1, SMAA 2
  • command Modernizecmd.util.modernize_rendering(1, cmd) (packages/engine/modules/pymol/util.py:553)
  • Shadows: 9 commands → cmd.util.ray_shadows(v) (packages/engine/modules/pymol/util.py:821) for none/light/medium/heavy/black, then matte/soft/occlusion/occlusion2
  • Texture radios ray_texture: None 0, Matte 1 = 1, Matte 2 = 4, Swirl 1 = 2, Swirl 2 = 3, Fiber 5
  • Interior Texture radios ray_interior_texture: same 6 values
  • Memory radios hash_max: 70 / 100 / 170 / 230 / 300
  • check Cull Backfaces backface_cull
  • check Opaque Interiors ray_interior_color on=74 off=−1
File-loading group (_gui.py:743-757)
  • PDB File Loading: check ignore_pdb_segi
  • mmCIF File Loading: check cif_use_auth; check Load Assembly assembly on=“1” off="" (a string setting used as a toggle); check Bonding by CCD connect_mode on=4 off=0
  • Map File Loading: checks normalize_ccp4_maps, normalize_o_maps
Auto-Show … (_gui.py:758-768)
  • check Cartoon/Sticks/Spheres by Classification auto_show_classified on=−1 off=0
  • checks auto_show_lines, auto_show_spheres, auto_show_nonbonded
  • checks auto_show_selections, auto_hide_selections
Tail (_gui.py:769-773)
  • check Auto-Zoom New Objects auto_zoom
  • check Auto-Remove Hydrogens auto_remove_hydrogens
  • check Show Text (Esc) text
  • check Overlay Text overlay

B3. Settings that also live in the Display menu (packages/engine/modules/pymol/_gui.py:377-491)

Not in the Setting menu but pure setting toggles, so they belong to this area’s data model: seq_view; seq_view_format (0 Residue Codes, 1 Residue Names, 3 Chain Identifiers, 2 Atom Names, 4 States); seq_view_label_mode (2/1/0/3); seq_view_gap_mode (0/1/2); internal_gui; internal_prompt; internal_feedback (0/1/3/5); overlay (0/1/3/5); stereo; opaque_background; show_alpha_checker; bg_rgb radios White 0 / Light Grey 134 / Grey 104 / Black 1; grid_mode (By Object 1, By State 2, By Object-State 3, Disable 0); orthoscopic; valence; line_smooth; depth_cue; two_sided_lighting; specular (1.0); animation; roving_detail. Plus command-only entries: stereo <mode> (_gui.py:419-433), space cmyk|pymol|rgb (_gui.py:461-465), util.performance(100|66|33|0) (_gui.py:466-471). The Mouse menu also carries mouse_selection_mode radios 0–6, and checks virtual_trackball, mouse_grid, roving_origin (_gui.py:807-832). Scene carries check scene_buttons (_gui.py:797).

B4. Advanced settings table (packages/engine/modules/pmg_qt/advanced_settings_gui.py)

PyMOLAdvancedSettings(QWidget), min size 400×500, title “PyMOL Advanced Settings”.
  • QLineEdit placeholder “Filter” wired to QSortFilterProxyModel.setFilterRegularExpression (lines 25-28) — the filter is a regex over both columns.
  • populateData() (40-67): sorted(setting.get_name_list()) → 779 rows; per row setting._get_index(name) then cmd.get_setting_tuple(index).
    • type 1 (bool) → checkable item, not text-editable
    • types 2, 6 (int, str) → str(v_list[0])
    • types 3, 4, 5 (float, float3, color) → cmd.get(index) i.e. the text form
    • name column is read-only (Qt.ItemIsEnabled only)
    • setting index stashed in value_item.setData(index)
  • formatTable() (69-80): headers hidden both axes, last column stretches, columns auto-resized.
  • itemChanged (82-99): checkbox → cmd.set(index, checked, log=1, quiet=0); else cmd.set(index, item.text(), log=1, quiet=0).
  • Known gaps to fix in the port: no level column, no default value, no min/max validation, no reset/unset action, no help text, no per-object scope, no live refresh from get_setting_updates (the table goes stale when a menu changes a setting), and edits are swallowed on error because cmd.set failures are not surfaced.
Legacy Tk equivalent for reference: packages/engine/modules/pmg_tk/SetEditor.py — 15 visible rows scrolled by a Scale widget (SetEditor.py:23,52), substring (not regex) filter with Filter/Reset buttons (SetEditor.py:74-86,127-147), Enter-to-commit with rollback on exception (SetEditor.py:109-125).

B5. Lighting Settings plugin (packages/engine/data/startup/lightingsettings_gui/main.py)

A second, curated settings panel shipped as a startup plugin (menu entry registered at packages/engine/data/startup/lightingsettings_gui/__init__.py:14-15). It is the model for “grouped slider panel” in the web UI.
  • SettingSlider(QSlider) (main.py:13-61): integer slider mapped onto a float range with resolution res, paired QLineEdit, writes on every valueChanged via cmd.set(self.setting, self.getDoubleValue()), objectName = setting name so update_setting() can find and refresh it (main.py:64-68).
  • Presets (main.py:70-139): Default, Metal, Plastic, Rubber, X-Ray — each a fixed list of cmd.set calls over ambient, direct, spec_direct, spec_direct_power, light_count, shininess, reflect, spec_count, spec_power, spec_reflect, specular, specular_intensity, ambient_occlusion_mode, ambient_occlusion_scale, ambient_occlusion_smooth, power, reflect_power.
  • Slider layout with section headers (main.py:157-188): Diffuse Reflection (ambient 0–1, reflect −1–1); Direct Light from Front (direct −1–1, spec_direct 0–1, spec_direct_power 0–100 step 1); Free placeable directed Lights (light_count 1–8, edit_light 1–7); Specular Reflection (spec_count −1–8, shininess 0–100, spec_reflect −0.01–1, specular 0–1, specular_intensity 0–1); Ambient Occlusion (Surface only) (ambient_occlusion_mode 0–2, ambient_occlusion_scale 1–50, ambient_occlusion_smooth 1–20); Ray trace only (power 1–10, reflect_power 1–10).
  • Note spec_power is commented out as deprecated since v1.5 (main.py:174).

PART C — THE COLOR SYSTEM

C1. The color table: 5388 slots, index layout is load-bearing

ColorReset (packages/engine/layer1/Color.cpp:825-1322) rebuilds CColor::Color from scratch. Registration is the reg_named_color(name,R,G,B) macro (packages/engine/layer1/Color.cpp:1017-1022) which appends to the vector and registers the name in CColor::Idx (packages/engine/layer1/Color.h:85). Counted from source: 188 explicitly named colors + 5200 generated = 5388 slots (index 0..5387). Verified layout (this arithmetic is relied upon by the menus, which hardcode indices): Spot checks that confirm the layout: grey80 = 134 and grey50 = 104 are hardcoded in the Display menu (_gui.py:456-461); gray80 = 4236 in the Surface Color menu (_gui.py:632); lightmagenta = 154, deepteal = 5262, darksalmon = 5280 in AutoColor (packages/engine/layer1/Color.cpp:35-75) and _color_cycle (packages/engine/modules/pymol/util.py:27-70). ColorGetStatus (packages/engine/layer1/Color.cpp:784-807) returns 1 for names with no digits, −1 for names containing digits, 0 for invalid. cmd.get_color_indices() (mode 1) therefore returns exactly the 178 digit-free names; cmd.get_color_indices(all=1) (mode 2) returns all 5388 (packages/engine/layer4/Cmd.cpp:1340-1381).

C2. Special / encoded color indices

packages/engine/layer1/Color.h:36-47: ColorGetIndex (packages/engine/layer1/Color.cpp:661-750) resolution order: all-digit string → literal index or special; 0x-prefixed hex → TRGB-encoded inline color (transparency packed into the top 6 bits, Color.cpp:704-712); exact word match for default/auto/current/atomic/object/front/back (Color.cpp:715-729, prefix matching was removed in 2.5); exact case-sensitive map hit; then case-insensitive prefix search over colors and ramps (Color.cpp:240-298, 736-748). ColorGetName (packages/engine/layer1/Color.cpp:759-782) inverts this, printing 0x%06x/0x%08x for TRGB indices and ramp names for index <= -10. ColorGetSpecial (Color.cpp:1789) returns tuples with a negative R component to flag special colors — this is what cmd.get_color_tuple(x, mode=4) exposes and what ramp_new uses (packages/engine/modules/pymol/creating.py:472). ColorGetEncoded (Color.cpp:1847) and Color3fToInt (Color.cpp:1883) do the 0x40RRGGBB packing used by spectrumany (packages/engine/modules/pymol/viewing.py:2053: 0x40000000 + r*0x10000 + g*0x100 + b). Front/back auto-contrast: ColorUpdateFront (Color.cpp:1754), ColorUpdateFrontFromSettings (Color.cpp:1765), ColorGetBkrdContColor (Color.cpp:81).

C3. Python color API

Color-name autocompletion/abbreviation is a Python-side Shortcut cache: _validate_color_sc builds it lazily from get_color_indices(all=1) plus special names (packages/engine/modules/pymol/internal.py:575-586), _interpret_color resolves user input (internal.py:563-573), _invalidate_color_sc clears it after set_color (viewing.py:2210) and ramp_new (creating.py:486). cmd.color_sc is initialized to None (packages/engine/modules/pymol/cmd.py:382); completion hooks are at packages/engine/modules/pymol/completing.py:96,103-104.

C4. Palettes and spectrum

packages/engine/modules/pymol/constants_palette.py:1-89palette_dict, 57 palettes, each (prefix, digits, first, last) where prefix ∈ o|s|r|c|w selects one of the generated 1000-color bands and first/last are indices within it. Examples: rainbow = ('o',3,107,893), rainbow_cycle = ('o',3,0,999), rainbow2 = ('s',3,167,833), gcbmry = ('r',3,166,999), blue_white_red = ('w',3,83,167), blue_red = ('c',3,83,167). palette_sc = Shortcut(palette_dict.keys()) at packages/engine/modules/pymol/constants.py:223. cmd.spectrum (viewing.py:2065-2151): resolves the palette through palette_sc; falls back to spectrumany when the expression is not purely alphabetic or the palette is not a known name (viewing.py:2137-2140); otherwise calls _cmd.spectrum(selection, expression, min, max, first, last, prefix, digits, byres, quiet). minimum=0, maximum=-1 signals auto-ranging (viewing.py:2143-2145). Returns (min, max). cmd.spectrumany (viewing.py:1978-2063): arbitrary color lists, interpolation in rgb (default), hls, or hsv (_spectrumany_interpolations, viewing.py:1968-1973); expression aliases pc→partial_charge, fc→formal_charge, resi→resv (viewing.py:2016-2017); non-numeric values are enumerated (viewing.py:2027-2031); writes packed TRGB colors via cmd.alter(... 'color = next_color() or color') then recolor. palette_colors_dict (viewing.py:39-50) maps the 10 rainbow-family palette names to explicit color-name strings for this path.

C5. Color ramps (ramp objects) and volume ramps

  • Ramps are real objects (object:ramp) registered as color “extensions”: ColorRegisterExt (packages/engine/layer1/Color.cpp:347-365), ColorForgetExt (Color.cpp:367), ColorRenameExt (Color.cpp:1894). Their color index is -10 - ext_slot.
  • ColorCheckRamped (Color.cpp:168-171), ColorGetRamp (Color.cpp:173-187), ColorGetRamped (Color.cpp:189-218) — a ramped color is evaluated per vertex (vertex, state), which is why ramp-colored geometry must be baked server-side before it reaches three.js.
  • cmd.ramp_new(name, map_name, range, color, state, selection, beyond, within, sigma, zero, quiet)packages/engine/modules/pymol/creating.py:374-490. color may be a list of names/tuples (each resolved with get_color_tuple(a, 4) so specials survive) or one of the named spectra in ramp_spectrum_dict (creating.py:47-57): traditional 1, sludge 2, ocean 3, hot 4, grayable 5, rainbow 6, afmhot 7, grayscale 8, object [[-1,-1,-1]].
  • cmd.ramp_update(name, range, color)ramp_new(name, '', …) (creating.py:492-518).
  • Volume ramps are separate: packages/engine/modules/pymol/colorramping.py:17-54 namedramps presets 2fofc, fofc, esp, rainbow, rainbow2 (built with the peak() helper, colorramping.py:14-15); volume_ramp_new(name, ramp) registers more (colorramping.py:56-84); volume_color(name, ramp, state) sets/gets (colorramping.py:123-181, C: _cmd.set_volume_ramp / _cmd.get_volume_ramp); ramp_expand flattens (v, colorname, a) or (v, r, g, b, a) into flat float lists (colorramping.py:265-298); ramp_to_colors interpolates to N colors (colorramping.py:231-263, note: it uses range(len(ramp) / 5) — Python-2 division, this function is broken on Python 3).
  • volume_panel(name) opens the Qt volume ramp editor pmg_qt/volume.py (colorramping.py:183-227), which itself hosts a QColorDialog (pmg_qt/volume.py:414).

C6. Chain / element / secondary-structure coloring

All in packages/engine/modules/pymol/util.py:
  • _color_cycle (util.py:27-70) — 40 color indices, identical to C AutoColor (packages/engine/layer1/Color.cpp:35-76).
  • cbc(selection, first_color=7, quiet, legacy=0) — “color by chain”, iterates cmd.get_chains, quotes chains containing spaces, uses _color_cycle[c % 40] unless legacy=1 (then first_color + c) (util.py:771-785). Aliased color_chains = cbc (util.py:819).
  • color_objs(selection) — one color per object; when a real selection is given it also calls set_object_color (util.py:787-799).
  • chainbow(selection, palette='rainbow') — per model, per chain, spectrum('count', palette, …, byres=1) (util.py:809-817).
  • cbss(selection, helix_color='red', sheet_color='yellow', loop_color='green') — three cmd.color calls on ss H, ss S, not ss S+H (util.py:432-440).
  • Element/carbon family: color_carbon (427), cnc (512, color atomic on non-carbon), cba(color, sel) (518, atomic on non-C + color on C + flags=1 object color), cbh(color, sel) (526, same but hydrogens), and the fixed-carbon shortcuts cbag (green, 442), cbac (cyan, 449), cbam (lightmagenta, 456), cbay (yellow, 463), cbas (salmon, 470), cbaw (white/hydrogen, 477), cbab (slate, 484), cbao (brightorange, 491), cbap (purple, 498), cbak (pink, 505).
  • color_by_area(sele, mode='molecular', state=0, palette='rainbow') (util.py:80-119).
  • util.colors('jmol') (util.py:1029-1040) — redefines hydrogen carbon nitrogen oxygen fluorine sulfur to Jmol values, sets auto_color 0, then color carbon, elem C + recolor.
  • Deprecated: util.rainbow (993, prints deprecation, builds a 11-stop hex list), util.ss (1019), util.color_deep (801, forwards to cmd.color_deep).

C7. The internal-GUI “C” (color) menu — packages/engine/modules/pymol/menu.py

Menu items are [type, label, action] with type 1 = item/submenu, 2 = title, 0 = separator. Labels embed inline color escapes \RGB with one digit per channel (e.g. \900 = red).
  • mol_color(sele) (menu.py:672-686) — root: by element, by chain, by ss, by rep, spectrum, separator, auto, separator, then the full color palette from all_colors.
  • by_elem (menu.py:400-418) + by_elem2..by_elem6 (335-399) — 8 carbon-color choices per page, 6 pages; page 6 colors hydrogens via util.cbh. First entry is util.cnc (color non-carbon only).
  • by_chain (menu.py:464-481) — util.color_chains over elem C / */CA / all; util.chainbow; spectrum('segi','rainbow', …) for elem C and all.
  • by_ss (menu.py:420-426) — three util.cbss presets: red/yellow/green, cyan/magenta/salmon, cyan/red/magenta.
  • by_rep / by_rep_sub (menu.py:428-444) — writes the per-rep color settings rather than atom colors, driven by rep_setting_lists (menu.py:482-517):
    • molecule: line_color, stick_color, ribbon_color, cartoon_color, label_color, dot_color, sphere_color, mesh_color, surface_color
    • measurement: dash_color, angle_color, dihedral_color, label_color
    • extra: cartoon_highlight_color, cartoon_ladder_color, cartoon_nucleic_acid_color, cartoon_ring_color, ellipsoid_color, label_outline_color, ray_interior_color, ray_trace_color, stick_ball_color Each submenu ends with an unset entry (cmd.unset(setting, sele)).
  • spectrum (menu.py:446-462) — rainbow over elem C, */CA, byres; b-factors (all and */CA); util.color_by_area molecular/solvent.
  • color_auto (menu.py:659-670) — color auto on elem C / all; util.color_objs on elem C / all.
  • all_colors_list (menu.py:519-618) — the curated swatch palette, 9 groups: reds (11), greens (10), blues (9), yellows (7), magentas (10), cyans (7), oranges (7), tints (8), grays (11) — each entry a (3-digit swatch, color name) pair.
  • all_colors_generic(expr) (menu.py:625-636) renders those groups and appends a ramps submenu built from colorramps (menu.py:620-623), which lists live object:ramp names cached by the menucontext singleton (menu.py:24-46).
  • all_colors(sele) (menu.py:638-641) binds the expression to cmd.color_deep("{0}", <sele>, 0).
  • vol_color (menu.py:643-653) — “panel” entry + one item per name in colorramping.namedramps.
  • slice_color (menu.py:655-657), mesh_color (menu.py:696-709, adds negative-color submenu driving <rep>_negative_visible / <rep>_negative_color), measurement_color (687-694), general_color (711-712).

C8. Color editor dialogs

QtPyMOLQtGUI.edit_colors_dialog (packages/engine/modules/pmg_qt/pymol_qt_gui.py:547-611) using packages/engine/modules/pmg_qt/forms/colors.ui. Widgets: list_colors (QListWidget, sorting enabled, populated from cmd.get_color_indices() — so only the 178 digit-free colors), frame_color (live swatch via stylesheet), input_name (QLineEdit), input_R/G/B (QDoubleSpinBox, max 1.0, step 0.01), slider_R/G/B (QSlider 0–100), button_apply. Wiring: list selection → name field → load_color() which does get_color_index then get_color_tuple; spinbox↔slider two-way with a re-entrancy lock; Apply runs cmd.do('set_color %s, [%.2f, %.2f, %.2f]\nrecolor') and inserts new names into the list. Tk (legacy)packages/engine/modules/pmg_tk/ColorEditor.py: ColorEditor (81-138) is a Pmw.SelectionDialog (500×400) with buttons New/Edit/Done over cmd.get_color_indices(); ColorEdit (60-79) opens the native tkColorChooser and emits set_color + recolor; NewColor (25-58) prompts for a name first.

C9. Color LUT / color space

cmd.space(space, gamma) (packages/engine/modules/pymol/importing.py:227-288) maps through constants.space_dict (packages/engine/modules/pymol/constants.py:141-148): cmyk → $PYMOL_DATA/pymol/cmyk.png, pymol → 'pymol', rgb → 'rgb', greyscale → 'greyscale', then _cmd.load_color_table(filename, gamma, quiet)ColorTableLoad (packages/engine/layer1/Color.cpp:1324-…). 'rgb' purges the table; 'greyscale' and 'pymol' synthesize a 512×512 LUT (the pymol space is parameterized by settings pymol_space_max_red/green/blue and pymol_space_min_factor, Color.cpp:1404-1408); anything else is loaded as a PNG. ColorUpdateFromLut (Color.cpp:1680) and ColorLookupColor (Color.cpp:1721) apply it. ColorRec::LutColor/LutColorFlag (packages/engine/layer1/Color.h:58-59) hold the mapped value, so get_color_tuple returns the LUT-mapped color, not the raw definition — the web client must re-fetch the whole palette after space.

PART D — REACT / BRIDGE MODEL

D1. One introspection RPC drives both UIs

The curated menu and the advanced table must be fed by the same catalogue. Nothing in the current Python layer returns level/default/min/max, so the bridge has to assemble it:
get_setting_level returns the level name ("global", "object", "object-state", "atom", "atom-state", "bond", "bond-state"). Defaults, min/max and help text are not reachable from Python at all — SettingInfo is static in Setting.cpp and nothing exports it. Three ways out were considered: a new C accessor beside CmdGetSettingLevel (packages/engine/layer4/Cmd.cpp:4403); snapshotting defaults by calling cmd.reinitialize('original_settings') in a scratch instance; or reading the header that generates the table. The third shipped: packages/bridge/tenmol_bridge/panels/settings.py parses packages/engine/layer1/SettingInfo.h for defaults and min/max and packages/engine/data/setting_help.csv for help text, and reports defaultsSource: null when neither is readable rather than inventing values. No C accessor was added. Wire schema (bridge → client, sent once, cached in a React context):

D2. Value RPCs

D3. Change propagation

Exactly one component calls cmd.get_setting_updates() (setting.py:440): the bridge’s status thread (packages/bridge/tenmol_bridge/pump.py). Because the read clears the flags, a second caller would silently steal updates. panels/settings.py::install therefore does not call it — it wraps it with a pass-through that records what the status thread received, and the client reads that recording through a cursor. Two delivery channels, one cursor: the settings topic pushes within one status interval, and the 5 Hz poll is an idempotent backstop that cannot lose an index the push missed. This mirrors the Qt design (pymol_qt_gui.py:952-956) without a second drain. The no-argument call is the only one recorded. get_setting_updates(name, state) targets one object’s state-level CSetting — a different channel the status thread does not poll. Colours have no change feed at all: set_color, ramp_new, space and session loads mutate the palette silently. The palette is therefore re-fetched after any of set_color \| ramp_new \| ramp_update \| space \| load \| reinitialize \| set_session, mirroring the _invalidate_color_sc invalidation points (internal.py:584, viewing.py:2210, creating.py:486).

D4. Component inventory

D5. Client-side color evaluation

The three.js layer needs an RGB for every color index it is handed. Ship the whole 5388-entry table once (name → index → rgb) after colors.list(all=1) + batched get_color_tuple, ~5388 × 3 floats ≈ 65 KB — cheap. But:
  • specials −1..−7 must be resolved by the backend, not the client (ColorGetSpecial, Color.cpp:1789; front/back depend on bg_rgb, Color.cpp:1754-1788);
  • 0x40RRGGBB-encoded colors decode client-side (Color.cpp:1847, viewing.py:2053);
  • ramp colors (index <= -10) are position-dependent (ColorGetRamped, Color.cpp:189) and must be baked into vertex colors server-side;
  • space rewrites the whole table through the LUT (Color.cpp:1680) — full re-fetch required.

Constraints this area lives under

  1. cmd.get_setting_updates() clears the changed flags as it reads (packages/engine/layer1/Setting.cpp:1128-1132). Two pollers = lost updates. The bridge must be the only caller, and any surviving Qt window must be disabled.
  2. Defaults, min/max and help text are not exposed to Python anywhere (SettingInfo is static in Setting.cpp; packages/engine/data/setting_help.csv has zero consumers). Without a new accessor or a build-time extraction, the advanced table cannot show “default” or validate ranges.
  3. get_setting_level exists in C (packages/engine/layer4/Cmd.cpp:4403) but has no Python wrapper — must be called as cmd._cmd.get_setting_level(i), which is undocumented/private and could change.
  4. Setting indices are session-file ABI (packages/engine/layer1/SettingInfo.h:5-6). Any cached client-side catalogue keyed by index is safe across versions only if new settings are appended, never renumbered.
  5. Wrong-level writes are silently accepted with only a console warning (packages/engine/layer3/Executive.cpp:12279-12286). The web UI must gate by level client-side or users will see “it saved but nothing happened”.
  6. Float min/max declared in the table are never enforced (Setting.cpp:1890 checks type == cSetting_int only). Client-side validation is the only guard.
  7. Menu items hardcode color indices (134, 104, 4236, 74, 104…) computed from the generated color-table layout (_gui.py:456-461,632,741). If a user runs set_color on a generated name or space changes the LUT, those labels lie. The web UI should resolve label→index at render time.
  8. set_color auto-detects the 0–1 vs 0–255 range by “any component > 1.0” (viewing.py:2205-2207). A legitimate [1, 0, 0] in 0–255 space becomes red-in-0–1. Web pickers must always send 0–1 floats.
  9. There is no event when colors change. Anything that calls set_color, ramp_new, space, load or reinitialize desynchronizes the client palette.
  10. colorramping.ramp_to_colors uses Python-2 integer division (colorramping.py:243) and will raise on Python 3 — do not port it, reimplement in TS.
  11. Color name lookup falls back to case-insensitive prefix matching in C (packages/engine/layer1/Color.cpp:736-748), which the source itself questions (“TODO does this even make sense?”). Two different lookup policies exist (C prefix match vs Python Shortcut in internal.py:566); the web client should send exact names to avoid divergence.
  12. advanced_settings_gui writes with cmd.set(index, text); a bad value raises inside PyMOL and the table keeps showing the new text. Any port must round-trip the value back from the backend.
  13. SettingGenerateSideEffects can invalidate reps and force geometry rebuilds (packages/engine/layer1/Setting.cpp:1872+). Setting changes are not cosmetic — the three.js scene may need a fresh geometry payload after almost any set.
  14. 179 settings are atom/atom-state/bond/bond-state/object-level and are invisible to the current advanced table, which only reads globals. Feature parity with the desktop C menu (menu.rep_setting_lists) requires per-object/per-selection scope.

Decisions this map fed

  • No C accessor for SettingInfo default/min/max. The header is parsed instead (packages/bridge/tenmol_bridge/panels/settings.py); see D1.
  • packages/engine/data/setting_help.csv (875 rows, zero upstream consumers) is the tooltip source. Row count does not equal setting count, so entries are matched by name and a missing row yields no tooltip rather than a wrong one.
  • Menu writes keep log=1, quiet=0 so the browser’s command log mirrors desktop PyMOL’s .pml log.
  • Per-object and per-selection scope is offered wherever the level allows it (apps/web/src/features/settings/atomSettings.ts), because 179 settings are atom/atom-state/bond/bond-state/object level and the Qt advanced table only ever read globals (constraint 14).
  • The generated bands (s000…, r, c, w, o) sit behind an “advanced” toggle, not in the default palette: apps/web/src/features/colors/SwatchGrid.tsx is pymol.menu.all_colors_list (80 tiles) and BandGrid.tsx pages through the remaining 5388 slots. They cannot be dropped — spectrum, ramp_new and the constants_palette palettes are expressed in those names.
  • atomic/object/front/back are guarded on index < 0, not index == -1. They resolve to -4/-5/-6/-7 and cmd.get_color_tuple returns None for all of them (packages/engine/layer4/Cmd.cpp:1336); the Qt dialog tests only -1 (pymol_qt_gui.py:558) and raises TypeError on the others.