Skip to content

Switch TD view to perspective camera#9281

Open
hotzenklotz wants to merge 25 commits into
masterfrom
perspective-camera
Open

Switch TD view to perspective camera#9281
hotzenklotz wants to merge 25 commits into
masterfrom
perspective-camera

Conversation

@hotzenklotz
Copy link
Copy Markdown
Member

@hotzenklotz hotzenklotz commented Feb 9, 2026

Summary

  • convert the TD viewport camera to use a perspective frustum and adjust controller logic for pan/zoom handling
  • normalize trackball controls, store target data, and ensure camera updates account for target offsets and aspect ratio
  • default TD view state now stores a target and saves up/down fields to reconstruct the view direction
  • added a switch to the 3D viewport options to toggle between perspective and orthographic camera
    • image

URL of deployed dev instance (used for testing):

https://perspectivecamera.webknossos.xyz/

Testing

TODO

  • Decide if ortho or perspective camera should be the default
  • Follow Up: Use an icon switcher instead of antd <Switch> with text for the settings UI.

Issues:

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

Broad change to support both OrthographicCamera and PerspectiveCamera across viewer code: trackball controls, camera controller, TD controller, views, store types, and default state. Adds explicit camera target tracking and updates TD view projection/near handling for perspective cameras.

Changes

Cohort / File(s) Summary
Trackball & Camera Types
frontend/javascripts/libs/trackball_controls.ts, frontend/javascripts/viewer/store.ts
TrackballControls now accepts `OrthographicCamera
Camera Controller
frontend/javascripts/viewer/controller/camera_controller.ts
Widened cameras map to include PerspectiveCamera; added getTDCameraTarget and tdViewDiagonalDatasetExtent; TD near/frustum handling updated to support perspective off‑axis projection and keep TD target synced with other viewports.
TD Controller
frontend/javascripts/viewer/controller/td_controller.tsx
cameras prop widened to include PerspectiveCamera; removed exported threeCameraToCameraData, added private objToArr; dispatches now include explicit target; uses TrackballControls.lastTarget to stabilize external target updates.
Views & Initialization
frontend/javascripts/viewer/view/plane_view.ts, frontend/javascripts/viewer/view/arbitrary_view.ts
Cameras maps and return types widened to include PerspectiveCamera; TD view now uses PerspectiveCamera where appropriate; createDirLight accepts generic Camera.
Default State
frontend/javascripts/viewer/default_state.ts
Added tdCamera.target: [0,0,0] to default plane view TD camera state.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

rendering, new feature

Suggested reviewers

  • MichaelBuessemeyer
  • philippotto

Poem

🐰 I hopped from ortho to perspective bright,
I nudged the targets so views align right.
Frustums adjusted, near planes in tune,
Controls remember where views swoon —
A tiny rabbit cheers: hop, camera, swoon! 📷✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main change: converting the TD view camera from orthographic to perspective projection.
Description check ✅ Passed The PR description is directly related to the changeset, describing conversion of TD viewport to perspective camera, trackball control normalization, and target data storage.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perspective-camera

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.

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: 1

🤖 Fix all issues with AI agents
In `@frontend/javascripts/viewer/controller/camera_controller.ts`:
- Around line 121-127: getTDCameraTarget's null check is ineffective because
CameraData.target is declared non-nullable; either make CameraData.target
nullable (Vector3 | null) and update deserialization/other uses so the existing
null check in getTDCameraTarget remains valid, or keep the type and replace the
null check with a meaningful-value check (e.g., add an isZeroVector(target:
Vector3) helper and use it inside getTDCameraTarget) to detect legacy/empty
targets and then fall back to voxelToUnit(state.dataset.dataSource.scale,
getPosition(state.flycam)). Ensure to update all places that construct or read
CameraData if you change the type.
🧹 Nitpick comments (4)
frontend/javascripts/viewer/controller/camera_controller.ts (2)

204-231: The zero-height early return dispatches a no-op action.

When oldHeight === 0 (line 213), the dispatch on lines 214–220 writes back the same left/right values already in the store, which is effectively a no-op. If the intent is simply to bail out, a plain return would be clearer and avoid the unnecessary dispatch + state update cycle.

♻️ Simplify early return
       if (oldHeight === 0) {
-        Store.dispatch(
-          setTDCameraWithoutTimeTrackingAction({
-            left: tdCameraData.left,
-            right: tdCameraData.right,
-          }),
-        );
         return;
       }

87-127: getCameraBasis is duplicated across files with different signatures and purposes.

Two getCameraBasis functions exist:

  • camera_controller.ts (line 91): Takes Vector3 tuples (position, target, up) and returns a basis computed from these vectors
  • td_controller.tsx (line 47): Takes a Three.js OrthographicCamera | PerspectiveCamera and extracts the basis from the camera's state

While they compute related concepts, the different input types and contexts make unification challenging without adding abstraction complexity. If extracting shared logic becomes valuable, consider a third utility function for the core math with separate wrapper functions for each context.

frontend/javascripts/viewer/controller/td_controller.tsx (2)

186-196: Defensive fallback for target is unnecessary given the type.

Line 193: cameraData.target || [0, 0, 0]CameraData.target is Vector3 (required, non-nullable), so the || [0, 0, 0] fallback can never trigger. Same observation as in camera_controller.ts's getTDCameraTarget.

This is harmless but adds noise. Consider removing the fallback or, if guarding against legacy state, making the type explicitly nullable.


334-368: setTargetAndFixPosition still uses Euler-based rotation inversion with @ts-expect-error suppression.

Lines 361–366 cast a Vector3 (from camera.rotation.clone()) to work with Euler-like properties (order). This was pre-existing tech debt, but now that the camera can be a PerspectiveCamera, the rotation representation is the same (both use Euler internally), so the behavior is unchanged. The ts-expect-error comments remain a maintenance risk.

Comment thread frontend/javascripts/viewer/controller/camera_controller.ts
@hotzenklotz hotzenklotz self-assigned this Feb 10, 2026
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.

🧹 Nitpick comments (1)
frontend/javascripts/viewer/store.ts (1)

469-479: Consider replacing PartialCameraData with Partial<CameraData>.

PartialCameraData is a manual mirror of CameraData with every field made optional, which creates a drift risk — demonstrated by this PR requiring two symmetric edits instead of one. Using Partial<CameraData> would automatically stay in sync with future CameraData changes.

♻️ Proposed simplification
-export type PartialCameraData = {
-  readonly near?: number;
-  readonly far?: number;
-  readonly left?: number;
-  readonly right?: number;
-  readonly top?: number;
-  readonly bottom?: number;
-  readonly up?: Vector3;
-  readonly position?: Vector3;
-  readonly target?: Vector3;
-};
+export type PartialCameraData = Partial<CameraData>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/javascripts/viewer/store.ts` around lines 469 - 479, Replace the
manual mirror type PartialCameraData with a built-in Partial utility so it stays
in sync with CameraData: change the export type declaration to "export type
PartialCameraData = Partial<CameraData>;" (remove the explicit field list),
ensure CameraData is in scope/imported, and keep the same exported symbol name
(PartialCameraData) so callers don't need changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@frontend/javascripts/viewer/store.ts`:
- Around line 469-479: Replace the manual mirror type PartialCameraData with a
built-in Partial utility so it stays in sync with CameraData: change the export
type declaration to "export type PartialCameraData = Partial<CameraData>;"
(remove the explicit field list), ensure CameraData is in scope/imported, and
keep the same exported symbol name (PartialCameraData) so callers don't need
changes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19cbdf7 and ee97dd6.

📒 Files selected for processing (2)
  • frontend/javascripts/viewer/store.ts
  • frontend/javascripts/viewer/view/plane_view.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/viewer/view/plane_view.ts

@hotzenklotz
Copy link
Copy Markdown
Member Author

@Albane-dlv Norman suggested to use icons with buttons to switch between orthographic and perspective cameras. Can you please design two icons for that.

Blender:
image
image

Other inspiration:
image

@hotzenklotz hotzenklotz requested a review from philippotto April 21, 2026 08:58
Copy link
Copy Markdown
Member

@philippotto philippotto left a comment

Choose a reason for hiding this comment

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

cool stuff 💯

Comment thread frontend/javascripts/viewer/view/arbitrary_view.ts Outdated
Comment thread frontend/javascripts/viewer/controller/camera_controller.ts
Comment thread frontend/javascripts/viewer/controller/camera_controller.ts
Comment thread frontend/javascripts/viewer/controller/camera_controller.ts Outdated
Comment thread frontend/javascripts/viewer/controller/camera_controller.ts Outdated
Comment thread frontend/javascripts/viewer/controller/td_controller.tsx
Comment thread frontend/javascripts/viewer/view/plane_view.ts Outdated
Comment thread frontend/javascripts/viewer/view/plane_view.ts Outdated
hotzenklotz and others added 5 commits April 24, 2026 08:44
Co-authored-by: Philipp Otto <philippotto@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
@hotzenklotz hotzenklotz requested a review from philippotto April 24, 2026 08:13
@philippotto
Copy link
Copy Markdown
Member

unfortunately, rotating is buggy in the perspective mode. happens always when releasing the right-mouse. sometimes, the jump is subtle, sometimes severe. ortho mode is fine.

buggy-rotation-in-perspective-cam.webm

@hotzenklotz
Copy link
Copy Markdown
Member Author

@philippotto I addressed the camera jitter issue after SHIFT + left clicking on a mesh.

@philippotto
Copy link
Copy Markdown
Member

buggy-rotation-in-perspective-cam-2.webm

I can still reproduce the same issue (dev instance was up to date). unfortunately, the mouse isn't visible in the screencast, but I only do shift+click and then rotate. most of the time, the jump is subtle, but sometimes it's jarring (in the video at the very end).

not sure how annoying this really will be in production. maybe we should merge this but not make it a default for everyone just yet? instead, we could internally guinea pig this in the team for a while and then switch everyone over in a month?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide Perspective Camera for 3D viewport

2 participants