Skip to main content

Engine + renderer backlog — “what’s left”

A prioritized, categorized inventory of everything the in-browser TypeScript port of PyMOL (@tenmol/engine-ts + @tenmol/viewport) does not do yet: unported commands, missing and degraded rendering, deliberately deferred work, and performance/architecture levers. This is the forward-looking companion to two existing docs:
  • docs/engine-port-gaps.md — narrative of known gaps.
  • docs/feature-parity.md — parity status.

How this was produced

Compiled from a 6-way parallel audit of the codebase (Aug 2026): six agents each swept a different slice — not-ported commands, missing reps, rendering-quality gaps, in-code TODO/deferral markers, plan/docs deferrals, and perf/architecture — producing 62 findings that were then deduped and ranked by user-visible impact. Every claim was verified against the source, not inferred.

How to read it

  • Effort tagsS small/localized · M moderate · L large / new subsystem.
  • Ranking — items are ordered by user-visible impact within each section (highest first).
  • Parity metric — where a percentage is quoted, it is the TS render compared pixel-for-pixel (pixelmatch) against a real-PyMOL reference PNG. Current mean ≈ 92.7% across 22 scenes (up from 89.4% after the SES surface shift); solid/line/cartoon reps sit at 90–99%, surfaces at 84–92%, mesh at 74% (worst). See the packages/e2e docs page / apps/web/e2e/visual.e2e.mjs.

Where recent work landed (context)

Backlog-closure effort (Waves 0–2, rolling PR). A KPI-driven push to close this list is underway; see docs/parity-dashboard.md for the live command/rep burndown. Landed so far — the §1 items they close are marked ✅ ported inline below:
  • Wave 0 — a committed command/rep coverage scoreboard + ratchet (packages/engine-ts/test/coverage.test.ts, pnpm coverage). Baseline: 271/506 real command handlers.
  • Wave 1 — the preset.* (20/21), util.* remainder (+22), movie.* (20/20), and gui.* (7/7) namespaces, plus flat check / fork / dist / help verbs and British-spelling colour aliases — composed via a new ctx.call(...) seam. Coverage rose to 350/506 (69.2%).
  • Wave 2 — the console parser now handles key=value kwargs, quoted commas/=, and reports @script honestly.
  • Parity wave (2026-08) — a red parity suite (packages/engine-ts/test/parity-*.test.ts) pinned every remaining gap and closed 53 of 64 achievable targets: structure-based superposition, selection extras (byring/bycalpha/pepseq/altloc/fc./pc.), measurement objects + labels + set_name, map/ramp/isosurface gadgets, mol/sdf/mol2 export + multisave + get_session, bond orders + ANISOU + object matrix + groups + P 2 symmetry, edit/remove_picked/fab/ torsion/uniquify, and the util.* charge/area helpers (incl. Amber-99 protein_assign_charges_and_radii). Command coverage 375→392/506. The deferred 11 (nucleic builders, valence-chemistry edits, combine_fragment, byte-exact color_by_area) stay pinned as it.fails; the individual §1 entries below are marked accordingly.
The visual/perf regression suite and three fixes already shipped and set the baseline the numbers above reflect:
  • F1 — engine memoizes each rep’s built frame by a content hash, so show/ hide and no-op publishes no longer rebuild unchanged reps.
  • F3 — the 4 Hz idle camera poll no longer forces a full repaint when the view is unchanged.
  • F5 — the surface mesh gets 3 Taubin smoothing passes for an SES-like look.
  • F4 (ambient occlusion) and F2 (viewport pooling) were deliberately not done — see §4 for why.

1. Not supported / not ported commands

What a user hits as “not ported by @tenmol/engine-ts yet” (a NotPorted throw) — or, worse, a call that silently does nothing. Grouped by namespace.

editor.* namespace — 🟡 residue growth ported (3/23) — L · was the reported bug

