Skip to content

feat(widgets): circle lock_center — pin the centre, keep the radius draggable - #58

Merged
CSSFrancis merged 1 commit into
mainfrom
feat/circle-lock-center
Aug 13, 2026
Merged

feat(widgets): circle lock_center — pin the centre, keep the radius draggable#58
CSSFrancis merged 1 commit into
mainfrom
feat/circle-lock-center

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

A ring measured on a power spectrum is centred on the DC term. Its centre is not
a free parameter, so a draggable one is a control that can only ever be wrong —
and a ring nudged off-centre silently corrupts every radius measured from it.

plot.add_circle_widget(cx=256, cy=256, r=64, lock_center=True)

The centre is pinned; the radius handle still drags.

Why the hit-test, and not a callback

The obvious implementation lives entirely in user code: watch the widget, and
put the centre back when the drag settles. It does not work, and the way it
fails is worth writing down because it looks like it is working when you read
the code.

_doDrag2d recomputes the position every frame from its own grab-time
snapshot (w.cx = s.cx + dix). A centre pushed from Python is therefore
overwritten on the very next mousemove. What the user sees is the ring tracking
the cursor for the whole drag and snapping home on release — which reads as a
broken lock rather than a locked ring. Moving the correction from pointer_up
to pointer_move only shortens the wander; it cannot remove it, because the
fight is one round trip against one animation frame.

So the constraint has to be applied where the grab is decided. _ovHitTest2d
now continues past a locked circle's body instead of returning mode:'move'.
Two things fall out of that for free:

  • the hover cursor stays default rather than promising a move it will refuse;
  • the drag falls through to the plot's own pan, which is what a drag on the
    image does everywhere else — so the ring simply stops being a handle, rather
    than becoming a dead zone.

_doDrag2d keeps a matching guard for the one case the hit-test cannot cover:
a drag already in flight when the flag is turned on.

Tests

test_circle_lock_center.py — 5 API tests (including that the flag survives
to_dict(), since JS reads the widget dict) and 4 real browser drags:

body drag on a locked circle centre must not move — asserted on every event, not just the last, so a transient wander fails
the ring band, not just the centre hotspot the band is the move target and the part a user actually grabs
radius handle on a locked circle still drags — a locked centre must not cost the measurement
body drag on an unlocked circle still moves — a lock that leaked onto every circle would otherwise pass the whole file

The two lock assertions fail with the hit-test continue removed. 578 tests in
test_interactive/ pass.

Downstream

This is the fix for a Ground Crew calibration bug: the FFT ring wandered under
exactly the Python-side snap-back described above. Its e2e drives a real drag
and measures the ring against the FFT's DC disc — an oracle chosen because the
ring's canvas position moves 1:1 with a pan, so locked, snap-back and unlocked
all reported an identical 119.6 px displacement until it was measured relative
to the image instead. Against this branch it passes; against a snap-back lock it
fails at ~125 px.

…draggable

A ring measured on a power spectrum is centred on the DC term, so its centre
is not a free parameter: a draggable one is a control that can only ever be
wrong, and one nudged off-centre silently corrupts every radius measured from
it.

Enforced in the HIT-TEST, not afterwards. Correcting the centre from Python
when the drag settles cannot work -- _doDrag2d recomputes the position every
frame from its own grab-time snapshot, so a pushed-back centre is overwritten
on the next mousemove; the ring tracks the cursor for the whole drag and jumps
back on release, which reads as a broken lock rather than a locked ring.
Refusing the grab also leaves the hover cursor at 'default' and lets the drag
fall through to the plot's own pan, so the ring simply stops being a handle.
_doDrag2d keeps a guard for a drag already in flight when the flag is set.

test_circle_lock_center.py drives real browser drags: the two lock assertions
fail with the hit-test gate removed, and the unlocked circle + the radius
handle keep working.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.74%. Comparing base (729cb16) to head (bc7424c).

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #58   +/-   ##
=======================================
  Coverage   90.74%   90.74%           
=======================================
  Files          40       40           
  Lines        4526     4526           
=======================================
  Hits         4107     4107           
  Misses        419      419           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CSSFrancis
CSSFrancis merged commit 9f64e65 into main Aug 13, 2026
12 checks passed
@CSSFrancis
CSSFrancis deleted the feat/circle-lock-center branch August 13, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants