Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/app/src/ui/tools/protein.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct Panel {
confirmed_only: bool,
feedback: String,
clear_spectrum: Option<(DatasetId, u64)>,
reference_baseline: Option<plotium_core::protein_reference::ShiftReference>,
reference_draft: Option<(DatasetId, plotium_core::protein_reference::ShiftReference)>,
context_draft: Option<scientific_context_editor::ContextDraft>,
}

Expand Down Expand Up @@ -244,6 +246,7 @@ fn body(app: &mut PlotiumApp, ui: &mut Ui, panel: &mut Panel) {
});
}
scientific_context(app, ui, panel, active);
reference_editor::render(app, ui, panel, active);
clear_review(app, ui, panel);
ui.separator();
if panel.draft.is_some() {
Expand Down Expand Up @@ -454,3 +457,6 @@ mod tests;
#[path = "protein_scientific_context.rs"]
mod scientific_context_editor;
use scientific_context_editor::scientific_context;

#[path = "protein_reference.rs"]
mod reference_editor;
67 changes: 67 additions & 0 deletions crates/app/src/ui/tools/protein_reference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use super::Panel;
use plotium_core::state::{DatasetId, PlotiumApp};
pub(super) fn render(
app: &mut PlotiumApp,
ui: &mut egui::Ui,
panel: &mut Panel,
active: Option<DatasetId>,
) {
ui.collapsing("Chemical-shift reference", |ui| {
ui.label("Absolute offsets from acquisition reference: corrected ppm = original ppm + offset.");
if let Some(id) = active {
if ui.button("Edit reference").clicked() {
match plotium_core::protein::volume(&app.doc, id) {
Ok(v) => {
panel.reference_baseline = Some(v.peaks.reference.clone());
panel.reference_draft = Some((id, v.peaks.reference.clone()));
},
Err(e) => panel.feedback = e,
}
}
for &other in app.doc.protein.context.experiments.keys() {
if other != id {
let result = app.reference_differences(id, other);
if let Ok(rows) = &result {
if rows.is_empty() { ui.label("No matching confirmed single-atom observations; not evaluated."); }
for row in rows {
ui.label(format!("Peak {} F{} vs {} F{}: delta {:+.5} ppm", row.left_peak.0, row.left_axis + 1, row.right_peak.0, row.right_axis + 1, row.delta_ppm));
}
}
ui.label(format!("{}: {}", other, result.err().unwrap_or_else(|| "References manually confirmed; inspect matching nuclei and axis roles before comparing peaks".into())));
}
}
}
let mut close = false;
if let Some((id, draft)) = &mut panel.reference_draft {
let name = app.doc.dataset_index(*id).and_then(|i| app.doc.datasets[i].as_nmr2d()).map(|n| n.name.as_deref().unwrap_or("Unnamed spectrum")).unwrap_or("Deleted spectrum");
ui.label(format!("Reference draft target: {name} ({id})"));
if active != Some(*id) {
ui.colored_label(ui.visuals().warn_fg_color, "The active spectrum differs from this draft. Saving edits the draft target shown above.");
}

for (axis, value) in draft.offsets_ppm.iter_mut().enumerate() {
ui.horizontal(|ui| { ui.label(format!("F{} offset (ppm)", axis + 1)); ui.add(egui::DragValue::new(value).speed(0.001)); });
}
ui.label("Reference source / standard / comparison evidence");
ui.text_edit_singleline(&mut draft.source);
ui.checkbox(&mut draft.confirmed, "Manually reviewed reference (not scientific certification)");
if ui.button("Save reference").clicked() {
let current = plotium_core::protein::volume(&app.doc, *id).map(|v| &v.peaks.reference);
let result = match current {
Ok(value) if Some(value) == panel.reference_baseline.as_ref() => app.set_shift_reference(*id, draft.clone()),
_ => Err("Reference changed or spectrum was removed. Discard and reopen the reference draft.".into()),
};
match result {
Ok(()) => { close = true; panel.feedback = "Reference saved".into(); }
Err(e) => panel.feedback = e,
}
}
close |= ui.button("Discard reference draft").clicked();
}
if close { panel.reference_draft = None; }
});
}

#[cfg(test)]
#[path = "protein_reference_tests.rs"]
mod tests;
123 changes: 123 additions & 0 deletions crates/app/src/ui/tools/protein_reference_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use super::*;
use crate::ui::tools::protein::Panel;
fn spectrum(app: &mut PlotiumApp) -> DatasetId {
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../io/tests/fixtures/nmr/bruker-3d-qf");
let (dataset, _) = plotium_core::workflow::dataset_from_acquisition(
plotium_io::load_path(fixture).unwrap().acquisition,
)
.unwrap();
let id = dataset.resource_id();
app.doc.datasets.push(dataset);
id
}

fn frame(
app: &mut PlotiumApp,
panel: &mut Panel,
ctx: &egui::Context,
active: Option<DatasetId>,
events: Vec<egui::Event>,
) -> egui::FullOutput {
ctx.run_ui(
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(1200., 2400.),
)),
events,
..Default::default()
},
|ui| render(app, ui, panel, active),
)
}

