Skip to content

feat: focus 3D viewer on point under cursor#12424

Draft
kijai wants to merge 3 commits into
Comfy-Org:mainfrom
kijai:feat/load3d-focus-under-cursor
Draft

feat: focus 3D viewer on point under cursor#12424
kijai wants to merge 3 commits into
Comfy-Org:mainfrom
kijai:feat/load3d-focus-under-cursor

Conversation

@kijai
Copy link
Copy Markdown
Contributor

@kijai kijai commented May 22, 2026

This is personally really important QoL feature for me after years of Blender use, extremely helpful when inspecting generated meshes etc. The code is generated by Claude, but I've done multiple iterations of reviews and also tested this quite a bit in practice. Future features could include configurable hotkey and snap distance.

Summary

Adds Blender-style "focus under cursor" — pressing F while hovering a 3D viewer raycasts to the surface under the cursor, then smoothly dollies the camera so the new pivot is the hit point and the camera lands at a tight close-up distance derived from the hit object's bounding sphere.

Changes

  • What: New attachFocusUnderCursor helper in src/extensions/core/load3d/load3dFocusUnderCursor.ts (window keydown gated by pointer-over-canvas, NDC-from-pointer math that accounts for graph zoom, THREE.Raycaster against the loaded model). New ControlsManager.animateTarget(point, distance, durationMs) that tweens both camera.position and controls.target with easeOutCubic and cancels on user interaction; setTarget collapses to animateTarget(...0) for snap. setRequestRender setter wires Load3d.forceRender into the tween. ControlsManagerInterface updated to reflect the new public methods.

Review Focus

  • NDC math under graph zoom: getNDCFromPointer corrects for ancestor CSS transforms (rect.width !== clientWidth). Without this, F lands off-target when the LiteGraph canvas is zoomed in/out. Worth a careful read.
  • Multi-Load3d safety: window keydown listener gated by per-instance pointerOnCanvas closure ensures only the hovered viewer responds. N instances on a graph = N listeners, each bails early except the hovered one.
  • Animation cancel-on-interact: if the user grabs orbit controls mid-tween, cancelAnimation removes the start listener and aborts the RAF chain so we don't fight user input. cancelAnimation nulls the cleanup ref after invocation so it can't double-fire.
  • Idempotent focus distance: computeFocusDistance derives the landing distance from the hit object's bounding sphere (FOV-corrected), not from the current camera distance. Repeated F presses on the same area settle, not compound.
  • Dispose ordering in Load3d.remove(): disposeFocusUnderCursor and controlsManager.dispose() both run before renderer.dispose(), so the in-flight RAF can't touch a torn-down WebGL context.
  • Test mocks: extended the existing OrbitControls mock with removeEventListener; animateTarget test stubs performance.now + requestAnimationFrame and restores via afterEach.

Screenshots

comfy_3d_focus.mp4

@kijai kijai requested a review from a team May 22, 2026 15:53
@kijai kijai requested a review from jtydhr88 as a code owner May 22, 2026 15:53
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label May 22, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47aa9a6c-da09-40cb-82b7-d2551166a56b

📥 Commits

Reviewing files that changed from the base of the PR and between dbbd19c and db2df93.

📒 Files selected for processing (3)
  • src/extensions/core/load3d/ControlsManager.ts
  • src/extensions/core/load3d/load3dFocusUnderCursor.test.ts
  • src/extensions/core/load3d/load3dFocusUnderCursor.ts

📝 Walkthrough

Walkthrough

Adds external render callback and animated/immediate target APIs to ControlsManager, a focus-under-cursor module that raycasts on F-key press, and Load3d integration wiring plus tests for both animation and focus behavior.

Changes

Focus animation integration

Layer / File(s) Summary
ControlsManager interface updates
src/extensions/core/load3d/interfaces.ts
ControlsManagerInterface adds setRequestRender(callback), setTarget(point, distance?), and animateTarget(point, distance?, durationMs?).
ControlsManager fields and request-render
src/extensions/core/load3d/ControlsManager.ts
Adds private fields for an optional requestRender callback and animation lifecycle, and refactors orbit end event to emit cameraChanged via buildCameraState.
ControlsManager animate/set target implementation
src/extensions/core/load3d/ControlsManager.ts
Implements setTarget/animateTarget with RAF-driven interpolation, computeFocusEnd, animation cancellation on re-invoke or user interaction, per-frame optional requestRender calls, cancelAnimation, buildCameraState, and ensures dispose() cancels animations.
Load3d construction and lifecycle integration
src/extensions/core/load3d/Load3d.ts
Imports and initializes attachFocusUnderCursor, sets controlsManager.setRequestRender to forceRender(), and stores/removes the focus handler during lifecycle teardown.
Focus-under-cursor module
src/extensions/core/load3d/load3dFocusUnderCursor.ts
New getNDCFromPointer and attachFocusUnderCursor converting pointer to NDC (accounting for CSS transforms and render-region offsets), handling F/f keydown guards, raycasting, computing focus distance from bounding sphere with FOV correction, and returning an abort-based cleanup.
ControlsManager and focus tests
src/extensions/core/load3d/ControlsManager.test.ts, src/extensions/core/load3d/load3dFocusUnderCursor.test.ts
Adds vitest teardown, OrbitControls mock removeEventListener, tests for setTarget and animateTarget (mocking time/RAF) and comprehensive tests for getNDCFromPointer and attachFocusUnderCursor behaviors and edge cases.

Sequence Diagram

sequenceDiagram
  participant User
  participant Load3d
  participant FocusHandler
  participant Raycaster
  participant ControlsManager
  participant OrbitControls
  participant Renderer
  User->>FocusHandler: F key + pointer over object
  FocusHandler->>FocusHandler: pointer to NDC (canvas bounds + CSS transform)
  FocusHandler->>Raycaster: raycast(ndc, camera, scene)
  Raycaster->>FocusHandler: hit mesh + point
  FocusHandler->>FocusHandler: distance = boundingSphere.radius × factor / FOV
  FocusHandler->>Load3d: focusOn(point, distance)
  Load3d->>ControlsManager: animateTarget(point, distance, 450ms)
  loop animation frames
    ControlsManager->>ControlsManager: t = elapsed / 450
    ControlsManager->>OrbitControls: target ← lerp(start, end, t)
    ControlsManager->>OrbitControls: position ← lerp(start, end, t)
    ControlsManager->>Load3d: requestRender?.()
    Load3d->>Renderer: forceRender()
  end
  ControlsManager->>Load3d: cameraChanged event
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

size:M

Suggested reviewers

  • dante01yoon

Poem

🐰 A hop toward focus, smooth and true,
When F is pressed o'er objects new,
The camera glides to see the sight,
With cursor-guided aim so right!

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: focus 3D viewer on point under cursor' clearly and concisely describes the main feature addition: a Blender-style focus/inspect capability. It accurately reflects the core change across the changeset.
Description check ✅ Passed The PR description follows the required template with all key sections completed: Summary, Changes (What, Breaking, Dependencies), Review Focus with critical design details, and a screenshot. All required information is present and thorough.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
End-To-End Regression Coverage For Fixes ✅ Passed PR uses "feat:" prefix indicating a feature addition, not a bug fix. Check applies only to PRs with bug-fix language; this is a feature PR.
Adr Compliance For Entity/Litegraph Changes ✅ Passed PR modifies only src/extensions/core/load3d/ files (3D viewer controls, not litegraph/ECS/graph entities), so ADR compliance check does not apply.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

🎨 Storybook: loading Building...

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

🎭 Playwright: ❌ 1633 passed, 1 failed · 3 flaky

❌ Failed Tests

📊 Browser Reports
  • chromium: View Report (✅ 1614 / ❌ 0 / ⚠️ 3 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 16 / ❌ 1 / ⚠️ 0 / ⏭️ 0)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/extensions/core/load3d/ControlsManager.ts`:
- Around line 52-57: When durationMs <= 0 the code updates this.controls.target,
this.camera.position and emits 'cameraChanged' but doesn't trigger a render;
update the immediate-path branch in ControlsManager (the block handling
durationMs <= 0) to request a frame after emitting cameraChanged by calling the
viewer render trigger—e.g. add this.eventManager.emitEvent('requestRender') (or,
if the codebase uses a different method to schedule a frame, call that like
this.viewer.requestRender()) right after
this.eventManager.emitEvent('cameraChanged', this.buildCameraState()) so the new
camera state is actually rendered.

In `@src/extensions/core/load3d/load3dFocusUnderCursor.ts`:
- Around line 80-137: Add unit tests for attachFocusUnderCursor covering: (1)
key gating — simulate keydown with 'f' and with modifier keys and with
deps.isDisabled returning true to assert focusOn is only called when allowed;
(2) pointer-to-NDC mapping — mock deps.canvas size and deps.getRenderRegion to
test getNDCFromPointer math (including letterboxed regions) by setting
pointerOnCanvas and triggering the keydown so raycaster.setFromCamera receives
the expected ndc; (3) raycast and focus behavior — stub deps.getModel,
deps.getCamera, and raycaster.intersectObject (or simulate hits) to confirm
deps.focusOn is called with hit.point and computeFocusDistance result; and (4)
cleanup — call the returned disposer and assert event listeners are removed (or
that AbortController.signal.aborted is true) so the
pointermove/pointerleave/keydown handlers stop firing; use the exported symbols
attachFocusUnderCursor, FocusUnderCursorDeps, getNDCFromPointer,
computeFocusDistance when wiring the tests.
- Around line 58-77: The NDC conversion at the end uses region.width and
region.height without checking for zero, which yields invalid coordinates for
raycasting; in the function that computes the focus under cursor (using canvas,
pointer, region and returning new THREE.Vector2), add a guard after the existing
bounds check to return null if region.width === 0 or region.height === 0 (or
otherwise handle zero-sized render regions) so you never perform the
normalization when dimensions are zero.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fd9e1a26-9015-45a4-80bb-21973fa8d731

📥 Commits

Reviewing files that changed from the base of the PR and between 91d2df4 and dbbd19c.

📒 Files selected for processing (5)
  • src/extensions/core/load3d/ControlsManager.test.ts
  • src/extensions/core/load3d/ControlsManager.ts
  • src/extensions/core/load3d/Load3d.ts
  • src/extensions/core/load3d/interfaces.ts
  • src/extensions/core/load3d/load3dFocusUnderCursor.ts

Comment thread src/extensions/core/load3d/ControlsManager.ts
Comment thread src/extensions/core/load3d/load3dFocusUnderCursor.ts
Comment thread src/extensions/core/load3d/load3dFocusUnderCursor.ts
@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 87.68116% with 17 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/extensions/core/load3d/Load3d.ts 13.33% 13 Missing ⚠️
src/extensions/core/load3d/ControlsManager.ts 95.16% 3 Missing ⚠️
...c/extensions/core/load3d/load3dFocusUnderCursor.ts 98.36% 1 Missing ⚠️
@@             Coverage Diff             @@
##             main   #12424       +/-   ##
===========================================
- Coverage   74.58%   60.16%   -14.42%     
===========================================
  Files        1529     1416      -113     
  Lines       92750    72581    -20169     
  Branches    25381    20165     -5216     
===========================================
- Hits        69176    43671    -25505     
- Misses      22725    28437     +5712     
+ Partials      849      473      -376     
Flag Coverage Δ
e2e ?
unit 60.16% <87.68%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...c/extensions/core/load3d/load3dFocusUnderCursor.ts 98.36% <98.36%> (ø)
src/extensions/core/load3d/ControlsManager.ts 95.34% <95.16%> (-1.63%) ⬇️
src/extensions/core/load3d/Load3d.ts 47.95% <13.33%> (-32.67%) ⬇️

... and 1028 files with indirect coverage changes

🚀 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.

@jtydhr88
Copy link
Copy Markdown
Collaborator

I will test it

@jtydhr88 jtydhr88 self-assigned this May 22, 2026
@jtydhr88
Copy link
Copy Markdown
Collaborator

as discussed on slack, I drafted proposal for node level keybinding https://www.notion.so/comfy-org/Node-Level-Keybindings-Design-Proposal-3696d73d365080ffa536eb315410abdb
we could wait for team feedback, and I would like make this PR as draft for now

@jtydhr88 jtydhr88 marked this pull request as draft May 23, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants