Skip to content

feat(processing): pick the Reference source position on the spectrum - #1

Closed
Limdongcheng wants to merge 9 commits into
fix/peaks-follow-referencefrom
feat/reference-pick
Closed

feat(processing): pick the Reference source position on the spectrum#1
Limdongcheng wants to merge 9 commits into
fix/peaks-follow-referencefrom
feat/reference-pick

Conversation

@Limdongcheng

@Limdongcheng Limdongcheng commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Problem

The Reference step's source position (at_ppm) could only be typed as a number, while the value the user actually wants is "the peak currently sitting there" — a position they can see on the plot but had to read off manually and re-type.

What this adds

The Reference step editor gains a Pick position on spectrum row (rendered with the shared clickable-affordance accent). Clicking it arms a one-shot on-plot pick:

  • Hovering the target plot previews the position with a dashed guide line, an apex dot, and a ppm readout by the cursor.
  • The pick reuses the zoom-scaled apex snap that manual peak picking uses (Trace1d::pick), so weak lines are reachable by zooming in first; Shift skips the snap and reads the exact cursor position.
  • A click writes the step's at_ppm through the property catalog — one undo step, the same recompute as typing — and the status bar prompts for the target position.
  • Esc, collapsing the editor, or switching datasets disarms the pick.

Design

  • Mode lifetime mirrors the on-plot phase mode: the arm state (session.ui.reference_pick, typed DatasetId + StepId) is revalidated every frame and is only valid while the step editor that armed it stays expanded on the active dataset. A collapsed card can never leave a live click trap on the plot.
  • Coordinate conversion: the picked coordinate lives on the finished axis while at_ppm lives on the axis entering the step. The commit subtracts the calibration applied at or after the step, exposed as AxisPipeline::chemical_shift_offset_from_step_ppm beside the existing whole-pipeline reduction so reference-step semantics stay in one place. Re-picking a step that already carries an offset still lands the picked feature exactly on target_ppm.
  • Pointer ownership: the pick handler runs after navigation (wheel/trackpad zoom keep working while aiming at a peak) and consumes the pointer over the plot, so the click cannot fall through to layout or data gestures.
  • 2D axes: a dimension without a 1D trace picks the raw coordinate (x for vertical axes, y for F1) without snapping.
  • No new CommandId: the row is a panel-local widget interaction parameterized by the step whose editor it sits in — the same catalog exemption the Add-step menu uses. A palette command would have no visible editor context and would arm an invisible mode.

Tests

  • plotx-processing: the from-step offset reduction (enabled/disabled steps, unknown id).
  • plotx-core: arm/resolve/sync lifecycle — expanded-editor gating, toggle disarm, non-Reference steps never resolve.
  • plotx (app): end-to-end commit through the property catalog — offset-aware conversion, one undo step, undo restores the previous value.
  • canvas/mod.rs's inline test modules moved to a sibling mod_tests.rs (unchanged) to keep the file under the 800-line limit.

cargo pr-check passes. The Processing manual page (EN + zh-CN) gains a Reference section documenting the workflow; npm run build in docs/ passes.


Part 6 of the stack, based on fix/peaks-follow-reference (#6); full chain: #2#3#4#5#6 → this PR. Retarget to main once the predecessors merge.

@github-actions

Copy link
Copy Markdown


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

Limdongcheng and others added 7 commits August 29, 2026 10:40
fit_layout translated a card rigidly with its anchored boundary's
displacement, so hiding the secondary sidebar teleported every
right-anchored card by the full sidebar width even when it was parked
mid-board — the user then had to drag it all the way back. CardLayout
now keeps the user-intended rectangle (`preferred`) separate from the
fitted one: only an edge resting on a boundary follows that boundary
(the default top-right docking), a parked card keeps its absolute
position and is merely clamped inside the new bounds, and a card that a
sidebar pushed aside returns to its place once the boundary recedes.
Drag and resize gestures write both rectangles, so drag origins always
match what is on screen.

A full-render regression test drives sidebars, workspace geometry and
the Processing card through a hide/drag sequence; fit-level tests pin
the parked-stays-put and displaced-returns semantics.
…ist#72)

Task-card rows that expand or reveal content on click (processing step
rows, CRAFT signal groups, region rows) rendered as plain text until
hovered, so users never discovered they were clickable. The Ribbon
already has a clickable visual language — enabled button glyphs carry
`Visuals::hyperlink_color` — so a new `ui::affordance` module exposes
that colour (`clickable_tint`) plus a `selectable_row` helper that
tints a row's leading glyph with it while the label keeps the theme
text colour. The three click-to-enter rows now render through the
helper; Statistics and Curve Fit cards expose their actions through
framed buttons and combo boxes already, so they need no change.
`H` fits the y axis to the data visible in the current x window (the
NMR vertical-fit convention — reset_y's window-scoped auto range, not
full_y) and `F` fits both axes, both as single undoable viewport steps
sharing the double-click reset path.

Plain `F` was already bound to ZoomToSelection (board-level frame fit),
so the chord is now context-split by a focused handler instead of the
dispatch table: with the pointer on a plot's data area it runs the new
FitPlotXY, anywhere else it keeps ZoomToSelection. Both fits are
registered as CommandId (FitPlotY / FitPlotXY), gated in describe on an
active plot, searchable in the palette, and listed in the View menu;
invoked without a pointer target they fall back to the active plot.
`H` was unbound before.

The keyboard shortcut reference (English and Simplified Chinese) now
documents the new keys; shortcuts.rs tests moved to a sibling
shortcuts_tests.rs to stay under the source-size limit.
Manual picking snapped to the tallest local maximum inside a window
fixed at 1.5% of the full x span, regardless of zoom — so next to a
strong line, a weak line could never be picked: zooming in sharpened
the click but not the window. The apex search window is now derived
from a fixed screen radius (12 px) at the current zoom, so zooming in
narrows it in step with the click precision; tallest-in-window is kept
(over nearest-local-maximum) so zoomed-out clicks land on real peaks
instead of the nearest noise wiggle. Shift+click skips the snap and
places the mark on the nearest sample — the escape hatch for shoulders.
The hover preview resolves through the same path, modifier included.

`Trace1d::snap` becomes `snap_within(x, half_width)` +
`nearest_sample`, unified under `pick(x, ManualPeakSnap)`;
`add_manual_peak` takes the snap explicitly. Range drag-picking
(`pick_in_range`) keeps its 3-sigma prominence floor: it is scoped to
the dragged window with MAD-based noise, which peaks inflate very
little, and the reported failure was click-snap, not range picking.

New peaks_tests.rs pins the narrow-window weak-apex pick, the
wide-window tallest pick, free placement, and the no-apex fallback.
The peak-picking guide (EN + zh-CN) documents the zoom-scaled snap and
Shift placement.
)

PeakMark stored its x as a finished-spectrum snapshot, so editing a
Reference step shifted the whole ppm axis while every existing mark
stayed at the old coordinate — visibly detached from its peak.

Marks now store the *uncalibrated* position (finished x minus the
pipeline's reference offset at pick time) and every reader adds the
current offset back through `PeakSet::resolve(offset)`, fed by the new
`Dataset::peak_reference_offset_ppm()` (the pipeline's single semantic
outlet `chemical_shift_reference_offset_ppm`, previously used only by
CRAFT; tables calibrate by zero). This is the lifecycle-copy option:
the calibration lives once, in the pipeline, and marks follow any
Reference edit — including undo/redo of the processing step — without
being rewritten, so no mark-translation action or migration exists.
Both write paths (manual picks and detection on the finished trace)
subtract the current offset symmetrically; de-duplication compares in
uncalibrated space. Persisted mark coordinates change meaning; the
format stays v1 with no compatibility reader, per repository policy.

`y` deliberately remains a pick-time intensity snapshot: reference
edits never change intensities, and refreshing y after re-phasing or
normalization is an orthogonal concern.

Regression tests: mark → reference edit → resolved ppm follows the
shifted trace and its default label; picks on an already-referenced
spectrum round-trip through the uncalibrated store.
The Reference step's source position could only be typed as a number,
while the value the user wants is "the peak currently sitting there" —
a position they can see on the plot but had to read off manually.

The step editor gains a "Pick position on spectrum" row (styled with the
shared clickable-affordance accent) that arms a one-shot on-plot pick.
While armed, hovering the target plot previews the position with a
dashed guide line, an apex dot and a ppm readout; the pick reuses the
zoom-scaled apex snap manual peak picking uses (Shift = nearest sample),
so weak lines are reachable by zooming in first. A click writes the
step's at_ppm through the property catalog — one undo step, the same
recompute as typing — and prompts for the target position. Esc,
collapsing the editor, or switching datasets disarms; the arm state is
revalidated per frame like the on-plot phase mode, so a collapsed card
never leaves a live click trap.

The picked coordinate lives on the finished axis while at_ppm lives on
the axis entering the step, so the commit subtracts the calibration
applied at or after the step — exposed as
AxisPipeline::chemical_shift_offset_from_step_ppm beside the existing
whole-pipeline reduction, keeping reference-step semantics in one place.
After the edit the picked feature reads exactly target_ppm even when
re-picking a step that already carries an offset.

The pick handler runs after navigation (wheel/trackpad zoom keep
working while aiming) and consumes the pointer over the plot so the
click cannot fall through to layout or data gestures. An axis without a
1D trace (a 2D dimension) picks the raw coordinate without snapping.

canvas/mod.rs's inline test modules move to a sibling mod_tests.rs, and
ui_state.rs's dialog-state cluster to ui_state_dialogs.rs, to
stay under the 800-line limit. The Processing manual page (EN + zh-CN)
gains a Reference section documenting the workflow.
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants