Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 70 additions & 10 deletions packages/ui/src/component/UICanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export class UICanvas extends Component implements IElement {
@ignoreClone
private _renderCamera: Camera;
@ignoreClone
private _eventCamera: Camera | null = null;
@ignoreClone
private _cameraObserver: Camera;
@assignmentClone
private _resolutionAdaptationMode = ResolutionAdaptationMode.HeightAdaptation;
Expand Down Expand Up @@ -140,6 +142,11 @@ export class UICanvas extends Component implements IElement {
const preMode = this._renderMode;
if (preMode !== mode) {
this._renderMode = mode;
// Clear eventCamera when renderMode changes away from WorldSpace
if (preMode === CanvasRenderMode.WorldSpace && 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());
}
Expand Down Expand Up @@ -176,6 +183,31 @@ export class UICanvas extends Component implements IElement {
}
}

/**
* The camera used for event detection in `WorldSpace` mode.
* @remarks If not set, all cameras will be used for event detection. Only effective in `WorldSpace` render mode.
*/
get eventCamera(): Camera | null {
return this._eventCamera;
}

set eventCamera(value: Camera | null) {
const preEventCamera = this._eventCamera;
if (preEventCamera !== value) {
if (value && this._renderMode !== CanvasRenderMode.WorldSpace) {
Logger.warn(
"EventCamera is only effective in WorldSpace render mode. Current render mode is not WorldSpace."
);
}
value &&
this._isSameOrChildEntity(value.entity) &&
Logger.warn(
"Event camera entity matching or nested within the canvas entity may cause unexpected behavior in WorldSpace mode."
);
this._eventCamera = value;
}
}

/**
* The screen resolution adaptation mode of the UI canvas in `ScreenSpaceCamera` and `ScreenSpaceOverlay` mode.
*/
Expand Down Expand Up @@ -277,6 +309,25 @@ export class UICanvas extends Component implements IElement {
return this._renderMode !== CanvasRenderMode.ScreenSpaceCamera || this._renderCamera === camera;
}

/**
* Check if this camera can process UI events for this canvas
* @internal
*/
_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;
}
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


/**
* @internal
*/
Expand Down Expand Up @@ -389,17 +440,26 @@ export class UICanvas extends Component implements IElement {
*/
_cloneTo(target: UICanvas, srcRoot: Entity, targetRoot: Entity): void {
target.renderMode = this._renderMode;
const renderCamera = this._renderCamera;
if (renderCamera) {
const paths = UICanvas._targetTempPath;
// @ts-ignore
const success = Entity._getEntityHierarchyPath(srcRoot, renderCamera.entity, paths);
// @ts-ignore
target.renderCamera = success
? // @ts-ignore
Entity._getEntityByHierarchyPath(targetRoot, paths).getComponent(Camera)
: renderCamera;
target.renderCamera = this._cloneCamera(this._renderCamera, srcRoot, targetRoot);
target.eventCamera = this._cloneCamera(this._eventCamera, srcRoot, targetRoot);
}

/**
* @internal
* Clone camera reference for entity cloning
*/
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 _getRenderers(): UIRenderer[] {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/input/UIPointerEventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class UIPointerEventEmitter extends PointerEventEmitter {
// Post-rendering first detection
for (let k = 0, n = canvasElements.length; k < n; k++) {
const canvas = canvasElements.get(k);
if (!canvas._canRender(camera)) continue;
if (!canvas._canProcessEvent(camera)) continue;
if (canvas._raycast(ray, hitResult, farClipPlane)) {
this._updateRaycast((<UIHitResult>hitResult).component, pointer);
return;
Expand Down
71 changes: 71 additions & 0 deletions tests/src/ui/UICanvas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,75 @@ describe("UICanvas", async () => {
const anoCloneCanvas = anoCloneEntity.getComponent(UICanvas);
expect(anoCloneCanvas.renderCamera).to.eq(anoCloneEntity.findByName("camera").getComponent(Camera));
});

it("EventCamera for WorldSpace", () => {
rootCanvas.renderMode = CanvasRenderMode.WorldSpace;

// @ts-ignore
expect(rootCanvas.eventCamera).to.be.null;

// @ts-ignore
rootCanvas.eventCamera = camera;
// @ts-ignore
expect(rootCanvas.eventCamera).to.eq(camera);

// @ts-ignore
rootCanvas.eventCamera = null;
// @ts-ignore
expect(rootCanvas.eventCamera).to.be.null;
});

it("_canProcessEvent with eventCamera in WorldSpace", () => {
const camera2 = root.createChild("camera3").addComponent(Camera);

rootCanvas.renderMode = CanvasRenderMode.WorldSpace;
// @ts-ignore
rootCanvas.eventCamera = camera;

// @ts-ignore
expect(rootCanvas._canProcessEvent(camera)).to.be.true;
// @ts-ignore
expect(rootCanvas._canProcessEvent(camera2)).to.be.false;

// @ts-ignore
rootCanvas.eventCamera = null;
// @ts-ignore
expect(rootCanvas._canProcessEvent(camera)).to.be.true;
// @ts-ignore
expect(rootCanvas._canProcessEvent(camera2)).to.be.true;
});

it("Clone with eventCamera", () => {
rootCanvas.renderMode = CanvasRenderMode.WorldSpace;
// @ts-ignore
rootCanvas.eventCamera = camera;

const cloneEntity = canvasEntity.clone();
const cloneCanvas = cloneEntity.getComponent(UICanvas);

// @ts-ignore
expect(cloneCanvas.eventCamera).to.eq(camera);

const localEventCamera = canvasEntity.createChild('eventCamera').addComponent(Camera);
// @ts-ignore
rootCanvas.eventCamera = localEventCamera;

const localCloneEntity = canvasEntity.clone();
const localCloneCanvas = localCloneEntity.getComponent(UICanvas);

// @ts-ignore
expect(localCloneCanvas.eventCamera).to.eq(localCloneEntity.findByName("eventCamera").getComponent(Camera));
});

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;
});
});