cmd/editor.ts now ports the headline build path: editor.attach_amino_acid (the exact cmd.editor.attach_amino_acid that prompted this list), plus editor.attach_fragment and editor.build_peptide. It grows a chemically-valid residue — a trans peptide bond (omega ≈ 180°, C–N ≈ 1.33 Å, ideal backbone angles, no clashes) by superposing the real chempy fragment geometry (model/aa-fragments.json, all 20 amino acids, extracted by scripts/extract-fragments.py) with a 3-point rigid fit (model/superpose.ts). The low-level topology half of editor.py (add_bond, attach, h_add, fuse, invert, protonate, replace, rebond, fix_chemistry, sort) remains in cmd/builder.ts. Idealisation vs PyMOL: phi/psi are not applied (the residue comes out extended), and native fuse/set_dihedral are replaced by the direct rigid-fit construction. Now also real (flat verbs, cmd/nucleic.ts + cmd/editor.ts + cmd/builder.ts): fab, combine_fragment, fnab, attach_nuc_acid, extend_nuc_acid, bond_single_stranded, bond_double_stranded, and the valence edits protonate/replace/cycle_valence/set_geometry/invert (bond-order perception from model/residue-bonds.json).
Still missing: fit_sugars, fit_DS_fragment, attach_O5_phosphate, add2pO, move_atom_in_res, move_new_res, rename_three_to_one, iterate_to_list, check_dummy_oriention, check_DNA_base_pair, check_valid_attachment, get_chains_oppo, get_new_chain
Files: cmd/editor.ts, model/{aa-fragments,superpose}.ts, scripts/extract-fragments.py, ref packages/engine/modules/pymol/editor.py.

File readers — ✅ core formats real (Wave 3); export partial — L

load is real (cmd/fileio.ts): it parses structured content for PDB, CIF/ mmCIF, MOL/SDF, MOL2 and XYZ — by an explicit format or by sniffing — and read_cifstr/read_mol2str/read_xyzstr join the existing read_pdbstr/ read_molstr/read_sdfstr. get_str now serializes pdb/fasta/xyz.
Still missing: reading from a path/URL or fetch-from-PDB (the sync browser engine has no filesystem/async network — belongs to the web-app/worker layer), MMTF/mmCIF maps + trajectories (load_traj, load_mtz), and session/ mesh export (get_session, multisave, STL/OBJ/glTF/POV). save (disk write) stays a no-op — the web app downloads the get_str output instead.
Files: cmd/fileio.ts, model/{cif,mol2,xyz,bonding}.ts, ref docs/engine-port-gaps.md.

Large batch of documented no-op verbs (callable, do nothing) — L

Return-placeholder stubs that mislead scripts because they neither throw nor act:
logging (log, log_open/close, resume) · picking (edit, edit_keys, drag, release, unpick) · movie edits (mcopy, mdelete, mmove, mdo, minsert, scene_order) · maps/volumes (map_set, slice_new, volume, volume_panel, spheroid, vdw_fit) · app/render (cls, cache, callout, capture, quit, meter_reset, focal_blur, decline, feedback, extend, alias, matrix_copy) · chemistry/typing (assign_stereo, text_type, set_geometry, uniquify, unset_deep, pbc_wrap/unwrap) · export (get_mtl_obj, get_povray, povray, remove_picked)
Files: cmd/extras.ts:505.

preset.* namespace — ✅ ported (Wave 1, 20/21) — M

All one-click representation presets are ported in cmd/preset.ts, composing show/hide/color/set/spectrum/util.* via ctx.call. Some steps that depend on still-unported sub-verbs (flag, set_bond, the extend selector operator, the licorice rep alias) are skipped and will fill in as those land.
Ported: simple, simple_no_solv, technical, pretty, pretty_solv, pretty_no_solv, publication, pub_solv, pub_no_solv, default, ligands, ligand_cartoon, ligand_sites (+_hq/_trans/_trans_hq/_mesh/_dots), ball_and_stick, b_factor_putty, interface, classified
Files: cmd/preset.ts, registrars.ts, ref preset.py.

util.* — ✅ largely ported (Wave 1, 30/45) — M

Beyond the carbon/rainbow colorers in cmd/coloring.ts (cbc, rainbow, cbag, cbac, cbay, cbas, cbap, cbaw), cmd/util2.ts now ports the coloring, analysis and labeling helpers: cnc, color_carbon, cbss, ss, chainbow, cba, cbh, cbab, cbao, cbak, cbam, color_objs, color_deep, get_area, get_sasa, compute_mass, find_surface_residues, find_surface_atoms, phipsi, mass_align, label_chains, label_segments.
Still missing (need machinery the port lacks — charges, ESP maps, shader/render settings): protein_vacuum_esp, protein_assign_charges_and_radii, color_by_area, get_sasa_relative, sum_formal/partial_charges, ray_shadows, b2vdw, ff_copy, colors, interchain_distances, ligand_zoom, enable_all_shaders, modernize_rendering, performance
Files: cmd/util2.ts, cmd/coloring.ts, ref util.py.

movie.* namespace — ✅ ported (Wave 1, 20/20) — M

cmd/movie3.ts ports the whole movie.* namespace, composing the top-level mview/mset/mplay/mdo verbs. The frame-table / camera-keyframe side effects are faithful; per-frame motion via mdo and file encoding (produce, find_exe) are in-engine no-ops (no ffmpeg / filesystem), noted in the module.
Ported: produce, roll, tdroll, timed_roll, rock, nutate, screw, sweep, zoom, pause, load, add_blank, add_roll, add_rock, add_state_sweep, add_state_loop, add_nutate, add_scenes, get_movie_fps, find_exe
Files: cmd/movie3.ts, ref movie.py, _gui.py:236.

Structure-based superposition — ✅ ported (parity wave) — M

super is now a real structure-based superposition (pairs guide Cα by position, Kabsch-fits, carries the whole mobile object — no sequence similarity needed); cealign(target, mobile) returns {alignment_length, RMSD, rotation_matrix}; usalign returns {tm_score_target, tm_score_mobile, RMSD, alignment_length, seq_identity}; pair_fit fits explicit atom-pair selections; alignto/extra_fit superpose every other/listed object onto the reference; and fit/rms/rms_cur now pair atoms by identity (matchmaker=0), with rms the fitted RMS and intra_rms fitting each state first. Pinned by parity-superposition.test.ts (10/10). Files: cmd/align.ts, cmd/extras.ts.

fab + nucleic builders ported; h_fix still a stub — M

cmd.fab(seq, name) builds a peptide from a one-letter sequence (editor.build_peptide), and the nucleic-acid builders are now real (cmd/nucleic.ts): fnab/attach_nuc_acid/extend_nuc_acid grow a strand (and, with dbl_helix, the antiparallel complement) from the vendored monomer fragments (model/nucleic-fragments.jsonatpB/ttpB/gtpB/ctpB/utpA), phosphodiester-linking O3’(i)→P(i+1). combine_fragment fuses a fragment into an object. Only h_fix remains a documented no-op. Files: cmd/nucleic.ts, cmd/editor.ts, cmd/extras.ts.

Top-level verbs that throw NotPorted — M · partly landed (Wave 1)

cmd/topics.ts now ports check (a real atom/bond structure summary), fork (→spawn), dist (→distance), and the flat help verbs re-exported by api.py (commands, show_help, help_setting, editing_ring), plus British-spelling colour aliases (colour, bg_colour, recolour, set_colour). Wave 4a (cmd/xform.ts, cmd/misc2.ts) additionally ports transform_object/transform_selection, set_state_order, get_coordset, load_coordset, set_frame, set_discrete, label2, get_phipsi, and the get/set/del_colorection set.
Still throwing: torsion (interactive torsion edit) and functional verbs left for later waves (map_generate, auto_measure, copy_image, set_object_ttt, …). Python-language keywords and disabled vendored verbs (slice_lock, rgbfunction) stay intentionally absent.
Files: cmd/topics.ts, ref keywords.py, engine.ts.

gui.* namespace — ✅ ported (Wave 1, 7/7) — S

cmd/guins.ts registers the whole gui.* namespace. The verbs are external-GUI window controls; the browser has no such window, so most are faithfully modelled as env-bound (ext_show/ext_hide track visibility intent; accessors return null exactly as PyMOL’s ImportError branch does). Files: cmd/guins.ts, packages/client/src/cmd.ts:286, ref gui.py.

2. Rendering — missing representations

Reps with no builder at all: the command may run and mutate state, but nothing is ever drawn.

Volume rep never rendered — L

Rep.Volume has no builder and volume / volume_panel are no-ops. isomesh / isosurface / isodot run marching cubes and publish state but never emit a Mode-G geometry frame — map surfaces are computed and then dropped. Direct 3D-texture ray-march is entirely absent, so any density workflow shows nothing. Files: geometry/registry.ts:43-61, cmd/maps.ts:298-343, cmd/extras.ts:517.

CGO rep never rendered — L

Rep.CGO has no builder and there is no load_cgo / CGO object model. User compiled-graphics objects (cgo.* primitives, pseudoatom CGO, ramp/gradient CGOs) can’t be created or drawn. Files: geometry/registry.ts:43-61.

Slice rep not implemented — M

Rep.Slice has no builder and slice_new is a no-op; no planar map-slice quad is generated. Files: geometry/registry.ts, cmd/extras.ts:518.

Callback rep not implemented — S

Rep.Callback (axes / gadget / internal draw callbacks) has no builder or object path. Niche, but absent. Files: geometry/registry.ts:43-61.

3. Rendering — quality gaps

Reps that do render but don’t yet match desktop PyMOL. Mean parity ≈ 92.7%; surfaces 84–92%, mesh 74% (worst), everything else 90–99%.

Surface — 🟡 SES-approximated (shift toward vdW); true probe-rolling still absent — M

The raw field min(|p−c|−(vdw+probe)) is the solvent-accessible surface (puffy). It is now shifted inward by ~0.9·probe toward the vdW surface — a cheap solvent-excluded approximation that removed most of the puffiness and lifted the surface/mesh scenes sharply: 3al1-surface 75.1→87.2%, bfactor 68.5→83.7%, helix 72.2→88.4%, pept-surface 71.7→91.7%, pept-mesh 64.6→74.0% (mean 89.4→ 92.7%). Still not modelled: true probe-rolling of re-entrant toroidal saddles, surface_type dot/mesh modes, surface_cavity_mode, per-atom surface flags; the faceting-vs-resolution lever was tried (finer grid + spatial index) and found NOT to matter — the gap was shape (SES), not tessellation. Files: geometry/surface_gen.ts (SES_SHRINK), geometry/surface.ts.

Cartoon heavily simplified — L

Splines through CA only with a parallel-transport frame (no carbonyl / peptide-plane orientation), so the flat ribbon can rotate about the backbone and present its edge. Helix and strand share one hard-coded rectangular cross-section (RIBBON_HALF_WIDTH=0.9, HALF_THICKNESS=0.2); only strands taper to a simple arrow. No fancy/dumbbell helices, flat sheets, nucleic-acid cartoon (ring/ladder/base), or subtype variants (tube, putty/b-factor, oval, skip). Ignores cartoon_* width settings. Files: geometry/cartoon.ts:32-349.

Transparency is plain sorted alpha, not OIT — L

three.js default alpha blend + per-object painter sort with DoubleSide, so a single transparent surface blends its own front/back in raster order. No depth peeling / weighted-blended OIT / transparency_mode → wrong front-back ordering and popping as the camera turns on any transparent surface or sphere. (Unmeasured — no transparent scene in the suite yet — but real.) Files: viewport/modeG/materials/vertex.ts:48, lighting.ts:24.

Mesh — the worst-scoring scene (64.6%) — M

Reuses the same approximate SAS generator but does not smooth (surface gets 3 Taubin passes; mesh doesn’t) and emits every deduplicated marching-cubes edge as a line. Ignores mesh_width / mesh_quality / mesh_type, so the coarse faceted edges look little like PyMOL’s finer tessellation. Files: geometry/mesh.ts:28.

