Skip to content

focus: judge a filter by how sharply it peaks, not by how big it reads - #32

Merged
widgetii merged 2 commits into
mainfrom
focus-sweep
Sep 24, 2026
Merged

widgetii merged 2 commits into
mainfrom
focus-sweep

Conversation

@widgetii

Copy link
Copy Markdown
Member

The filter designer could change a filter but gave no way to tell whether the change was an improvement. The only feedback was the live focus number — and that number is a trap.

Measured on the 85H50AI: turning section 3 on took the peak from 25,698 to 55,642 while making the filter worse. That section reads higher as the picture blurs (r = −0.49 against an independent Laplacian ground truth), so it inflates every reading, the out-of-focus ones included. "Make the number go up" is exactly backwards.

What this adds

Measure it walks the lens out eight steps, reads the zone grid at each, walks it back, and reports the ratio — "Falls to 1/12 of its peak across the sweep" — never the peak. Contrast along a defocus sweep is the thing a focus filter is for.

The lens always comes back

The walk-back lives in a finally and is not conditional on the sweep still being current. Stopping, leaving the tab, a camera that stops answering and a thrown error all arrive there. Abandoning the operator's focus where an abort happened to leave it is worse than never measuring: the focus is gone and nothing said so.

The button exists only where focus.move does — a camera focused by hand cannot be asked to sweep.

Tests

Nine new checks in tests/ui-check.html: no button without a lens; the ratio is reported rather than the peak; the out and back step counts match; it ends at position 0; stopping returns the lens; leaving the tab returns it; a camera that stops answering is reported and the button comes back.

Each was mutation-tested — breaking the guard it covers turns exactly that check red:

break red
walk-back only when still current stopping still brings the lens back; leaving the tab brings it back too
report the peak, not the ratio it reports how far the reading falls, not how big it got
do not report a read failure a camera that stops answering is reported
walk out without walking back 4 checks
button offered without a lens no way to measure without a lens to drive

node tools/smoke.mjs and node tools/ui-check.mjs both pass.

The designer could change a filter but not tell you whether the change was
an improvement, so the only feedback was the live number -- and that number
is a trap. On the 85H50AI, turning section 3 on took the peak from 25,698 to
55,642 while making the filter worse: that section reads HIGHER as the
picture blurs (r = -0.49 against a Laplacian ground truth), so it inflates
every reading including the out-of-focus ones. "Make the number go up" is
backwards.

What actually matters is contrast along a defocus sweep: how far the reading
falls off peak as the lens walks away from focus. So "Measure it" walks the
lens out eight steps, reads the grid at each, walks it back, and reports the
ratio -- "falls to 1/12 of its peak" -- never the peak itself.

The lens is walked back in a finally, unconditionally. Stopping, leaving the
tab, a camera that stops answering and a thrown error all arrive there, and
abandoning someone's focus where an abort happened to leave it is worse than
never measuring: the focus is gone and nothing said so.

Offered only where focus.move exists; a camera focused by hand cannot be
asked to sweep.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Measure focus filters by defocus peak sharpness

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds lens sweeps that score focus filters by peak-to-trough contrast.
• Restores completed lens movements after success, cancellation, navigation, or read failures.
• Limits measurement to motorized lenses and adds comprehensive UI workflow coverage.
Diagram