fn click(
app: &mut PlotiumApp,
panel: &mut Panel,
ctx: &egui::Context,
active: Option<DatasetId>,
label: &str,
) {
let output = frame(app, panel, ctx, active, vec![]);
let pos = output
.shapes
.iter()
.find_map(|shape| match &shape.shape {
egui::Shape::Text(t) if t.galley.text() == label => Some(t.pos + egui::vec2(3., 5.)),
_ => None,
})
.unwrap_or_else(|| panic!("Missing widget: {label}"));
for pressed in [true, false] {
frame(
app,
panel,
ctx,
active,
vec![
egui::Event::PointerMoved(pos),
egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed,
modifiers: Default::default(),
},
],
);
}
}

#[test]
fn switching_spectra_displays_draft_target_and_saves_only_that_target() {
let mut app = PlotiumApp::new();
let a = spectrum(&mut app);
let b = spectrum(&mut app);
app.doc.datasets[0].as_nmr2d_mut().unwrap().name = Some("Spectrum A".into());
app.doc.datasets[1].as_nmr2d_mut().unwrap().name = Some("Spectrum B".into());
let ctx = egui::Context::default();
let mut panel = Panel::default();
click(
&mut app,
&mut panel,
&ctx,
Some(a),
"Chemical-shift reference",
);
click(&mut app, &mut panel, &ctx, Some(a), "Edit reference");
let (_, draft) = panel.reference_draft.as_mut().unwrap();
draft.offsets_ppm = [0.125, 0., 0.];
draft.source = "External standard".into();
draft.confirmed = true;
let output = frame(&mut app, &mut panel, &ctx, Some(b), vec![]);
let labels: Vec<_> = output
.shapes
.iter()
.filter_map(|s| match &s.shape {
egui::Shape::Text(t) => Some(t.galley.text().to_owned()),
_ => None,
})
.collect();
assert!(
labels
.iter()
.any(|t| t.contains("Reference draft target: Spectrum A"))
);
assert!(labels.iter().any(|t| t.contains("active spectrum differs")));
click(&mut app, &mut panel, &ctx, Some(b), "Save reference");
assert!(panel.reference_draft.is_none());
assert_eq!(
plotium_core::protein::volume(&app.doc, a)
.unwrap()
.peaks
.reference
.offsets_ppm,
[0.125, 0., 0.]
);
assert_eq!(
plotium_core::protein::volume(&app.doc, b)
.unwrap()
.peaks
.reference,
Default::default()
);
}
28 changes: 28 additions & 0 deletions crates/core/src/actions/app_impl/processing_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,34 @@ impl PlotiumApp {
.doc
.dataset_index(snapshot.resource_id())
.ok_or_else(|| ActionApplyError::StaleTarget(snapshot.resource_id().to_string()))?;
let reference = |d: &Dataset| {
d.as_nmr2d()
.and_then(|n| n.volume.as_ref())
.map(|v| v.peaks.reference.offsets_ppm)
};
if reference(&self.doc.datasets[index]) != reference(snapshot) {
// Pins contain sampled coordinates/readings and cannot survive a reference change,
// including when a snapshot is installed by undo or redo.
let ui = &mut self.session.ui;
let owner = snapshot.resource_id();
if ui.inspect_cursor_pin.is_some_and(|p| p.dataset == owner) {
ui.inspect_cursor_pin = None;
}
if ui.delta_cursor_anchor.is_some_and(|p| p.dataset == owner) {
ui.delta_cursor_anchor = None;
ui.delta_cursor_pin = None;
}
if ui
.delta_cursor_pin
.is_some_and(|p| p.first.dataset == owner || p.second.dataset == owner)
{
ui.delta_cursor_pin = None;
ui.delta_cursor_anchor = None;
}
if ui.slice.is_some_and(|p| p.dataset == index) {
ui.slice = None;
}
}
let phase_basis = |dataset: &Dataset| {
dataset
.as_nmr2d()
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod project;
pub mod properties;
pub mod protein;
pub mod protein_context;
pub mod protein_reference;
pub mod settings;
pub mod state;
pub mod templates;
Expand Down
4 changes: 3 additions & 1 deletion crates/core/src/project/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,9 @@ pub fn object_to_dataset(
if let Some(peaks) = recipe.extensions["plotium.volume"].get("peaks") {
let volume = dataset.volume.as_mut().unwrap();
volume.peaks = serde_json::from_value(peaks.clone())?;
volume.refresh_peaks().map_err(ProjectError::Invalid)?;
dataset
.restore_volume_reference()
.map_err(ProjectError::Invalid)?;
}
dataset.acquisition_identity = read_acquisition_identity(data)?;
dataset.field_catalog = read_field_catalog(data)?;
Expand Down
Loading
Loading