Sticks — no bond-valence rendering — M

One split-colour cylinder2 per bond regardless of order (no double/triple twin cylinders when valence is on). Ignores every stick setting except stick_radius; isolated sticks atoms get no stub, so they’re invisible. Files: geometry/frames.ts:201-256.

Lines — no valence; isolated atoms invisible — M

One line per bond, no double/triple valence offsets, ignores line_width; an atom with no bond draws nothing (PyMOL draws a small cross). Files: geometry/frames.ts:143-193.

Dashes / measurements — solid lines, values dropped — M

Distance/angle/dihedral render as solid lines (no gap pattern; ignores dash_gap/length/radius/color) and the computed numeric value text is discarded — never shown on screen. Angle arc is a 6-segment polyline. Files: exec/measurement.ts:91-160.

Atom labels — DOM overlay, no depth occlusion — M

Labels are absolutely-positioned DOM <span>s with hard-coded style, so label_color/size/font_id/bg_color/outline_color/offset are all ignored. Only culled behind the camera, never occluded by geometry (always on top); no connector lines. Files: viewport/src/labels.ts:37-84, cmd/display.ts:210-255.

Anti-aliasing — impostor silhouettes not smoothed — M

MSAA only smooths triangles; sphere/cylinder impostor edges come from shader discard + gl_FragDepth, so silhouettes are hard 1-sample cutouts, and pixelRatio is pinned to 1 (no supersampling headroom). Jaggy edges on otherwise-strong scenes. Wants analytic edge-AA in the impostor shaders or a supersampled offscreen target. Files: viewport/modeG/renderer.ts:118-162, materials/sphere.ts, cylinder.ts.

Ambient occlusion — shader path exists, no geometry feeds it — M · matches default PyMOL

Full AO plumbing in the material (u_hasAo, in float ao, color.rgb *= v_ao) but no builder emits an ao buffer, so v_ao is pinned to 1.0. This MATCHES default GL PyMOL (ambient_occlusion_mode 0) and only diverges from the ray-traced reference — see §4. Files: materials/vertex.ts:11-82, surface.ts:32.

4. Deliberately deferred / declined

Items consciously not done, with the reason — so they don’t get re-litigated. Most trace to the parity target being real-time GL, not the ray-tracer.

F4 per-vertex ambient occlusion — declined — M

Desktop PyMOL’s real-time GL path (the chosen parity target) ships AO off by default, so adding it would move the render away from the “looks like a fresh PyMOL install” target. The shader already consumes it; a spike measured it live on 5,235 verts. Revisit only if the quality bar shifts to the ray-traced look. Files: plan:58, materials/vertex.ts:50, surface_gen.ts.

True SES surface generation — deferred (F5 residual) — L

F5 only Taubin-smoothed the SAS mesh to look SES-like; real rolled-probe re-entrant surfaces are a substantial two-pass algorithm (or an EDT inward offset) left as follow-up. Surface similarity plateaued ~71–75%. Same root cause as the §3 surface item. Files: surface_gen.ts:11-14, docs/engine-port-gaps.md:31.

Ray tracing (cmd.ray) — likely permanent, punted to remote backend — L

There is no CPU ray tracer in the port; ray-traced PyMOL is explicitly not the parity gate, kept only as an aspirational reference. ray / draw-to-disk are side-effect-free no-ops. Files: docs/engine-port-gaps.md:124, plan:7.

Shadows / silhouette outline / OIT — out of scope — L

A real-time WebGL renderer can’t reproduce ray shadows/AO/SES pixel-for-pixel; only F4/F5 were scoped as quality fixes. This is the constant ceiling on the parity metric across all scenes — even strong sticks/cartoon top out ~95–98% partly because of the shadowless flat lighting. Files: plan:13, lighting.ts:22.

F2 viewport material/geometry pooling — subsumed by F1 — M

F1 memoizes rep frames at the engine push boundary, so an unchanged rep is neither rebuilt nor re-emitted — that stops the re-emit before it reaches the viewport, absorbing most of F2’s intended win. Viewport-side pooling stays available if per-frame BufferGeometry/material re-allocation still costs under interaction (see §5). Files: plan:57, renderer.ts:136.

