Commit 602ad23
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
File tree
- .beads
- crates
- ferritin-bevy/src
- ferritin-cellscape
- examples
- src
- ferritin-core/src
- io
- model
- ferritin-examples
- examples/bevy
- ferritin-molviewspec
- src/molviewspec
- tests
- docs/screenshots/ferritin-ala
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
0 commit comments