Skip to content

Add native touch rotation to FlyByCamera - #2943

Open
riccardobl with Copilot wants to merge 4 commits into
masterfrom
copilot/fix-mouse-keyboard-emulation-defaults
Open

riccardobl with Copilot wants to merge 4 commits into
masterfrom
copilot/fix-mouse-keyboard-emulation-defaults

Conversation

Copilot AI commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Android FlyByCamera rotation previously depended on emulated mouse events. With mouse emulation disabled by default, touch drag no longer rotates the camera.

  • Touch input

    • Register a TouchTrigger in FlyByCamera.
    • Handle touch MOVE events directly through TouchListener.
  • Rotation behavior

    • Convert touch deltas using the existing mouse-equivalent scale.
    • Preserve inverted-Y behavior.
    • Allow touch dragging when dragToRotate is enabled.
flyCam.onTouch("FLYCAM_Touch",
        new TouchEvent(TouchEvent.Type.MOVE, 0f, 0f, 128f, 0f), 0f);

Copilot AI and others added 2 commits August 30, 2026 08:43
Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com>
Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix mouse and keyboard emulation defaults in Android 3.10.0 Add native touch rotation to FlyByCamera Aug 30, 2026
Copilot AI requested a review from riccardobl August 30, 2026 08:45
@riccardobl
riccardobl marked this pull request as ready for review August 30, 2026 08:50
if (!enabled || !name.equals(FLYCAM_TOUCH) || event.getType() != TouchEvent.Type.MOVE) {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Multi-touch is not handled: AndroidTouchInput.onTouch() converts every pointer in an ACTION_MOVE into its own MOVE event, so with two fingers moving both deltas are applied here and the camera rotates at roughly double speed / jerks. The emulated-mouse path explicitly avoids this (generateMouseEvent() returns null when numPointers > 1), and TouchTrigger(TouchInput.ALL) delivers all pointers to this listener.

Suggested fix: latch the pointer that started the gesture on DOWN and ignore MOVE events from other pointers (DOWN/UP also have keyCode == 0, so they reach this mapping):

private int touchPointerId = -1;
...
case DOWN: if (touchPointerId == -1) touchPointerId = event.getPointerId(); break;
case UP:   if (event.getPointerId() == touchPointerId) touchPointerId = -1; break;
case MOVE: if (event.getPointerId() != touchPointerId) return;  // then rotate

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

add a code suggestion to fix this issue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Committable minimal fix: drop the secondary pointers before the deltas are applied.

Suggested change
// Only the initial (primary) finger may rotate the camera: AndroidTouchInput
// emits one MOVE event per pointer, so without this filter every finger's
// delta would reach the two rotateCamera() calls below.
if (event.getPointerId() != 0) {
return;
}

Android assigns pointer id 0 to the first finger of a gesture (event.getPointerId(getPointerIndex(event)) in AndroidTouchInput.onTouch), and TouchEvent defaults pointerId to 0, so existing callers and the current tests keep working. Trade-off: if the primary finger is lifted while a second finger keeps dragging, rotation pauses until the next gesture (a new DOWN with id 0).

The stricter variant described above (latch on DOWN, clear on UP, ignore other pointers) needs a second edit site that cannot be folded into this suggestion: the event.getType() != TouchEvent.Type.MOVE early return must be relaxed so DOWN/UP reach the handler, plus a private int touchPointerId = -1; field next to inputMappingsRegistered. Both hunks are non-adjacent, so a single-suggestion apply is not possible for that version.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

propose both changes with two different suggestions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both hunks of the stricter variant, applied together (the switch will not compile without the field). This supersedes the minimal pointerId != 0 variant.

1. State — applied at the field declaration (line 127, next to inputMappingsRegistered):

Suggested change
private boolean inputMappingsRegistered;
private int touchPointerId = -1;

2. Guard/dispatch — replaces lines 580–582:

Suggested change
if (!enabled || !name.equals(FLYCAM_TOUCH)) {
return;
}
switch (event.getType()) {
case DOWN:
if (touchPointerId == -1) {
touchPointerId = event.getPointerId();
}
return;
case UP:
if (event.getPointerId() == touchPointerId) {
touchPointerId = -1;
}
return;
case MOVE:
if (event.getPointerId() != touchPointerId) {
return;
}
break;
default:
return;
}

InputManager.onTouchEventQueued() dispatches on evt.getKeyCode(), which is 0 for every DOWN/UP/MOVE/gesture event, so all of them reach the FLYCAM_Touch mapping; the switch is what separates them. Only the finger latched on DOWN rotates (Android assigns pointer id 0 to the first finger of a gesture); if that finger lifts while a second one keeps dragging, rotation pauses until the next DOWN.

Since this thread is anchored at line 583, block 1 has to be applied at the field declaration, not here.

new TouchEvent(TouchEvent.Type.MOVE, 0f, 0f, 128f, 0f), 0f);

assertFalse(initialDirection.equals(camera.getDirection()));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These tests invoke onTouch() directly, so they pass even if the FLYCAM_Touch mapping, the TouchTrigger, or the TouchListener registration were missing — the wiring this PR fixes is untested, and the direction/scale of the rotation is not asserted either.

Drive the event through the input pipeline instead, e.g. build an InputManager (dummy mouse/key inputs), call flyCam.registerWithInput(inputManager), then inputManager.onTouchEventQueued(new TouchEvent(TouchEvent.Type.MOVE, 0f, 0f, 128f, 0f)) and assert the sign of the resulting yaw change. That also gives you a place to cover the multi-touch case and the non-MOVE event filter.

Also note touchDragRotatesCameraWhenDragToRotateIsEnabled currently asserts exactly the same thing as touchDragRotatesCamera.

@jaime-jmebot jaime-jmebot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The rotation math itself is consistent with the mouse path: TOUCH_ROTATION_SCALE = 1/1024 matches InputManager's abs(dx)/1024, and the signs (yaw on initialUpVec, pitch on getLeft(), invertY handling) mirror FLYCAM_LEFT/RIGHT/UP/DOWN. The two blocking items are the multi-touch handling and the missing coverage of the wiring (see inline comments).

  • Multi-touch: Android emits one MOVE per pointer, so all finger deltas are applied to the rotation; the emulated-mouse path guards against exactly this.
  • Touch tests bypass InputManager, so they would pass even with a broken/missing FLYCAM_Touch mapping; they also assert nothing about direction or scale.
  • rotateCamera(..., true) bypasses the dragToRotate && !canRotate gate, so touch rotation is always active, regardless of dragToRotate. If that is intended ("touch drag is the drag"), please document it in the class javadoc (the Controls: list still mentions mouse only) and in setDragToRotate(); the PR description ("allow touch dragging when dragToRotate is enabled") reads as if the gate were applied.
  • If mouse emulation is enabled again (AppSettings.isEmulateMouse() / InputManager.isSimulateMouse()), a single-finger drag produces both a MouseMotionEvent and a TouchEvent, so the camera rotates twice as fast. Worth guarding (e.g. skip touch rotation while inputManager.isSimulateMouse()).

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.

Android: Mouse and Keyboard are no longer emulated by default in 3.10.0

3 participants