Skip to content

Commit 602ad23

Browse files
zachcpclaude
andauthored
ferritin-bevy: MolViewSpec rendering pipeline + two rounds of visual polish (#164)
* feat: molviewspec volume nodes + data-driven color theme stubs - Add Volume/VolumeRepresentation node kinds for isosurface and grid-slice representations (ferritin-2g6) - Add Uncertainty/PlddtConfidence/Occupancy color theme variants with graceful uniform-white fallback until AtomCollection carries per-atom b_factor/occupancy (ferritin-ujk, follow-up ferritin-clv) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: screenshot capture example + visual ground truth for all bevy representations Adds bevy_capture_representations example that drives MvsPlugin through 29 scenes (6 representations x 4 color themes, plus 5 molviewspec_viewer presets) and screenshots each to docs/screenshots/ferritin-ala/. Grounds ferritin-ala's brainstormed improvement list in actual pixels rather than code-reading, and led to 10 filed follow-up issues (ferritin-ala.1-10) covering real bugs found this way: BallAndStick missing bonds, ChainId color theme being a no-op, labels never rendering, and others. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-core): infer bonds from residue templates when parsing CIF to Model parse_to_model/parse_to_trajectory always built an empty Bonds table with a "build bonds (empty for now)" stub, so any Model loaded from mmCIF had zero connectivity. This made ferritin-bevy's BallAndStick representation render as a disconnected point-cloud of atom spheres (ferritin-ala.1) since it reads bonds straight off Model::hierarchy. Add infer_bonds_from_residue_templates, mirroring AtomCollection's existing connect_via_residue_names: canonical-20 residue bond templates plus backbone C->N peptide bonds between consecutive polymer residues on the same chain. Wire it into both CIF->Model call sites. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-core): build chain_keys per-residue, not per-atom, in CIF parsing parse_to_model/parse_to_trajectory pushed one chain_keys entry per ATOM row while comp_ids/label_seq_ids/auth_seq_ids/groups were built per-RESIDUE. Since residue_to_chain is a Segmentation over residue_keys.len() elements built from chain_keys via change-point detection, feeding it atom-indexed data produced offsets in atom-count units. For 4hhb (~1069 atoms/chain but far fewer residues), chain_of_residue(res_idx) then returned chain 0 for nearly every real residue index, so every atom's derived chain_id string collapsed to "A". This made ferritin-bevy's ChainId color theme render every chain identically (ferritin-ala.2) even though the theme's own color-assignment logic was correct in isolation. Fix: push chain_keys only inside the "new residue" branch, alongside the other per-residue fields. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): render MVS label nodes as screen-space billboard text MVS `label` nodes were spawning an MvsLabel marker entity with a world-space Transform anchor but nothing ever turned it into visible text (the doc comment on MvsLabel even said as much: "a UI layer turns it into billboard text" — that layer never existed). ferritin-ala.3. Add update_mvs_label_billboards: each frame, projects every MvsLabel anchor's world position through the active Camera3d via Camera::world_to_viewport, and keeps a companion absolute-positioned bevy_ui Text entity at that screen position, creating it lazily on first sight and hiding it (Display::None) when the anchor is off-screen or behind the camera. The companion entity carries MvsEntity so it's cleaned up on the next scene reload like everything else the executor spawns. New test: test_label_billboard_ui_node_created, which manually seeds Camera::computed (MinimalPlugins doesn't run the camera-projection update system) the same way bevy_camera's own internal test helper does, then asserts a Text/UiNode entity appears at the expected screen position. Note: could not re-verify this visually via bevy_capture_representations in this session — the sandbox's GPU/render context is currently exhausted after many repeated window launches (confirmed by re-running the *unmodified* prior commit, which now also renders solid black for every scene, including scene 1). Verified instead via the new headless test plus direct code tracing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): render Line/Wireframe representation unlit rep_Line_*.png showed near-black hairlines, illegible without heavy zoom (ferritin-ala.4). Root cause: Wireframe geometry is a PrimitiveTopology:: LineList mesh with no meaningful vertex normals, but render_representation spawned every representation (Line included) with a default StandardMaterial, so Bevy's PBR lighting had nothing to shade against and rendered the lines essentially black regardless of their vertex color. Give Wireframe/Line its own unlit StandardMaterial so vertex colors (element/ chain/secondary-structure theme) show through directly; other representations keep normal PBR shading. Added RenderOptions: PartialEq to compare render_opt against RenderOptions::Wireframe. New tests: test_line_representation_material_is_unlit, test_ball_and_stick_material_is_lit. Note: same GPU/render-context exhaustion noted in the prior commit prevented visual re-verification via bevy_capture_representations this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): tighten focus() camera fit to actual view direction focus() nodes fit the camera distance using aabb_diagonal * 1.5 -- the worst-case any-angle bounding sphere. That heuristic is correct only when viewing the structure exactly along its longest diagonal; from the fixed default yaw/pitch every viewer example actually uses, it left single structures filling only a small fraction of the viewport (ferritin-ala.5). Measured directly (not just theorized): reconstructing the real camera and projecting the actual rendered 4hhb cartoon mesh's vertices onto a 2560x1440 viewport showed the structure covering only 660x562px (~26% width, ~39% height) despite focus() supposedly fitting it. Replace the flat diagonal*1.5 multiplier with fit_radius_for_view, which builds the camera's actual right/up basis from the current orbit yaw/pitch/up, projects the AABB's 8 corners onto those axes to get the real half-extents as seen from that direction, and picks a distance from Bevy's known 45 degree default vertical FOV (with a 15% margin) -- a tight fit for the actual view instead of a fit for every possible view. aabb_of_masked now returns (min, max) instead of (center, diagonal) so apply_focus has the corners to work with. New test: test_focus_fills_most_of_viewport, which reconstructs the viewer's camera and projects the real mesh geometry the same way the diagnosis did, asserting >50% viewport coverage in at least one axis (verified this fails against the prior formula: measured 26%/39% there). Note: same sandbox GPU/render-context exhaustion noted in the prior two commits prevented visual re-verification via bevy_capture_representations this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): focus() unions all focused components and honors transforms Finishes ferritin-ala.5's "clips multi-structure scenes" half. Two problems in apply_focus, both visible in the superposition preset (second/translated structure clipped at the frame edge): 1. Each focus() call fully overwrote ctx.orbit.focus/radius, so with multiple focused components (e.g. superposition's two structures) only the last one processed determined the framing — earlier structures were never accounted for. 2. apply_focus computed the AABB from `ac`'s raw local coordinates without applying the component's own `transform` (translate/rotate), so a translated structure's focus point was wrong even in isolation. Ctx now tracks focus_union: Option<(Vec3, Vec3)>, merged on every focus() call; apply_focus takes the component's Transform and applies it to all 8 local AABB corners (not just min/max, since rotation can change which corner is extremal) before folding into the union. Camera focus/radius are derived from the running union, so a scene with N focused components frames all of them. New test: test_two_focused_structures_frame_union (two copies of the same structure, second translated +300 on X; asserts orbit.focus.x lands near the midpoint rather than either structure alone). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore(beads): sync issue tracker state Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): tint Surface's Spacefill fallback so it reads as degraded Surface representation silently falls back to VdW spheres (Solid/Spacefill) with only a log warning; rep_Surface_*.png were pixel-identical to rep_Spacefill_*.png, giving a viewer user zero in-app signal their requested surface wasn't actually rendered (ferritin-ala.7). render_representation already carries `degraded` (from map_representation); apply an amber base_color tint + slight warm emissive to the material whenever a representation degrades, instead of only warning in the log. Real (non-degraded) Spacefill requests are unaffected. New tests: test_surface_fallback_is_tinted, test_spacefill_is_not_tinted. Note: same sandbox GPU/render-context exhaustion noted in prior commits this session prevented visual re-verification via bevy_capture_representations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): disable tonemapping so named colors read at full saturation Named colors (blue, orange, seagreen, steelblue) looked muted/desaturated against the viewport background across every representation (ferritin-ala.8). Root cause: Camera3d auto-inserts Bevy's default Tonemapping::TonyMcMapface as a required component -- a filmic display transform explicitly designed to compress and selectively desaturate bright input for cinematic realism. That tradeoff is backwards for a molecular viewer, where legible, undistorted color themes (element/chain/secondary-structure) matter more than a "film look". Add disable_tonemapping_on_new_cameras, an Update system (chained after the executor/label systems) that reacts to Added<Camera3d> and inserts Tonemapping::None, so every consumer of MvsPlugin gets legible color by default without needing to remember to configure it themselves. New test: test_camera3d_gets_tonemapping_disabled. Note: same sandbox GPU/render-context exhaustion noted in prior commits this session prevented visual re-verification via bevy_capture_representations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): surface Surface's degradation warning through MvsError, not just log Surface's fallback to VdW spheres only called warn!(), so a user running the interactive viewer had no way to know their scene wasn't what they asked for -- the MvsError message channel and molviewspec_viewer.rs's status bar (collect_errors/format_error, already built and working) never saw it because nothing was pushed into ctx.errors for this case (ferritin-ala.9). Add MvsError::RepresentationDegraded { requested, rendered_as }, pushed alongside the existing warn!() call when render_representation degrades a representation. RepresentationTypeT now derives PartialEq (needed since MvsError derives it). Wire a message in molviewspec_viewer.rs's format_error match arm. New test: test_surface_fallback_emits_mvs_error. Note: same sandbox GPU/render-context exhaustion noted in prior commits this session prevented visual re-verification via bevy_capture_representations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): give Putty a synthetic varying-radius placeholder Putty rendered as a constant-radius (0.3) tube, pixel-identical to Cartoon's loop tube, since AtomCollection has no per-atom b_factor yet to drive real variable-radius rendering (tracked separately in ferritin-clv). Until that data is plumbed through, ferritin-ala.10 asked for at least a visual placeholder so Putty doesn't look like an unstyled Cartoon. generate_tube_mesh now takes one radius per curve point (generate_tube_mesh_ varying); the old constant-radius wrapper was removed since both call sites (render_putty, render_putty_mapped) now pass synthetic_putty_radii's output: a smooth sinusoidal thickness variation along the chain. This is explicitly not meaningful per-residue data -- just a legibility cue that the representation is supposed to carry variable thickness. New tests: test_synthetic_putty_radii_vary, test_putty_mesh_radius_varies_ along_chain (measures actual cross-section ring radii in the rendered mesh). Note: same sandbox GPU/render-context exhaustion noted in prior commits this session prevented visual re-verification via bevy_capture_representations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore(beads): sync issue tracker state Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): second-pass cartoon/putty rendering fixes (ferritin-t0h) Grounded in a fresh screenshot review of all representations (ferritin-t0h): - t0h.1: split cartoon/putty ribbons into contiguous backbone segments at chain-id changes, masked-index gaps, and CA-CA jumps >5A (is_backbone_break) so the spline no longer draws straight tubes across chain breaks. render_cartoon /render_putty now delegate to the segmented _mapped path. - t0h.2: replace the tangent x global-Y cross-section frame (which flips 180deg near the Y axis, causing white starburst spikes at joints) with rotation- minimizing parallel-transport frames (sweep_frames) in both tube generators. - t0h.4: add a per-camera AmbientLight fill and give the capture rig the same 3-point directional setup as the interactive viewer, so lit cartoon surfaces no longer read near-black. - t0h.6: focus() the translated copy in the superposition preset so the camera frames both structures instead of clipping one off-frame. New tests: test_backbone_break_detection, test_cartoon_segments_multi_chain_4hhb, test_sweep_frames_are_continuous_and_orthonormal. 107 ferritin-bevy tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ferritin-bevy): thin BallAndStick bond sticks relative to atom balls (ferritin-c1k.1) Both render_ballandstick and render_ballandstick_mapped used radius=0.5 for atom spheres AND bond cylinders, so the representation read as one uniform fat licorice tube instead of balls joined by thin sticks. Added Structure::BALL_RADIUS=0.3 / STICK_RADIUS=0.15 (2:1 ratio) and wired them into both renderers. New test test_ballandstick_stick_thinner_than_ball guards the ratio. 108 tests pass. Also refreshes docs/screenshots/ferritin-ala/ with a full re-capture on current HEAD, visually confirming this fix plus the previously-landed ferritin-t0h fixes (chain-break segmentation, rotation-minimizing tube frames, ambient lighting, superposition framing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): clamp focus() radius to clear the whole rendered scene (ferritin-t0h.5) apply_focus's fit_radius_for_view only considered the focused component's own AABB, so focusing a tiny target (e.g. two ions) deep inside a much larger already-rendered structure pulled the camera in close enough to near-clip through the surrounding geometry (preset_label). Track Ctx.scene_bounds as the union of every rendered component's world-space AABB, and clamp the focus radius to also clear that full scene via a new fit_radius_for_view_from_center helper (refactored out of fit_radius_for_view). When focus == scene (the common whole-structure case) the two radii coincide, so ferritin-ala.5's tight single-structure fit is unaffected. New test test_focus_small_target_clamped_to_scene_bounds proves the same tiny target's radius is >3x larger when a full structure is rendered around it vs. focused in isolation. 109 tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): use true horizontal FOV for widescreen camera framing (ferritin-t0h.10) fit_radius_for_view_from_center used the vertical half-FOV (22.5deg) for both axes, over-estimating the distance needed to fit a structure's horizontal extent on a widescreen viewport (the true horizontal FOV is wider) and leaving whole-structure framing filling under half the frame. Query the live camera's logical_viewport_size() in execute_mvs_on_load, thread the resulting aspect ratio through Ctx, and derive the horizontal half-FOV via atan(aspect * tan(v_half_fov)) instead of reusing the vertical one. When the vertical extent is already the binding constraint (common for tall/roughly-square structures) this is a no-op by design. New tests test_fit_radius_tighter_on_widescreen_for_wide_aabb and test_fit_radius_unaffected_by_aspect_for_tall_aabb pin both cases. 111 tests pass. Also refreshes docs/screenshots/ferritin-ala/ with a fresh capture, visually confirming this fix plus ferritin-t0h.5 (preset_label now shows the full structure comfortably framed instead of two giant ion spheres near-clipping the view). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): orient cartoon ribbon cross-section with backbone geometry (ferritin-t0h.3) The cartoon ribbon's cross-section frame was purely parallel-transported from an arbitrary seed (tangent x global-Y at the first curve point), with no relation to the actual backbone geometry. The flat sheet ellipse's orientation therefore didn't track any meaningful anatomical direction, making it read as a round-ish tube from most camera angles instead of a recognizable flat ribbon. Add peptide_plane_normals: a per-residue reference direction via the Carson-Bugg virtual-torsion construction (normalize((CA[i]-CA[i-1]) x (CA[i+1]-CA[i]))), sign-corrected across residues to undo the 180-degree flip that construction produces roughly every residue in extended/beta conformations. Interpolate it onto the finer curve via nlerp_direction, and steer each ring's up vector toward this hint in a new sweep_frames_oriented, falling back to parallel transport when the hint is degenerate (parallel to the tangent) and sign-correcting against the previous frame so rings still can't flip 180 degrees (preserves ferritin-t0h.2's no-spikes property). Applied only to Cartoon; Putty's circular cross-section has no orientation to get wrong, so it keeps plain sweep_frames. New tests: test_peptide_plane_normals_sign_corrected_and_unit, test_sweep_frames_oriented_tracks_hint_and_stays_continuous, test_sweep_frames_oriented_falls_back_when_hint_degenerate. 114 tests pass. Also refreshes docs/screenshots/ferritin-ala/ with a fresh capture -- no regression on 4hhb, though its near-total-helix content means the sheet orientation improvement itself isn't visible in these particular renders. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): two-tone BallAndStick bonds by endpoint atom (ferritin-c1k.2) Bond cylinders were a single mesh whose every vertex mapped to atom_a in atom_map (render_ballandstick_mapped) or was baked with a flat grey color (render_ballandstick), so the MVS color theme -- which recolors every vertex by its atom_map entry -- painted the whole stick as atom_a's element color; the second endpoint's color never appeared. Add Structure::half_bond_cylinders(pos1, pos2, radius), splitting a bond into two half-length cylinders at its midpoint. render_ballandstick_mapped now maps the first half's vertices to idx_a and the second half's to idx_b; render_ballandstick (the non-MVS baked-color path) colors each half by its own endpoint atom's element color. New tests: test_half_bond_cylinders_splits_at_midpoint, test_half_bond_cylinders_degenerate_returns_none, test_ballandstick_bond_second_endpoint_gets_own_atom_map_entries (proves a real atom that's only ever a bond's second endpoint now receives bond contributions in atom_map instead of zero). 117 tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): render Line representation as thin cylinders, not GPU LineList (ferritin-t0h.9) A GPU LineList rasterizes to a hard 1px edge with no normals, aliasing badly on large structures (e.g. 4hhb) and unable to be lit or anti-aliased. Replace it with thin triangle cylinders (reusing the BallAndStick half_bond_cylinders helper, split at the bond midpoint per endpoint atom), which gives the Line rep real geometry that can be smooth-shaded and MSAA-covered. Drops the unlit-material special case in mvs_executor.rs since Wireframe meshes now carry real normals. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): raise Cartoon/Putty tube tessellation density (ferritin-c1k.3) The tube's circumference (16 verts) and along-curve interpolation (3-4 per residue) were coarse enough to read as an angular low-poly prism at close zoom. Introduce shared TUBE_CROSS_SEGMENTS=20 and TUBE_CURVE_SEGMENTS=6 constants (replacing three separately hardcoded copies of the same numbers across generate_cartoon_mesh, render_cartoon_mapped, and render_putty_mapped) for a rounder, smoother tube. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(ferritin-core, ferritin-bevy): plumb b_factor/occupancy into AtomCollection for viz (ferritin-clv) AtomCollection now carries optional per-atom b_factor and occupancy vectors (set via with_b_factor/with_occupancy, remapped through filter(), and round-tripped through to_model()/From<&Model>, preferring predicted-structure confidence over crystallographic b_iso when both are present). The Uncertainty, PlddtConfidence, and Occupancy color themes in ferritin-bevy now compute real gradients/bands from this data instead of always falling back to uniform white. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): stop labeling the single-unit preset as "Symmetry" (ferritin-t0h.8) Real symmetry-mate expansion is deferred (ferritin-229); the preset always renders just the deposited unit, which made the plain "Symmetry" button/preset name misleading. Rename the interactive viewer's button to "Symmetry (deposited unit)" and document the same caveat above the headless capture tool's preset_symmetry(), which has no status-bar warning surface to fall back on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(ferritin-cellscape): resurrect 2D protein SVG renderer on the Model API (ferritin-v8s) Migrate StructureFlatten from the stale AtomCollection API to Model, fix the hardcoded radius=50/300x300-viewbox placeholder with real per-element van der Waals radii and a bounding-box-normalized canvas, and add configurable projection axis (XY/XZ/YZ) and per-chain coloring. Re-enable the crate in the workspace (previously commented out) and add a dev-dependency on ferritin-test-data for the new unit tests covering bounding-box math, projection selection, and SVG output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ferritin-bevy): shrink isolated water spheres in BallAndStick/Spacefill (ferritin-t0h.7) Water molecules (HOH/WAT/H2O) have no bonds, so at full atomic radius they showed up as a scattered field of disconnected dots cluttering the periphery of the real structure. Shrink their sphere radius to 35% (WATER_RADIUS_SCALE) in both representations instead of hiding them outright, matching Mol*'s default de-emphasis of solvent. Ions are left untouched since they're often the deliberately highlighted feature (e.g. catalytic metal sites). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore(beads): sync issue tracker state Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4985a0a commit 602ad23

46 files changed

Lines changed: 3365 additions & 283 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.beads/interactions.jsonl

Lines changed: 29 additions & 0 deletions
Large diffs are not rendered by default.

Cargo.lock

Lines changed: 165 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
members = [
33
"crates/ferritin",
44
"crates/ferritin-bevy",
5-
# "crates/ferritin-cellscape",
5+
"crates/ferritin-cellscape",
66
"crates/ferritin-core",
77
"crates/ferritin-examples",
88
"crates/ferritin-molviewspec",

0 commit comments

Comments
 (0)