Settings and colors
Map of PyMOL’s setting table, its change-notification machinery, and the colour/ramp system. Every claim is anchored to afile: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, )atpackages/engine/layer1/SettingInfo.h:167). - By level: 454
global, 231object-state(ostate), 44object, 27atom, 17atom-state(astate), 6bond, 18unused(+1 blank). - 779 settings are visible to Python.
SettingGetSettingIndices()(packages/engine/layer1/Setting.cpp:3227-3245) skips every record whose level iscSettingLevel_unused, so the 18 retired settings + 1 blank never appear inindex_dict. packages/engine/modules/pymol/setting.py:48then injects one legacy alias,ray_shadows -> ray_shadow, intoindex_dictaftername_listwas captured atsetting.py:44. Net effect:len(setting.get_name_list()) == 779,len(setting.index_dict) == 780.- Indices are frozen forever (they are serialized into
.psesessions) —packages/engine/layer1/SettingInfo.h:5-6“WARNING: do not delete or change indices”; regression-tested atpackages/engine/testing/tests/api/setting.py:5-8(collada_geometry_mode == 742). - The last three added settings are 795
salt_bridge_distance, 796use_tessellation_shaders, 797cell_color(packages/engine/layer1/SettingInfo.h:895-908).
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:
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-12286prints" 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-247documents the per-atom whitelist andsetting.py:148-155the per-bond whitelist as prose only. alter/alter_stateenforce hard:packages/engine/layer1/P.cpp:523-528(astate) andP.cpp:531-536(atom) raiseTypeError.
A3. Runtime introspection surface exposed to Python
C entry points registered inpackages/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) acceptstrue/false/on/off/1/0/1.0/0.0withShortcutabbreviation. - 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) → set2 → G->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 shapes — SettingGetTuple (packages/engine/layer1/Setting.cpp:1448-1479):
- boolean/int/color →
(type, int) - float →
(type, float)viapretty_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 representation — SettingGetTextPtr (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-1911 — rec.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_stereo → SceneUpdateStereo + 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 thes 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, raisesLookupErrorfor unknown). Lookup cascade: atom-state → atom → object-state/object/global.s[key] = vwrite —packages/engine/layer1/P.cpp:502-539; read-only outsidealter/alter_state(P.cpp:512-516), and level-checked as noted in A2.iter(s)—packages/engine/layer1/P.cpp:581-606→SettingUniqueGetIndicesAsPyList(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
CSettingUniquesparse map keyed byunique_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 atpackages/engine/layer3/Executive.cpp:11843, level gate atExecutive.cpp:11969). unset_deepdocstring (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
.psepersistence —packages/engine/layer1/Setting.cpp:627-660:unused-level plusantialias_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 bySettingAsPyList(Setting.cpp:956-975) andSettingFromPyList(Setting.cpp:1024). SettingInitGlobal(packages/engine/layer1/Setting.cpp, after line 1180) resets fromSettingInfo, honoursreset_gui(skipsinternal_gui,internal_gui_width), forcesvolume_mode=0in 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)withreinit_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
_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 Connectors →
label_connector - Background Color radios on
label_bg_color: None −1, Back −7, Front −6
_gui.py:543-580)
- check Ball and Stick →
stick_ball1 - 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 Cylinders →
line_as_cylinders1
_gui.py:581-612)
- Rings and Bases:
cartoon_ring_mode1/2/3/4/0 (Filled Round / Filled Flat / Filled with Border / Spheres / Base Ladders);cartoon_ring_finder1/2/3/4 (Bases and Sugars / Bases Only / Non-protein Rings / All Rings);cartoon_ring_transparency0.5 / −1 - checks: Side Chain Helper
cartoon_side_chain_helper, Round Helicescartoon_round_helices, Fancy Helicescartoon_fancy_helices, Cylindrical Helicescartoon_cylindrical_helices, Flat Sheetscartoon_flat_sheets, Fancy Sheetscartoon_fancy_sheets, Smooth Loopscartoon_smooth_loops, Discrete Colorscartoon_discrete_colors, Highlight Colorcartoon_highlight_coloron=104 off=−1 - Sampling radios
cartoon_sampling: −1 (Atom count dependent), 2, 7, 14 - Gap Cutoff radios
cartoon_gap_cutoff: 0, 5, 10, 20
_gui.py:613-627)
- checks
ribbon_side_chain_helper,ribbon_trace_atoms - radios
ribbon_as_cylinders0 (As Lines) / 1 (As Cylinders) - Cylinder Radius radios
ribbon_radius: 0. (Match Line Width), .2, .5, 1. Angstrom
_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 Proximitysurface_proximity - radios
surface_mode: Ignore None 1, Ignore HETATMs 0, Ignore Hydrogens 2, Ignore Unsurfaced 3
_gui.py:661-667)
- check Pre-integrated Rendering
volume_mode - Number of Layers radios
volume_layers: 100, 256, 500, 1000
_gui.py:668-689)
- four submenus from
transparency_menu()(_gui.py:66-71) ontransparency,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_oblique1.0
_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 Modernize →
cmd.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_coloron=74 off=−1
_gui.py:743-757)
- PDB File Loading: check
ignore_pdb_segi - mmCIF File Loading: check
cif_use_auth; check Load Assemblyassemblyon=“1” off="" (a string setting used as a toggle); check Bonding by CCDconnect_modeon=4 off=0 - Map File Loading: checks
normalize_ccp4_maps,normalize_o_maps
_gui.py:758-768)
- check Cartoon/Sticks/Spheres by Classification
auto_show_classifiedon=−1 off=0 - checks
auto_show_lines,auto_show_spheres,auto_show_nonbonded - checks
auto_show_selections,auto_hide_selections
_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”.
QLineEditplaceholder “Filter” wired toQSortFilterProxyModel.setFilterRegularExpression(lines 25-28) — the filter is a regex over both columns.populateData()(40-67):sorted(setting.get_name_list())→ 779 rows; per rowsetting._get_index(name)thencmd.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.ItemIsEnabledonly) - 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); elsecmd.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 becausecmd.setfailures are not surfaced.
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 resolutionres, pairedQLineEdit, writes on everyvalueChangedviacmd.set(self.setting, self.getDoubleValue()),objectName= setting name soupdate_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.setcalls overambient, 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 (
ambient0–1,reflect−1–1); Direct Light from Front (direct−1–1,spec_direct0–1,spec_direct_power0–100 step 1); Free placeable directed Lights (light_count1–8,edit_light1–7); Specular Reflection (spec_count−1–8,shininess0–100,spec_reflect−0.01–1,specular0–1,specular_intensity0–1); Ambient Occlusion (Surface only) (ambient_occlusion_mode0–2,ambient_occlusion_scale1–50,ambient_occlusion_smooth1–20); Ray trace only (power1–10,reflect_power1–10). - Note
spec_poweris 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-89 — palette_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.colormay be a list of names/tuples (each resolved withget_color_tuple(a, 4)so specials survive) or one of the named spectra inramp_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-54namedrampspresets2fofc, fofc, esp, rainbow, rainbow2(built with thepeak()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_expandflattens(v, colorname, a)or(v, r, g, b, a)into flat float lists (colorramping.py:265-298);ramp_to_colorsinterpolates to N colors (colorramping.py:231-263, note: it usesrange(len(ramp) / 5)— Python-2 division, this function is broken on Python 3). volume_panel(name)opens the Qt volume ramp editorpmg_qt/volume.py(colorramping.py:183-227), which itself hosts aQColorDialog(pmg_qt/volume.py:414).
C6. Chain / element / secondary-structure coloring
All inpackages/engine/modules/pymol/util.py:
_color_cycle(util.py:27-70) — 40 color indices, identical to CAutoColor(packages/engine/layer1/Color.cpp:35-76).cbc(selection, first_color=7, quiet, legacy=0)— “color by chain”, iteratescmd.get_chains, quotes chains containing spaces, uses_color_cycle[c % 40]unlesslegacy=1(thenfirst_color + c) (util.py:771-785). Aliasedcolor_chains = cbc(util.py:819).color_objs(selection)— one color per object; when a real selection is given it also callsset_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')— threecmd.colorcalls onss H,ss S,not ss S+H(util.py:432-440).- Element/carbon family:
color_carbon(427),cnc(512, coloratomicon non-carbon),cba(color, sel)(518, atomic on non-C + color on C +flags=1object color),cbh(color, sel)(526, same but hydrogens), and the fixed-carbon shortcutscbag(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) — redefineshydrogen carbon nitrogen oxygen fluorine sulfurto Jmol values, setsauto_color 0, thencolor carbon, elem C+recolor.- Deprecated:
util.rainbow(993, prints deprecation, builds a 11-stop hex list),util.ss(1019),util.color_deep(801, forwards tocmd.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 fromall_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 viautil.cbh. First entry isutil.cnc(color non-carbon only).by_chain(menu.py:464-481) —util.color_chainsoverelem C/*/CA/ all;util.chainbow;spectrum('segi','rainbow', …)forelem Cand all.by_ss(menu.py:420-426) — threeutil.cbsspresets: 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 byrep_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_colorEach submenu ends with an unset entry (cmd.unset(setting, sele)).
- molecule:
spectrum(menu.py:446-462) — rainbow overelem C,*/CA,byres;b-factors (all and*/CA);util.color_by_areamolecular/solvent.color_auto(menu.py:659-670) —color autoonelem C/ all;util.color_objsonelem 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 fromcolorramps(menu.py:620-623), which lists liveobject:rampnames cached by themenucontextsingleton (menu.py:24-46).all_colors(sele)(menu.py:638-641) binds the expression tocmd.color_deep("{0}", <sele>, 0).vol_color(menu.py:643-653) — “panel” entry + one item per name incolorramping.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
Qt —PyMOLQtGUI.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 callscmd.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) aftercolors.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 onbg_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; spacerewrites the whole table through the LUT (Color.cpp:1680) — full re-fetch required.
Constraints this area lives under
cmd.get_setting_updates()clears thechangedflags 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.- Defaults, min/max and help text are not exposed to Python anywhere
(
SettingInfoisstaticinSetting.cpp;packages/engine/data/setting_help.csvhas zero consumers). Without a new accessor or a build-time extraction, the advanced table cannot show “default” or validate ranges. get_setting_levelexists in C (packages/engine/layer4/Cmd.cpp:4403) but has no Python wrapper — must be called ascmd._cmd.get_setting_level(i), which is undocumented/private and could change.- 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. - 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”. - Float min/max declared in the table are never enforced (
Setting.cpp:1890checkstype == cSetting_intonly). Client-side validation is the only guard. - 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 runsset_coloron a generated name orspacechanges the LUT, those labels lie. The web UI should resolve label→index at render time. set_colorauto-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.- There is no event when colors change. Anything that calls
set_color,ramp_new,space,loadorreinitializedesynchronizes the client palette. colorramping.ramp_to_colorsuses Python-2 integer division (colorramping.py:243) and will raise on Python 3 — do not port it, reimplement in TS.- 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 PythonShortcutininternal.py:566); the web client should send exact names to avoid divergence. advanced_settings_guiwrites withcmd.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.SettingGenerateSideEffectscan 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 anyset.- 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
SettingInfodefault/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=0so the browser’s command log mirrors desktop PyMOL’s.pmllog. - 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.tsxispymol.menu.all_colors_list(80 tiles) andBandGrid.tsxpages through the remaining 5388 slots. They cannot be dropped —spectrum,ramp_newand theconstants_palettepalettes are expressed in those names. atomic/object/front/backare guarded onindex < 0, notindex == -1. They resolve to -4/-5/-6/-7 andcmd.get_color_tuplereturnsNonefor all of them (packages/engine/layer4/Cmd.cpp:1336); the Qt dialog tests only -1 (pymol_qt_gui.py:558) and raisesTypeErroron the others.