graph TD
  A["Filter designer"] --> B{"Motorized lens?"}
  B -- "Yes" --> C["Pause live poll"] --> D["Read defocus grid"] --> E["Restore lens"] --> F["Compute ratio"] --> G["Show result"]
  B -- "No" --> H["Hide measure"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Bidirectional through-focus sweep
  • ➕ Samples both sides of the focus peak
  • ➕ Could reveal asymmetric filter response
  • ➕ Provides a more complete response curve
  • ➖ Requires more lens movement and measurement time
  • ➖ Increases accumulated relative-position error
  • ➖ Creates more opportunities for restoration failure
2. Absolute-position sweep and restore
  • ➕ Could restore a recorded position directly
  • ➕ Would avoid relying on balanced relative movement
  • ➖ Requires an absolute focus-position API not exposed here
  • ➖ Would reduce compatibility with cameras supporting only directional movement
  • ➖ Still needs robust cleanup for failed commands

Recommendation: Keep the one-direction relative sweep used by this PR. It measures the relevant focus discrimination while minimizing hardware movement, and the unconditional step-for-step restoration is the safest strategy supported by the existing directional focus API. A bidirectional or absolute-position sweep should only be considered if cameras later expose reliable position feedback.

Files changed (3) +381 / -13

Enhancement (2) +298 / -12
editor.jsShip the motorized-lens filter measurement workflow +149/-6

Ship the motorized-lens filter measurement workflow

• Mirrors the editor implementation in the distributable build. It adds shared grid summarization, cancellable defocus sweeps, unconditional lens restoration, ratio-based results, capability-gated controls, and lifecycle cleanup.

dist/editor.js

editor.jsAdd ratio-based focus filter sweeps +149/-6

Add ratio-based focus filter sweeps

• Adds an eight-step defocus sweep that pauses live polling, samples summarized zone peaks, and reports peak-to-trough contrast. The workflow restores every completed movement in a finally block, supports stopping and navigation cleanup, reports read failures, and only appears when focus.move is available.

src/editor.js

Tests (1) +83 / -1
ui-check.htmlCover sweep scoring, restoration, and failure handling +83/-1

Cover sweep scoring, restoration, and failure handling

• Extends the focus-camera fixture with position tracking, movement recording, position-dependent grids, and injected read failures. Adds UI checks for lens capability gating, ratio reporting, balanced movement, stop and tab-change restoration, failure feedback, and control recovery.

tests/ui-check.html

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Move failures can strand the lens ✓ Resolved 🐞 Bug ☼ Reliability
Description
runSweep() ignores rejected promises from the outbound, return, and stop calls to focus.move(),
although the existing movement path explicitly handles asynchronous rejection. When a camera rejects
a movement request, the sweep can continue with unchanged positions or claim restoration even though
the return command failed.
Code

src/editor.js[R3327-3329]

+				focus.move('far');
+				out++;
+				await pause(SWEEP_SETTLE_MS);
Evidence
The established moveSend() helper handles both synchronous throws and promise rejections, proving
that focus.move() may fail asynchronously. The new sweep bypasses that handling for every movement
command.

src/editor.js[3159-3178]
src/editor.js[3327-3345]
tests/ui-check.html[1784-1800]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The sweep does not observe promises returned by `focus.move()`, so rejected movement and restoration requests can become unhandled and the UI can report an invalid result or falsely claim the lens was returned.
## Fix Focus Areas
- src/editor.js[3327-3345]
- dist/editor.js[3327-3345]
- tests/ui-check.html[2318-2379]
## Recommended Fix
Await or otherwise observe every `far`, `near`, and `stop` result. Record outbound failures as measurement failures, keep attempting necessary cleanup after a return failure, and surface any inability to restore the lens instead of claiming that it is back; add tests with asynchronously rejected movement calls.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. A closed editor resumes camera polling ✓ Resolved 🐞 Bug ☼ Reliability
Description
The sweep's finally block calls startFocusPoll() whenever mode remains focus, without
checking the editor's dead state. If the editor is destroyed during a sweep delay or return walk,
its pending cleanup subsequently starts an indefinite sequence of focus.zones() calls against a
component that has already been removed.
Code

src/editor.js[3348]

+			if (mode === 'focus') startFocusPoll();
Evidence
Destroy stops the current poll and invalidates the sweep but leaves mode unchanged, while a
pending sweep always restarts polling in focus mode. startFocusPoll() immediately reads and
continually schedules subsequent reads, so this is a persistent post-teardown operation.

src/editor.js[3069-3080]
src/editor.js[3345-3348]
src/editor.js[4043-4053]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An in-flight sweep can restart focus polling after `destroy()` has stopped polling and removed the editor because sweep cleanup checks only the current mode.
## Fix Focus Areas
- src/editor.js[3345-3348]
- src/editor.js[4043-4053]
- dist/editor.js[3345-3348]
- tests/ui-check.html[2347-2379]
## Recommended Fix
Track teardown in sweep cleanup and restart polling only when the editor is alive, still in focus mode, and no other sweep owns focus reads. Add a test that destroys the editor during a sweep and verifies that no later zone reads occur.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Restarted sweeps fight over the lens ✓ Resolved 🐞 Bug ≡ Correctness
Description
An invalidated runSweep() unconditionally completes all recorded return movements, while leaving
Focus only increments sweepGen and does not serialize that cleanup. If the operator returns to
Focus and starts another measurement before the old return walk finishes, the old sweep sends near
while the new one sends far, invalidating the reading and final lens position.
Code

src/editor.js[R3338-3341]

+			for (let i = 0; i < out; i++) {
+				try {
+					focus.move('near');
+				} catch (e) { /* nothing left to try */ }
Evidence
Mode exit only changes the generation, whereas the old sweep's finally continues issuing return
commands and delays. Re-entering Focus rebuilds an enabled Measure button immediately, with no
active-cleanup guard shared between panel instances.

src/editor.js[3303-3307]
src/editor.js[3338-3348]
src/editor.js[3594-3605]
src/editor.js[3762-3778]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Leaving Focus invalidates a sweep without waiting for its mandatory return walk, allowing a newly created Measure button to start another sweep that conflicts with the old cleanup.
## Fix Focus Areas
- src/editor.js[3303-3307]
- src/editor.js[3338-3348]
- src/editor.js[3762-3768]
- dist/editor.js[3303-3307]
- dist/editor.js[3338-3348]
- tests/ui-check.html[2362-2369]
## Recommended Fix
Keep a shared active-sweep or cleanup promise and prevent a new sweep from starting until the previous sweep has completely returned and stopped the lens. Ensure an older cleanup cannot restart polling while a newer sweep owns camera reads, and test immediate Focus exit, re-entry, and restart.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (1)
4. Manual input corrupts a filter sweep ✓ Resolved 🐞 Bug ≡ Correctness
Description
The sweep's busy() state disables only the filter write and reload buttons, leaving the Focus
panel's Near and Far controls active. If the operator holds either control during measurement or
restoration, those uncounted commands alter lens travel so the reported ratio and claimed starting
position are no longer valid.
Code

src/editor.js[R3598-3600]

+				send.disabled = on;
+				back.disabled = on;
+			};
Evidence
Near and Far directly invoke the hold-to-run movement path, but the new busy state only controls
Measure, Stop, Try, and Read from camera. There is no sweep guard in the manual movement handlers,
so both paths can call focus.move() concurrently.

src/editor.js[3190-3249]
src/editor.js[3594-3605]
src/editor.js[3652-3663]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Measure workflow does not exclude the existing Near and Far controls, so manual commands can run concurrently with the sweep and its return walk.
## Fix Focus Areas
- src/editor.js[3190-3249]
- src/editor.js[3594-3605]
- src/editor.js[3652-3663]
- dist/editor.js[3190-3249]
- dist/editor.js[3594-3605]
- tests/ui-check.html[2318-2379]
## Recommended Fix
Introduce a shared sweep-active state checked by manual movement handlers and disable the Near and Far buttons for the full sweep, including cleanup. Restore them only after the return and stop operations finish, and add a test proving manual input cannot issue movement commands during measurement.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can choose which labels appear on a finding, and whether they show icons or text

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/editor.js Outdated
Comment thread src/editor.js Outdated
Comment thread src/editor.js Outdated
Comment thread src/editor.js
Four findings from review, all real, three of them one root: the sweep drives
the lens for several seconds and nothing else in the module knew it.

A move the sweep can observe. It called focus.move() raw, so a host that
reports failure by REJECTING -- which is what a fetch-backed host does --
left the rejection unhandled and the step counted anyway. A counted step
that never happened is a step handed back that was never taken, which walks
the lens PAST where the operator left it. moveOnce() reports the outcome; a
refused step out ends the sweep, and a refused step back is counted and said
out loud rather than papered over with "the lens is back where it started".

Sweeps are serialised. Aborting one bumps the generation, which stops it
measuring but cannot stop it walking back -- that part must finish. So for a
few seconds after leaving Focus there is still a sweep sending `near`, and a
new one starting then sends `far` against it. A new sweep now waits for the
old one's walk home, and is dropped if it was cancelled while it waited.

A destroyed editor does not resume polling. destroy() stops the poll and
empties the root without touching `mode`, so a finally asking only "still in
Focus?" answered yes for a panel that no longer existed and read the camera
for as long as the page stayed open.

Near and Far are held down for the duration, both halves load-bearing: the
buttons are disabled, and the handler refuses, for anything that reaches one
another way. Uncounted travel mid-measurement invalidates the ratio and the
claim about where the lens ended up.

Nine more checks. Each was mutation-tested -- breaking the guard it covers
turns exactly that check red, and no other.
@widgetii

Copy link
Copy Markdown
Member Author

All four findings were real and are fixed in e2197e3. Three share one root: the sweep drives the lens for several seconds and nothing else in the module knew it.

1 — Move failures can strand the lens. Correct, and it is the bug I had already fixed once for the motor buttons (moveSend) and then failed to apply here. Added moveOnce(verb), which reports the outcome rather than calling back, because the sweep needs to know: it counts steps out so it can give exactly that many back, and a step counted but refused walks the lens past where the operator left it. A refused step out ends the sweep; refused steps back are counted and reported — "The lens may not be back where it started: the camera refused 3 of the 8 steps back." — instead of the old unconditional claim that it is back.

2 — A closed editor resumes camera polling. Correct. destroy() stops the poll and empties the root without touching mode, so a finally asking only "still in Focus?" answered yes for a panel that no longer existed. Added sweepClosed, set in destroy(). The walk back still runs — the lens is real — but nothing after it touches the panel.

3 — Restarted sweeps fight over the lens. Correct. Aborting bumps the generation, which stops the sweep measuring but cannot stop it walking back. A new sweep now waits on sweepBusy, and a sweep cancelled while queued is dropped rather than starting on a tab nobody is looking at.

4 — Manual input corrupts a filter sweep. Correct. ownLens(on) disables Near/Far and refuses in the handler. Both halves turned out to be load-bearing: the disabled attribute for the pointer, the handler guard for a synthetic or keyboard path that reaches one anyway.

On the alternatives in the summary: agreed, and for the reason given. A bidirectional sweep accumulates relative-position error on a lens with no position feedback, and every extra step is another chance the return falls short of where the operator left it.

Tests

Nine more checks, and every guard mutation-tested — breaking it turns exactly the check written for it red, and no other:

break red
count a far the camera refused a lens that will not move is reported, not measured; a refused step is never handed back
ignore a refused step back a lens that will not come back says so, instead of claiming it is back
poll after teardown a destroyed editor does not go on reading the camera
do not serialise sweeps a new sweep waits for the old one to bring the lens back
handler lets a hold start and holding one during a sweep moves nothing
buttons not disabled the manual controls are held down for the sweep

One of them needed rewriting before it meant anything: sampling the lens position mid-walk proves nothing, since 2 on the way home and 2 on the way out look identical. It asserts on order instead — once a walk back starts, no far may be issued until the lens is home.

node tools/smoke.mjs and node tools/ui-check.mjs both pass.

@widgetii
widgetii merged commit 58aa374 into main Sep 24, 2026
1 check passed
@widgetii
widgetii deleted the focus-sweep branch September 24, 2026 16:15
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.

1 participant