feat(spectrum): continuous edge auto-pan while dragging a slice#3581
Conversation
There was a problem hiding this comment.
Thanks @svabi79 — this is a thorough, well-instrumented change, and the velocity-vs-position-controller framing in the description (plus the env-tunable knobs and the AETHER_NO_DRAG_EDGEPAN A/B escape hatch) makes the UX decision easy to evaluate. CI is green across all six checks. One substantive concern and a couple of minor notes.
1. The new tune path bypasses applyTuneRequest's guards (lock / SWR sweep / diversity)
This is the main thing I'd want addressed before merge. The whole drag now routes through edgePanTuneRequested, and its MainWindow handler tunes the slice with a bare target->setFrequency(sliceFreqMhz):
queueActiveSliceForSpectrumTarget(target->sliceId());
target->setFrequency(sliceFreqMhz);Previously the drag emitted incrementalTuneRequested → applyTuneRequest(target, mhz, IncrementalTune, …). By going around applyTuneRequest you also drop the guards it applies to every other tune source (MainWindow.cpp:5654):
- Locked slice —
applyTuneRequestcallsnotifyTuneBlockedByLock()and restores the VFO readout.mousePressEventstartsm_draggingVforegardless of lock state, so with this PR a user can now drag a locked slice's passband and it will retune. That's a regression in the lock affordance. - SWR sweep —
applyTuneRequestearly-returns whilem_swrSweep.running. The direct path lets a drag retune mid-sweep. - Diversity mirroring —
applyTuneRequestcallsmirrorDiversityChildFrequency(slice, mhz); the direct path skips it, so the diversity child won't follow the parent during a drag.
I understand why you bypassed it — applyTuneRequest(IncrementalTune) ends in panFollowVfo, which is exactly the controller you're trying to keep out of the drag path. But the lock/SWR/diversity checks are orthogonal to pan-follow. Could you preserve them, e.g. by factoring the guard block (lock → notifyTuneBlockedByLock, m_swrSweep.running, mirrorDiversityChildFrequency) into a small helper that both applyTuneRequest and the edgePanTuneRequested handler call, so the edge-pan path tunes-without-follow but still honors them?
2. No upper bound on center (minor)
edgePanVelocityStep clamps the low edge (std::max(newCenter, m_bandwidthMhz/2.0)) and the MainWindow handler repeats that floor, but neither caps the high end. Holding at the right edge advances m_centerMhz (and reprojects the waterfall) unbounded until the radio echo clamps it back, which can show as overshoot-then-snap at the top of the tuning range. A symmetric upper clamp against the pan's max frequency would keep the local view consistent with what the radio will accept.
3. Direct sendCommand vs applyPanRangeRequest (nit, non-blocking)
The handler issues a raw display pan set … center=… rather than going through applyPanRangeRequest, so it doesn't update pan->applyPanStatus() locally or share that path's dedupe. You rely on the widget's local reproject for immediacy instead, which works — just flagging the divergence from the established explicit-pan path for consistency.
Nice work on the root-cause analysis and the symmetric L/R telemetry. The velocity-default sign-off is appropriately flagged as a maintainer call. Addressing #1 would be my one blocker.
🤖 aethersdr-agent · cost: $2.6158 · model: claude-opus-4-8
…Principle XI. Review of aethersdr#3581: the edge-pan tune path bypassed applyTuneRequest to avoid pan-follow, which also dropped the lock / SWR-sweep / diversity guards every other tune source honours -- a dragged *locked* slice would retune, a drag mid-SWR-sweep would tune, and the diversity child would not follow. Extract the lock + SWR-sweep guard into MainWindow::tuneBlockedByGuards() (a behaviour-identical refactor of applyTuneRequest's existing block) and call it from the edgePanTuneRequested handler before panning/tuning; a blocked tune now pans nothing either, matching the pre-existing locked-slice drag. Mirror the diversity child in the handler too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the careful review — addressed in ff79c04. 1. Guard bypass (lock / SWR / diversity) — fixed. You're right, this was a 2. No upper center clamp. Intentionally consistent with the existing pan 3. The velocity defaults remain flagged for your UX sign-off; |
…iple XI.
Dragging a slice to the panadapter edge to tune across the band only
crept and stuttered ("rubber band", worse going down): the edge follow
(revealFrequencyIfNeeded) is a position controller whose nudge is driven
by the cursor's overshoot past the trigger margin, and at the frame
border that overshoot is tiny and self-limiting (~0.1x span/s, measured),
so you had to zoom in first. The flag-extended trigger (aethersdr#2761) also fires
asymmetrically just inside the edge, fighting the follow on the flag side.
Replace the edge behaviour with a velocity controller: while the cursor
sits in the edge zone, a ~30 Hz timer pans at a speed that scales with
edge depth and ramps with hold time (measured 1.2x span/s at full
depth+ramp, monotonic, symmetric L/R), parking the slice just inside the
leading edge so it stays visible while the band scrolls under it. Pan+tune
go through a new edgePanTuneRequested signal that bypasses pan-follow so
the two controllers can't fight; reveal is removed from the drag path
entirely (in-window moves tune-only). aether.perf SliceDrag logging makes
a drag replayable from the support log.
The velocity defaults (top speed / ramp / interval) are a UX decision and
are exposed as env overrides (AETHER_DRAG_EDGEPAN_VMAX / _RAMP /
_INTERVAL) so the feel can be tuned without a rebuild -- they need
maintainer sign-off. AETHER_NO_DRAG_EDGEPAN=1 restores the legacy
reveal-only behaviour for A/B comparison.
Fixes aethersdr#3580
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Principle XI. Review of aethersdr#3581: the edge-pan tune path bypassed applyTuneRequest to avoid pan-follow, which also dropped the lock / SWR-sweep / diversity guards every other tune source honours -- a dragged *locked* slice would retune, a drag mid-SWR-sweep would tune, and the diversity child would not follow. Extract the lock + SWR-sweep guard into MainWindow::tuneBlockedByGuards() (a behaviour-identical refactor of applyTuneRequest's existing block) and call it from the edgePanTuneRequested handler before panning/tuning; a blocked tune now pans nothing either, matching the pre-existing locked-slice drag. Mirror the diversity child in the handler too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ff79c04 to
a5eaf6a
Compare
|
Ready for review when you have a moment, @ten9876. CI is green across all six checks, and the review blocker from @aethersdr-agent (lock / SWR-sweep / diversity guards on the edge-pan tune path) is addressed in the latest commit. The branch is rebased onto current This touches |
ten9876
left a comment
There was a problem hiding this comment.
Approving. The maintainer has signed off on the velocity feel (the UX decision this PR correctly flagged for sign-off), and I verified the engineering both by reading and by capturing live aether.perf SliceDrag telemetry on a build of this branch.
Engineering — verified correct
- Controller-fight avoidance is real. The drag path emits only
edgePanTuneRequested(→ a pan-without-reveal handler usingdisplay pan set center=+setFrequency(autopan=0));incrementalTuneRequestedis removed from the drag branch and still serves wheel tuning. The two controllers can't both run during a drag. - The
tuneBlockedByGuardsrefactor is the right call. A naive bypass ofapplyTuneRequestwould have silently dropped the lock + SWR-sweep affordances; extracting them into a shared guard both paths call is correct and behavior-preserving. - Timer-stop paths are comprehensive for the normal case (tick guards on
m_draggingVfo, zone-exit stops in bothupdateVfoDragEdgePanandedgePanVelocityStep, andmouseReleaseEventstop + ramp reset).
Live telemetry — behaves exactly as designed
Two clean sweeps, one each direction:
- center monotonic in both (14.2836→14.3439 up; 14.3438→14.2625 down) — no jump-back
- slice parked at the zone boundary (
sliceXpinned at 1780 right / 93 left) — symmetric L/R - ramp resets on zone re-entry and climbs to cap over ~600 ms; velocity ≈ 1.0× span/s at near-full depth (≈1.2× at full), matching
VMAX=120 - timer stopped cleanly on zone exit — no runaway pan observed; no errors from the feature
Recommended follow-ups (non-blocking — merging on maintainer's call)
- Runaway-pan robustness.
m_draggingVfois cleared only inmouseReleaseEvent. If a release is ever lost mid-drag (alt-tab, mouse-grab theft, widget hide), the 30 Hz timer would keep panning + retuning the rig off the stale cursor X. Worth a belt-and-suspenders clear onhideEvent/focus-out/grab-loss (or a no-mouse-move watchdog). Didn't trigger in testing, but it now drives the radio, so the failure mode is more serious than before. - Asymmetric center clamp.
newCenteris lower-clamped (bandwidthMhz/2) but has no upper clamp, so sweeping toward the top of the radio's range can overscroll the local view until a status update corrects it. Clamp both ends to the pan's valid range.
Both are robustness hardening, not correctness blockers. Nicely done — env-tunable feel knobs + AETHER_NO_DRAG_EDGEPAN=1 A/B hatch made validating this straightforward. 🚀
…elease (#3786) ## Summary Fixes #3785. With **Pan Lock** on, dragging a slice made the panadapter jump in large lurching steps instead of scrolling smoothly, and afterwards Pan Lock stopped keeping the slice centered (button lit) until toggled off/on. Two independently-developed features both drive the pan center and fought each other: - **Pan Lock** (#3408) recenters on every slice-0 `frequencyChanged` (*slice at center*). - **Edge auto-pan** (#3581) advances the center smoothly and parks the slice at the edge (*slice at edge*). The edge-pan path tried to *"avoid pan-follow"* but the bypass was incomplete — it still calls `setFrequency()`, which emits `frequencyChanged`, and Pan Lock is wired straight to that signal, so both fired each tick with opposite intents. The in-window drag had the matching symptom (slice snapping back to center instead of following the cursor). **Fix:** Pan Lock stands down for the **whole duration of a slice drag** (in-window tune *and* edge auto-pan), then **recenters once on release** so Pan Lock re-asserts. `SpectrumWidget` emits `sliceDragActiveChanged(bool)` on slice-drag start/end; `MainWindow` suppresses the recenter while the flag is set and recenters on release. The recenter logic is extracted into `MainWindow::recenterPanFollowOnSlice0()` (called on enable, on slice-0 `frequencyChanged`, and once on drag-end), self-guarded on the Pan Lock toggle state. This mirrors the existing WFM precedence (WFM already makes Pan Lock stand down). This is a UX precedence choice (during a drag the slice follows the cursor / the band scrolls; on release the slice snaps back to center). Flagged for maintainer confirmation, consistent with the WFM-precedence note already in the code. ## Measurements i9-13900K / RTX 4090, GPU path, dummy load, Pan Lock on; gated `aether.perf` `drag=1` windows + the `SliceDrag` center trace. | | before | after | |---|---|---| | in-window drag — `panCenterCmdRate` | ~34 / s (Pan Lock fights every move) | **0–2 / s** (stands down) | | in-window drag — pan center | snaps to slice each move | **stable** (slice follows cursor) | | edge drag — pan center steps | **~0.33 MHz lurches** | **smooth** (single controller) | | on release | `panCenterCmds=0` → slice left off-centre | **exactly 1** recenter → slice re-centered | Verified on real hardware: no jumping during in-window or edge drag, and Pan Lock stays active after release (no toggle needed). ## Constitution principle honored **Principle XI — Fixes Are Demonstrated** (before/after measured via gated `aether.perf`, table above). **Principle VIII — Evidence Over Assertion** (root cause and fix confirmed by instrumented measurement on real hardware). ## Test plan - [x] Local build passes (Windows, Qt 6.8.3, GPU path on) - [x] Behavior verified on a real radio / dummy load — in-window + edge drag smooth; Pan Lock re-asserts on release; normal tuning still recenters; WFM precedence intact - [ ] Existing tests pass (CI) - [x] Reproduction steps documented in the linked issue ## Checklist - [x] Commits are signed (SSH) - [x] No new flat-key `AppSettings` calls (N/A) - [x] Code is clean-room (Principle IV) - [x] All meter UI uses `MeterSmoother` (N/A — no meter changes) - [x] Documentation updated if user-visible behavior changed (N/A — restores intended Pan Lock behavior; no new user-facing controls) - [x] Security-sensitive changes reference a GHSA if applicable (N/A) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Continuous edge auto-pan while dragging a slice across the panadapter, so you can
sweep the whole band in one gesture without zooming in first. Fixes #3580.
Replaces the edge-follow position controller (
revealFrequencyIfNeeded) —whose nudge is bounded by the cursor's self-limiting overshoot at the frame
border (~0.1× span/s, the "rubber band" creep) — with a velocity controller:
while the cursor sits in the edge zone a ~30 Hz timer pans at a speed that scales
with edge depth and ramps with hold time (~1.2× span/s at full depth+ramp),
parking the slice just inside the leading edge so it stays visible while the band
scrolls under it. Pan+tune go through a new
edgePanTuneRequestedsignal thatbypasses pan-follow so the two controllers can't fight; reveal is removed from
the drag path entirely (in-window moves tune-only). This also removes a
one-sided stutter/jump-back caused by the flag-extended trigger (#2761) firing
asymmetrically just inside the edge on the flag side.
This is a UX behaviour change — per AGENTS.md not an autonomous change. The
velocity defaults (top speed / ramp / interval) are a feel decision and need
your sign-off. They're exposed as env overrides so you can tune them live without
rebuilding:
AETHER_DRAG_EDGEPAN_VMAX— top speed, % of span per second (default 120)AETHER_DRAG_EDGEPAN_RAMP— ms to ramp to top speed (default 600)AETHER_DRAG_EDGEPAN_INTERVAL— timer interval ms (default 33)AETHER_NO_DRAG_EDGEPAN=1— restore legacy reveal-only behaviour (A/B)Test plan / evidence
directions on a live FLEX.
aether.perfSliceDragtelemetry: legacy ≈ 0.0027 MHz/tick regardless of howhard you push (~0.1× span/s); fixed = 0.00182 MHz/tick at full depth+ramp
(1.2× span/s), center monotonic, no jump-back, symmetric L/R, slice parked at
the 5% boundary.
AETHER_NO_DRAG_EDGEPAN=1reproduces the old behaviour on the same build for adirect A/B comparison.
Scope
Independent of #3578 (waterfall perf) and deliberately kept separate from #3444
(mouseMoveEvent overload).
Principle XI.
🤖 Generated with Claude Code