Skip to content

Conversation

@ym2050
Copy link

@ym2050 ym2050 commented Dec 11, 2025

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

What kind of change does this PR introduce?

Fix

  • 为 WorldSpace 渲染模式的 UICanvas 新增事件相机设置功能

What is the current behavior?

Closes #2793

当前在 WorldSpace 模式下,场景中存在多个相机时,所有相机都可以触发 UI 事件,无法指定特定相机处理交互,导致多相机场景下的事件处理歧义。

What is the new behavior?

  • 新增 eventCamera 属性:允许为 WorldSpace 模式的 UICanvas 指定事件处理相机
  • 事件过滤机制:通过 _canProcessEvent() 方法实现基于相机的事件过滤
  • 向后兼容:未设置 eventCamera 时保持原有行为

Does this PR introduce a breaking change?

- 完全向后兼容,现有代码无需修改

eventCamera.mov

Summary by CodeRabbit

  • New Features

    • WorldSpace canvases can have a dedicated event camera for UI event detection.
    • Canvas cloning preserves event camera references when present.
  • Behavior

    • Pointer event processing now follows canvas-specific event-camera rules; ScreenSpaceCamera uses its render camera, WorldSpace can restrict to the event camera.
    • Setting an event camera logs warnings if misused and is cleared when render mode changes.
  • Tests

    • Added tests for event-camera behavior, processing eligibility across render modes, and cloning.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 11, 2025

Walkthrough

Adds eventCamera support to UICanvas (storage, getter/setter, validation), a per-camera eligibility check (_canProcessEvent), propagation of eventCamera during cloning, and updates UIPointerEventEmitter to use the new eligibility predicate; tests added to validate behaviors (some duplicated).

Changes

Cohort / File(s) Summary
Event camera core implementation
packages/ui/src/component/UICanvas.ts
Added private `_eventCamera: Camera
Event processing integration
packages/ui/src/input/UIPointerEventEmitter.ts
Replaced eligibility check canvas._canRender(camera)canvas._canProcessEvent(camera) in processRaycast so pointer raycasts respect the new eventCamera rules.
Tests (added / duplicated)
tests/src/ui/UICanvas.test.ts
Added tests covering setting/clearing eventCamera in WorldSpace, _canProcessEvent behavior for matching/non-matching cameras and ScreenSpaceCamera mode, and cloning preserving eventCamera; test blocks appear duplicated in the file.

Sequence Diagram(s)

sequenceDiagram
    participant Pointer as Pointer Input
    participant Emitter as UIPointerEventEmitter
    participant Canvas as UICanvas
    participant Camera as Camera

    Pointer->>Emitter: deliver pointer event (with Camera)
    Emitter->>Emitter: processRaycast(camera)
    loop for each canvas
        Emitter->>Canvas: ask _canProcessEvent(camera)?
        alt Canvas.renderMode == WorldSpace and Canvas.eventCamera != null
            Canvas->>Camera: compare to eventCamera
            Camera-->>Canvas: matches / not matches
        else Canvas.renderMode == ScreenSpaceCamera and Canvas.renderCamera != null
            Canvas->>Camera: compare to renderCamera
            Camera-->>Canvas: matches / not matches
        else
            Canvas-->>Emitter: eligible
        end
        alt eligible
            Emitter->>Canvas: perform raycast & dispatch pointer events
            Canvas-->>Emitter: hit (may return early) / miss
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect _canProcessEvent logic across render modes and edge cases (null cameras, nested entities).
  • Verify _cloneTo and _cloneCamera correctly resolve entity path mapping and fallback behavior.
  • Confirm UIPointerEventEmitter preserves raycast ordering and early-return semantics.
  • Remove duplicated test blocks in tests/src/ui/UICanvas.test.ts.

Poem

🐰 I nudged a lens into the light,
WorldSpace now knows which camera’s right.
Rays hop true where hop should be,
Events land sweet, as quick as me. 🥕✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding eventCamera support for WorldSpace UICanvas, directly addressing the core objective of the PR.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from issue #2793: allowing specification of which camera emits rays for UI events in WorldSpace mode, with proper event filtering and backward compatibility.
Out of Scope Changes check ✅ Passed All changes directly support the objective of adding eventCamera support: the new eventCamera property, _canProcessEvent filtering, camera cloning logic, and test coverage are all in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@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: 0

🧹 Nitpick comments (3)
packages/ui/src/component/UICanvas.ts (1)

181-204: Consider clearing eventCamera when renderMode changes away from WorldSpace.

The setter correctly warns when eventCamera is set in non-WorldSpace mode, but the eventCamera value persists. If a user sets eventCamera while in WorldSpace, then later changes renderMode to ScreenSpaceCamera, the eventCamera reference remains set (though ineffective). This could cause confusion when switching back to WorldSpace.

Consider clearing eventCamera when renderMode changes, or documenting this behavior explicitly. For reference, here's how the renderMode setter could be updated:

 set renderMode(mode: CanvasRenderMode) {
   const preMode = this._renderMode;
   if (preMode !== mode) {
     this._renderMode = mode;
+    if (mode !== CanvasRenderMode.WorldSpace && this._eventCamera) {
+      Logger.warn("EventCamera has been cleared because render mode is no longer WorldSpace.");
+      this._eventCamera = null;
+    }
     this._updateCameraObserver();
     this._setRealRenderMode(this._getRealRenderMode());
   }
 }
tests/src/ui/UICanvas.test.ts (2)

304-321: Unused variable camera2 in test.

The camera2 variable is created but never used in this test. Consider removing it or adding assertions that validate behavior with multiple cameras.

 it("EventCamera for WorldSpace", () => {
-    const camera2 = root.createChild("camera2").addComponent(Camera);
-    
     rootCanvas.renderMode = CanvasRenderMode.WorldSpace;

323-341: Consider adding test coverage for ScreenSpaceCamera mode.

The test validates WorldSpace behavior well, but _canProcessEvent also handles ScreenSpaceCamera mode (returning renderCamera === camera). Adding a test case for ScreenSpaceCamera mode would improve coverage and document the expected behavior.

Example additional test:

it("_canProcessEvent with ScreenSpaceCamera mode", () => {
  const camera2 = root.createChild("cameraSSC").addComponent(Camera);
  
  rootCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera;
  rootCanvas.renderCamera = camera;
  
  // @ts-ignore
  expect(rootCanvas._canProcessEvent(camera)).to.be.true;
  // @ts-ignore
  expect(rootCanvas._canProcessEvent(camera2)).to.be.false;
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01a56df and 5b8bad3.

📒 Files selected for processing (3)
  • packages/ui/src/component/UICanvas.ts (4 hunks)
  • packages/ui/src/input/UIPointerEventEmitter.ts (1 hunks)
  • tests/src/ui/UICanvas.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/src/ui/UICanvas.test.ts (3)
packages/core/src/index.ts (1)
  • Camera (16-16)
packages/ui/src/index.ts (1)
  • CanvasRenderMode (21-21)
packages/ui/src/component/index.ts (1)
  • UICanvas (1-1)
🔇 Additional comments (5)
packages/ui/src/component/UICanvas.ts (3)

84-86: LGTM! Field declaration follows established patterns.

The @ignoreClone decorator is correctly applied since the eventCamera is manually handled in _cloneTo, consistent with how _renderCamera is managed.


307-319: Logic is correct and handles all render modes appropriately.

The _canProcessEvent method correctly:

  1. Returns renderCamera === camera for ScreenSpaceCamera mode (consistent with _canRender)
  2. Filters by eventCamera in WorldSpace when set, otherwise allows all cameras
  3. Returns true for ScreenSpaceOverlay (implicit via the final return true)

However, there's a subtle difference from _canRender: ScreenSpaceOverlay is not explicitly handled here, but since Overlay canvases are processed separately in UIPointerEventEmitter.processRaycast (lines 44-54), this is acceptable.


444-454: Cloning logic correctly mirrors the renderCamera pattern.

The implementation properly resolves the eventCamera's entity within the cloned hierarchy when possible, falling back to the original reference otherwise. This ensures cloned canvases with local event cameras work correctly.

packages/ui/src/input/UIPointerEventEmitter.ts (1)

85-92: Correct integration of the new event processing predicate.

The change from _canRender(camera) to _canProcessEvent(camera) properly separates rendering eligibility from event processing eligibility. This allows WorldSpace canvases to filter which cameras can generate pointer events while _canRender continues to control rendering behavior.

tests/src/ui/UICanvas.test.ts (1)

343-363: Test correctly validates cloning behavior for eventCamera.

The test verifies both external camera references and local (child entity) camera references are properly handled during cloning, which matches the implementation in _cloneTo.

@ym2050
Copy link
Author

ym2050 commented Dec 11, 2025

#2793

return this._eventCamera === camera;
}
return true;
}
Copy link
Member

Choose a reason for hiding this comment

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

这里有点绕,比较通顺的逻辑应该是先判断 event Camera 相关

Copy link
Author

Choose a reason for hiding this comment

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

已经重新组织了 _canProcessEvent 方法,请看最新提交

Copy link
Member

Choose a reason for hiding this comment

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

+1

Copy link

@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: 0

🧹 Nitpick comments (1)
packages/ui/src/component/UICanvas.ts (1)

307-325: Consider basing _canProcessEvent on _realRenderMode and unifying comment language

Right now _canProcessEvent branches on this._renderMode. However, _getRealRenderMode() can downgrade ScreenSpaceCamera to ScreenSpaceOverlay when renderCamera is missing/disabled. In that fallback, the canvas will render as overlay but _canProcessEvent will still behave as if it were ScreenSpaceCamera and return false for all cameras because _renderCamera is null. If the intent is for pointer events to follow the actual effective render mode, using _realRenderMode (with a None fallback to _renderMode) would be more consistent.

For example:

-  _canProcessEvent(camera: Camera): boolean {
-    // WorldSpace模式下,如果设置了eventCamera,则只允许该相机处理事件
-    if (this._renderMode === CanvasRenderMode.WorldSpace && this._eventCamera) {
+  _canProcessEvent(camera: Camera): boolean {
+    const mode =
+      this._realRenderMode !== CanvasRealRenderMode.None ? this._realRenderMode : this._renderMode;
+
+    // In WorldSpace, if eventCamera is set, only that camera can process events.
+    if (mode === CanvasRenderMode.WorldSpace && this._eventCamera) {
       return this._eventCamera === camera;
     }
-    // ScreenSpaceCamera模式下,只允许renderCamera处理事件(与_canRender保持一致)
-    if (this._renderMode === CanvasRenderMode.ScreenSpaceCamera) {
+    // In ScreenSpaceCamera, only renderCamera can process events (consistent with _canRender).
+    if (mode === CanvasRenderMode.ScreenSpaceCamera) {
       return this._renderCamera === camera;
     }
-    // ScreenSpaceOverlay模式和WorldSpace未设置eventCamera时,所有相机都可以处理事件
+    // In ScreenSpaceOverlay and WorldSpace without eventCamera, all cameras can process events.
     return true;
   }

This also replaces the mixed Chinese comments with English, which keeps the file’s comment language consistent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b8bad3 and 02e5553.

📒 Files selected for processing (1)
  • packages/ui/src/component/UICanvas.ts (4 hunks)
🔇 Additional comments (3)
packages/ui/src/component/UICanvas.ts (3)

85-87: Initialization and cloning flags for _eventCamera are consistent with existing camera fields

_eventCamera is initialized to null and marked with @ignoreClone, with cloning handled explicitly in _cloneTo, mirroring how _renderCamera is treated. This is a clean, predictable way to manage the optional reference.


181-205: eventCamera accessor semantics match the documented behavior

The getter/setter and warnings align with the documentation: the camera is only effective in WorldSpace, but the value is still stored (so it starts working immediately if renderMode is later switched to WorldSpace), and entity nesting checks mirror renderCamera’s validation. No functional issues spotted here.


449-459: Event camera cloning logic correctly mirrors renderCamera handling

The _cloneTo block for eventCamera reuses _targetTempPath and Entity._getEntityHierarchyPath/_getEntityByHierarchyPath in the same way as renderCamera, with a safe fallback to the original eventCamera reference when the hierarchy lookup fails. This is a solid, predictable cloning strategy.

}

// ScreenSpaceCamera mode: only renderCamera can process events (consistent with _canRender)
if (this._renderMode === CanvasRenderMode.ScreenSpaceCamera) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this._renderMode === CanvasRenderMode.ScreenSpaceCamera && this._renderCamara

Entity._getEntityByHierarchyPath(targetRoot, paths).getComponent(Camera)
: renderCamera;
}
const eventCamera = this._eventCamera;
Copy link
Collaborator

Choose a reason for hiding this comment

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

这两个 if 大部分代码都相同,可以试试复用一下。

Copy link
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Choose a reason for hiding this comment

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

质量很好的 PR,有两个小建议可以修复一下。

- Extract _cloneCamera method to reduce code duplication
- Add null check for _renderCamera in _canProcessEvent
Copy link

@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: 0

🧹 Nitpick comments (3)
packages/ui/src/component/UICanvas.ts (3)

85-87: eventCamera lifecycle and warnings look good; minor optional behavior tweak

The _eventCamera field, its getter/setter, and the clearing logic in renderMode when leaving WorldSpace together match the intended behavior and keep existing setups unchanged when eventCamera is unset. This looks solid.

One small behavioral nit: eventCamera currently logs a warning when set while renderMode is not WorldSpace but still accepts and stores the value. If you expect users to often “preconfigure” eventCamera before switching renderMode to WorldSpace, this warning may be a bit noisy; you might consider either:

  • Downgrading it (e.g., to an info/debug‑level log), or
  • Only warning when leaving WorldSpace while eventCamera is set (you already do this in the renderMode setter).

Not required for correctness, just a usability consideration.

Also applies to: 141-149, 186-209


308-310: Align _canProcessEvent with _realRenderMode to match ScreenSpaceCamera → Overlay fallback

_canProcessEvent currently branches on _renderMode and _renderCamera. When renderMode === ScreenSpaceCamera but the render camera is effectively not usable (e.g., disabled), _getRealRenderMode() downgrades the canvas to ScreenSpaceOverlay, but _canProcessEvent will still only allow events from _renderCamera. That can lead to a situation where the canvas renders as overlay but never receives events from active cameras.

To make event routing follow the actual mode (as used by _prepareRender and _updateSortDistance), consider basing the decision on _realRenderMode instead of _renderMode:

-  _canProcessEvent(camera: Camera): boolean {
-    // WorldSpace mode: if eventCamera is set, only that camera can process events
-    if (this._renderMode === CanvasRenderMode.WorldSpace && this._eventCamera) {
-      return this._eventCamera === camera;
-    }
-    
-    // ScreenSpaceCamera mode: only renderCamera can process events (consistent with _canRender)
-    if (this._renderMode === CanvasRenderMode.ScreenSpaceCamera && this._renderCamera) {
-      return this._renderCamera === camera;
-    }
-    
-    // ScreenSpaceOverlay mode and WorldSpace without eventCamera: all cameras can process events
-    return true;
-  }
+  _canProcessEvent(camera: Camera): boolean {
+    const mode = this._realRenderMode;
+
+    // WorldSpace mode: if eventCamera is set, only that camera can process events
+    if (mode === CanvasRenderMode.WorldSpace && this._eventCamera) {
+      return this._eventCamera === camera;
+    }
+
+    // ScreenSpaceCamera mode: only the active renderCamera can process events
+    if (mode === CanvasRenderMode.ScreenSpaceCamera && this._renderCamera) {
+      return this._renderCamera === camera;
+    }
+
+    // ScreenSpaceOverlay, WorldSpace without eventCamera, or None: all cameras can process events
+    return true;
+  }

This keeps the new eventCamera behavior but avoids divergence between “real” render mode and event routing in ScreenSpaceCamera fallback cases.

Also applies to: 312-329, 705-712


441-463: Make _cloneCamera null‑aware and type‑safe for both renderCamera and eventCamera

_cloneCamera is typed as taking and returning Camera, but:

  • It is called with this._eventCamera, which is Camera | null.
  • Its implementation already handles a falsy camera (if (!camera) return camera;), so the signature doesn’t reflect actual usage.
  • If Entity._getEntityByHierarchyPath or getComponent(Camera) fail, you can end up returning undefined/null where a Camera is expected.

To better match usage and avoid future typing issues (especially if strictNullChecks is enabled), you can make _cloneCamera generic over Camera | null and be defensive about missing targets:

-  private _cloneCamera(camera: Camera, srcRoot: Entity, targetRoot: Entity): Camera {
-    if (!camera) {
-      return camera;
-    }
-    const paths = UICanvas._targetTempPath;
-    // @ts-ignore
-    const success = Entity._getEntityHierarchyPath(srcRoot, camera.entity, paths);
-    // @ts-ignore
-    return success
-      ? // @ts-ignore
-        Entity._getEntityByHierarchyPath(targetRoot, paths).getComponent(Camera)
-      : camera;
-  }
+  private _cloneCamera<T extends Camera | null>(camera: T, srcRoot: Entity, targetRoot: Entity): T {
+    if (!camera) {
+      return camera;
+    }
+    const paths = UICanvas._targetTempPath;
+    // @ts-ignore
+    const success = Entity._getEntityHierarchyPath(srcRoot, camera.entity, paths);
+    if (!success) {
+      return camera;
+    }
+    // @ts-ignore
+    const targetEntity = Entity._getEntityByHierarchyPath(targetRoot, paths);
+    const targetCamera = targetEntity?.getComponent(Camera) as T | null;
+    return (targetCamera ?? camera) as T;
+  }

With this, target.renderCamera = this._cloneCamera(this._renderCamera, ...) infers T = Camera, and target.eventCamera = this._cloneCamera(this._eventCamera, ...) infers T = Camera | null, matching the property types and avoiding implicit any/null leakage.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c2cc47 and 4bda744.

📒 Files selected for processing (1)
  • packages/ui/src/component/UICanvas.ts (5 hunks)

cptbtptpbcptdtptp

This comment was marked as resolved.

Copy link
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Choose a reason for hiding this comment

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

+1

return this._eventCamera === camera;
}
return true;
}
Copy link
Member

Choose a reason for hiding this comment

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

+1

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.

若 UICanvas 的渲染模式设置为 WorldSpace,需新增事件触发相机的设置

3 participants