TS-vs-PyMOL similarity left informational, not gated — S

Only the strict TS-vs-golden self-regression is enforced; the TS-vs-PyMOL similarity was meant to be a “threshold that tightens as fixes land” but was never converted to an enforced floor, so SES/cartoon work has no hard regression gate against the refs. Files: plan:46, apps/web/e2e/visual.e2e.mjs.

5. Performance & architecture

The structural levers on interactivity and scale — the biggest is getting the engine off the UI thread.

Move the TS engine off the main thread to a Web Worker — L · biggest interactivity win

LocalBackend is fully synchronous — every command (PDB parse, selection eval, whole Mode-G geometry build) runs on the UI thread, so a ~1.4s surface build freezes input and rendering. The Backend interface is already async/worker- shaped and geometry frames are already typed arrays that can post back as transferable ArrayBuffers (zero-copy). Large refactor. Files: backend.ts:31-60, engine.ts:855.

Surface/mesh build has no spatial index — ~1.4 s dominant cost — L

fieldAt() and nearest() each linearly scan all atoms; per emitted vertex the code calls fieldAt 6× (central-difference gradient) + nearest once + a second O(atoms) argmin fallback → ≈ 7·V·N distance evals with zero acceleration. A uniform grid / cell list / kd-tree over atom centers cuts each lookup from O(N) to ~O(1) — the dominant lever on surface latency. The grid also silently loses resolution above 90³ rather than scaling. Files: surface_gen.ts:171,184,240, surface_gen.ts:122.

Mode-G picking is approximate (~93.5%) and non-authoritative — L

The client-side geometric pick index is used only on the GL-free bridge (~1 click in 15 lands on a neighbouring atom); on a GL backend Mode-G has no independent pick. A real pick needs an offscreen id-buffer render + glReadPixels — a GL context the Web Worker can’t own, so it’s entangled with the worker move. Files: viewport.ts:508,515, viewport/src/picking.

Camera polled over transport at RTT — remote drag ~1–2 fps — M

Mode-G polls cmd.get_view() one request in flight, re-issued per input (+250 ms keep-alive), capping a drag at one frame per round trip. Optimistic local rotation exists but only on the GL-free path; against a GL backend the drag still tracks at RTT. Fix: extend optimistic local turn/move/zoom to the GL path, or push view deltas instead of polling. Files: viewport.ts:390,456, camera.ts:191.

No material/geometry pooling (F2) — M

renderer.apply() disposes and rebuilds GPU objects wholesale; each new RawShaderMaterial forces a shader recompile/relink and each dispose frees VBOs, so a rep that re-pulls (color change, compositor churn) pays full shader + buffer allocation. Pooling materials by static key and reusing setAttribute-updated buffers removes per-frame GC and compile pressure. Files: webgl/builder.ts:106,134, modeG/renderer.ts:332.

setView allocates a Matrix4 + re-pushes all uniforms per camera update — S

new Matrix4().fromArray(mv).invert() per call plus an O(keys×materials) pushUniforms walk (fog/ortho/bg) fired even on pure rotation, during active drag
  • the 4 Hz poll. Hoist a reusable scratch Matrix4 and gate the uniform fan-out on fog/ortho/bg actually changing.
Files: modeG/renderer.ts:276,282,289.

DPR / large-scene scaling gaps — M

Renderer fed device pixels with setPixelRatio(1); only dots re-apply DPR by hand, so other CSS-pixel primitives mis-size on HiDPI. Wide lines expand to instanced quads (geometry scaling with bond count, since WebGL2 clamps gl.lineWidth to 1). Mode-P server raster is glReadPixels-bound and scales with area×DPR. Not correctness bugs, but each caps how big / high-DPI a scene stays interactive. Files: modeG/renderer.ts:159,245, webgl/quadlines.ts:174.

6. Loose code TODOs worth tracking

Meaningful in-code markers and half-wired features surfaced across the engine, bridge, and web app.

Console command language — ✅ mostly resolved (Wave 2)

isCommandWord accepts any registered handler and runKeyword’s default case dispatches it generically, so every ported verb (scene new, store; spectrum count; dss; rotate x, 90; label …; distance d, …) is already a console verb — the old “13 verbs” limit is gone. Remaining: verbs that are themselves still unported (ray → remote backend; torsion, etc.) naturally still report. Files: packages/engine-ts/src/engine.ts (runKeyword), ref docs/engine-port-gaps.md.

cmd.do parser — ✅ kwargs + @script handled (Wave 2) — remaining: python-escape

cmd/parser.ts now splits keyword arg1, arg2, key=value into positional args + kwargs (quote-aware, so commas/= inside quotes are preserved), and @file.pml is reported honestly (no browser filesystem). Still out of scope: arbitrary python-escape expressions (those run as JavaScript via the /expr path). Files: cmd/parser.ts. menuData.ts needs the tools/gen-menus extractor (run get_menudata() in real PyMOL → JSON); the full tree is hundreds of leaves + radio groups and must not be grown by hand. Files: apps/web/src/layout/menuData.ts:16.

Object panel / mouse-mode / wizard blocks are static placeholders — L

placeholderData.ts values await the bridge objects topic; mouse-mode 5-char binding codes need ButModeGet/Translate; object-panel toggles are optimistic-only and the row A/S/H/L/C context menus are unbuilt; the Wizard block is absent; External-GUI docking has only the bottom dock. Files: placeholderData.ts:4,128, ObjectPanel.tsx:39, MouseModeBlock.tsx:17.

Fragment coordinates aren’t exact-parity with chempy fragments — M

Ships a hand-built heavy-atom library instead of reading data/chempy/fragments/*.pkl — enough for fragment ala to load and render, not coordinate-exact. Files: model/fragments.ts:8.

intra_rms / intra_rms_cur are unsuperposed — M

Should least-squares-fit each state before RMSD; the port skips the fit (documented approximation; reference state reports −1.0 like PyMOL). Files: cmd/extras.ts:423.

Command line has no Tab completion or drag-and-drop — M

Completion needs server-side cmd.kwhash / cmd.auto_arg + local filesystem (can’t be done in-browser); DnD live-preview insert is also missing. Files: apps/web/src/layout/CommandLine.tsx:21,26.

map_generate only accepts MTZ — M

Hard-codes MTZHeader; CIF/CNS/HKL reflection files are rejected with a canned message (upstream TODO). Files: packages/bridge/tenmol_bridge/panels/files.py:309.

Feedback broker needs a stdout/stderr tee — M

Python print() from cmd.do goes to process stdout, not the Ortho queue, so the web console misses every print from scripts/wizards/util.*. FeedbackBroker accepts plural sources but the tee isn’t wired. Files: packages/bridge/tenmol_bridge/feedback.py:16.

Dots use a Fibonacci-sphere approximation + O(N²) cull — S

Reproduces PyMOL’s Sphere0–4 dot counts (42/92/162/252/642) with a Fibonacci sphere instead of exact icosahedral point sets; the buried-point test scans every dot against all atoms with no acceleration; dot_normals / dot_lighting / connected-dot-surface not modelled. Files: geometry/dots.ts:45,99.

Viewport reshape reports CSS pixels, not framebuffer pixels — S

PyMOL wants framebuffer pixels; reshape currently sends CSS px → wrong on HiDPI (likely needs rect.width × devicePixelRatio). Files: apps/web/src/layout/Viewport.tsx:34.

Feedback log doesn’t translate PyMOL colour escape codes — S

\933-style escapes aren’t translated by text2html, so raw escape codes can leak into the console. Files: apps/web/src/layout/FeedbackLog.tsx:16.

Draw/Ray quick button rendered as a TODO (WP-19) — S

Shows quickbutton--todo and prints “not implemented in this wave” whenever no render_dialog hook is registered. Files: QuickButtons.tsx:54, RenderDialog.tsx:73.