Added 2 finger camera pan, zoom, and rotate

This commit is contained in:
Yousef Khadadeh 2026-03-05 12:03:10 +03:00
parent ac911f3f87
commit 8b46c31bd7
10 changed files with 295 additions and 18 deletions

View file

@ -1,9 +1,11 @@
#include "DrawCamera.hpp"
#include "DrawingProgram/Tools/DrawingProgramToolBase.hpp"
#include "Helpers/FixedPoint.hpp"
#include "Helpers/MathExtras.hpp"
#include "MainProgram.hpp"
#include "World.hpp"
#include "InputManager.hpp"
#include <Helpers/Logger.hpp>
DrawCamera::DrawCamera():
c({0, 0}, WorldScalar(1000000), 0.0)
@ -138,7 +140,7 @@ void DrawCamera::input_key_callback(const InputManager::KeyCallbackArgs& key) {
}
void DrawCamera::input_mouse_button_on_canvas_callback(World& w, const InputManager::MouseButtonCallbackArgs& button) {
if(!smoothMove.occurring) {
if(!smoothMove.occurring && !isTouchTransforming) {
bool newIsAccurateZooming = (w.drawProg.controls.middleClickHeld && w.main.input.pen.isDown && w.main.toolbar.tabletOptions.zoomWhilePenDownAndButtonHeld) || // Hold middle click (pen button assigned to middle click) while pen is down
(w.drawProg.controls.middleClickHeld && w.main.input.key(InputManager::KEY_GENERIC_LCTRL).held) || // Hold middle click/pen button while holding control
(w.drawProg.controls.leftClickHeld && w.drawProg.drawTool->get_type() == DrawingProgramToolType::ZOOM); // Hold left click while on zoom tool
@ -154,7 +156,7 @@ void DrawCamera::input_mouse_button_on_canvas_callback(World& w, const InputMana
}
void DrawCamera::input_mouse_motion_callback(World& w, const InputManager::MouseMotionCallbackArgs& motion) {
if(!smoothMove.occurring) {
if(!smoothMove.occurring && !isTouchTransforming) {
if(isAccurateZooming && startZoomVal != WorldScalar(0)) {
WorldScalar zoomFactor(std::pow(1.0 + w.main.toolbar.dragZoomSpeed, w.main.toolbar.flipZoomToolDirection ? motion.move.y() : -motion.move.y()));
if(zoomFactor < WorldScalar(0.000001))
@ -175,7 +177,7 @@ void DrawCamera::input_mouse_motion_callback(World& w, const InputManager::Mouse
}
void DrawCamera::input_mouse_wheel_callback(World& w, const InputManager::MouseWheelCallbackArgs& wheel) {
if(!smoothMove.occurring && wheel.tickAmount.y() && w.main.toolbar.check_if_position_isnt_obstructed(wheel.mousePos)) {
if(!smoothMove.occurring && !isTouchTransforming && wheel.tickAmount.y() && w.main.toolbar.check_if_position_isnt_obstructed(wheel.mousePos)) {
WorldVec mouseWorldPos = c.from_space(wheel.mousePos);
WorldScalar zoomFactor(1.0 + w.main.toolbar.scrollZoomSpeed);
@ -199,6 +201,40 @@ void DrawCamera::input_mouse_wheel_callback(World& w, const InputManager::MouseW
}
}
void DrawCamera::input_multi_finger_touch_callback(World& w, const InputManager::MultiFingerTouchArgs& touch) {
if(!smoothMove.occurring && !isAccurateZooming && !isTouchTransforming && touch.down) {
touchInitialPositions = touch.pos;
touchInitialC = c;
isTouchTransforming = true;
}
else
isTouchTransforming = false;
}
void DrawCamera::input_multi_finger_motion_callback(World& w, const InputManager::MultiFingerMotionArgs& motion) {
if(!smoothMove.occurring && !isAccurateZooming && isTouchTransforming) {
c = touchInitialC;
Vector2f initialCenter = (touchInitialPositions[0] + touchInitialPositions[1]) * 0.5f;
Vector2f newCenter = (motion.pos[0] + motion.pos[1]) * 0.5f;
c.pos -= c.dir_from_space(newCenter - initialCenter);
WorldVec newCenterWorld = c.from_space(newCenter);
float initialDistance = vec_distance(touchInitialPositions[0], touchInitialPositions[1]);
float newDistance = vec_distance(motion.pos[0], motion.pos[1]);
float scaleAmount = newDistance / initialDistance;
c.scale_about_double(newCenterWorld, scaleAmount);
Vector2f initialDiff = touchInitialPositions[0] - initialCenter;
float initialAngle = std::atan2(initialDiff.y(), initialDiff.x()) + std::numbers::pi;
Vector2f newDiff = motion.pos[0] - newCenter;
float newAngle = std::atan2(newDiff.y(), newDiff.x()) + std::numbers::pi;
float rotateAngle = initialAngle - newAngle;
rotateAngle = std::fmod(rotateAngle + std::numbers::pi, std::numbers::pi * 2.0f) - std::numbers::pi;
c.rotate_about(newCenterWorld, rotateAngle);
}
}
void DrawCamera::save_file(cereal::PortableBinaryOutputArchive& a, const World& w) const {
a(c, w.main.window.size.cast<float>().eval());
}

View file

@ -34,6 +34,8 @@ class DrawCamera {
void input_mouse_button_on_canvas_callback(World& w, const InputManager::MouseButtonCallbackArgs& button);
void input_mouse_motion_callback(World& w, const InputManager::MouseMotionCallbackArgs& motion);
void input_mouse_wheel_callback(World& w, const InputManager::MouseWheelCallbackArgs& wheel);
void input_multi_finger_touch_callback(World& w, const InputManager::MultiFingerTouchArgs& touch);
void input_multi_finger_motion_callback(World& w, const InputManager::MultiFingerMotionArgs& motion);
private:
struct SmoothMove {
CoordSpaceHelper start;
@ -49,6 +51,9 @@ class DrawCamera {
WorldScalar startZoomVal;
WorldVec startZoomMousePos;
WorldVec startZoomCameraPos;
bool isAccurateZooming = false;
CoordSpaceHelper touchInitialC;
std::vector<Vector2f> touchInitialPositions;
bool isTouchTransforming = false;
};

View file

@ -5,6 +5,7 @@
#include <SDL3/SDL_keycode.h>
#include <SDL3/SDL_pen.h>
#include <SDL3/SDL_timer.h>
#include <optional>
#include <Helpers/Logger.hpp>
@ -607,6 +608,178 @@ void InputManager::backend_pen_axis_update(const SDL_PenAxisEvent& e) {
}
}
std::vector<Vector2f> InputManager::get_multiple_finger_positions() {
std::vector<Vector2f> pos;
for(const SDL_TouchFingerEvent& fingerEvent : touch.fingers)
pos.emplace_back(fingerEvent.x * main.window.size.x() * main.window.density, fingerEvent.y * main.window.size.y() * main.window.density);
return pos;
}
std::vector<Vector2f> InputManager::get_multiple_finger_motions() {
std::vector<Vector2f> pos;
for(const SDL_TouchFingerEvent& fingerEvent : touch.fingers)
pos.emplace_back(fingerEvent.dx * main.window.size.x() * main.window.density, fingerEvent.dy * main.window.size.y() * main.window.density);
return pos;
}
void InputManager::backend_touch_finger_down_update(const SDL_TouchFingerEvent& e) {
touch.fingers.emplace_back(e);
switch(touch.touchEventType) {
case Touch::NO_TOUCH_EVENT: {
if(touch.fingers.size() == 2) {
touch.isDown = true;
touch.touchEventType = Touch::TWO_FINGER_EVENT;
main.input_multi_finger_touch_callback({
.down = true,
.pos = get_multiple_finger_positions()
});
}
break;
}
case Touch::ONE_FINGER_EVENT: {
break;
}
case Touch::TWO_FINGER_EVENT: {
break;
}
case Touch::EVENT_DONE: {
if(touch.fingers.size() == 1)
touch.touchEventType = Touch::NO_TOUCH_EVENT;
break;
}
}
}
void InputManager::backend_touch_finger_up_update(const SDL_TouchFingerEvent& e) {
switch(touch.touchEventType) {
case Touch::NO_TOUCH_EVENT: {
Vector2f mouseNewPos = {e.x * main.window.size.x() * main.window.density, e.y * main.window.size.y() * main.window.density};
mouse.set_pos(mouseNewPos);
touch.isDown = false;
mouse.leftDown = false;
if((std::chrono::steady_clock::now() - touch.lastLeftClickTime) > std::chrono::milliseconds(300))
touch.leftClicksSaved = 0;
touch.leftClicksSaved++;
mouse.leftClicks = touch.leftClicksSaved;
touch.lastLeftClickTime = std::chrono::steady_clock::now();
main.input_mouse_button_callback({
.button = MouseButton::LEFT,
.down = true,
.clicks = mouse.leftClicks,
.pos = mouseNewPos
});
main.input_mouse_button_callback({
.button = MouseButton::LEFT,
.down = false,
.clicks = 0,
.pos = mouseNewPos
});
touch.touchEventType = Touch::EVENT_DONE;
break;
}
case Touch::ONE_FINGER_EVENT: {
Vector2f mouseNewPos = {e.x * main.window.size.x() * main.window.density, e.y * main.window.size.y() * main.window.density};
mouse.set_pos(mouseNewPos);
touch.isDown = false;
mouse.leftDown = false;
main.input_mouse_button_callback({
.button = MouseButton::LEFT,
.down = false,
.clicks = 0,
.pos = mouseNewPos
});
touch.touchEventType = Touch::EVENT_DONE;
break;
}
case Touch::TWO_FINGER_EVENT: {
touch.isDown = false;
main.input_multi_finger_touch_callback({
.down = false,
.pos = get_multiple_finger_positions()
});
touch.touchEventType = Touch::EVENT_DONE;
break;
}
case Touch::EVENT_DONE: {
break;
}
}
std::erase_if(touch.fingers, [&e](auto& tE) {
return e.fingerID == tE.fingerID;
});
}
void InputManager::backend_touch_finger_motion_update(const SDL_TouchFingerEvent& e) {
switch(touch.touchEventType) {
case Touch::NO_TOUCH_EVENT: {
{
auto& prevTouch = touch.fingers[0];
Vector2f mouseNewPos = {prevTouch.x * main.window.size.x() * main.window.density, prevTouch.y * main.window.size.y() * main.window.density};
mouse.set_pos(mouseNewPos);
mouse.leftDown = true;
if((std::chrono::steady_clock::now() - touch.lastLeftClickTime) > std::chrono::milliseconds(300))
touch.leftClicksSaved = 0;
touch.leftClicksSaved++;
mouse.leftClicks = touch.leftClicksSaved;
touch.lastLeftClickTime = std::chrono::steady_clock::now();
main.input_mouse_button_callback({
.button = MouseButton::LEFT,
.down = true,
.clicks = mouse.leftClicks,
.pos = mouseNewPos
});
}
{
Vector2f mouseNewPos = {e.x * main.window.size.x() * main.window.density, e.y * main.window.size.y() * main.window.density};
mouse.set_pos(mouseNewPos);
Vector2f mouseRel = {e.dx * main.window.size.x() * main.window.density, e.dy * main.window.size.y() * main.window.density};
main.input_mouse_motion_callback({
.pos = mouseNewPos,
.move = mouseRel
});
}
touch.touchEventType = Touch::ONE_FINGER_EVENT;
break;
}
case Touch::ONE_FINGER_EVENT: {
Vector2f mouseNewPos = {e.x * main.window.size.x() * main.window.density, e.y * main.window.size.y() * main.window.density};
mouse.set_pos(mouseNewPos);
Vector2f mouseRel = {e.dx * main.window.size.x() * main.window.density, e.dy * main.window.size.y() * main.window.density};
main.input_mouse_motion_callback({
.pos = mouseNewPos,
.move = mouseRel
});
break;
}
case Touch::TWO_FINGER_EVENT: {
main.input_multi_finger_motion_callback({
.pos = get_multiple_finger_positions(),
.move = get_multiple_finger_motions()
});
break;
}
case Touch::EVENT_DONE: {
break;
}
}
for(auto& tE : touch.fingers) {
if(tE.fingerID == e.fingerID) {
tE = e;
break;
}
}
}
void InputManager::backend_key_down_update(const SDL_KeyboardEvent& e) {
auto kPress = e.key;
auto kMod = e.mod;
@ -936,6 +1109,30 @@ void InputManager::Mouse::set_pos(const Vector2f& newPos) {
move = newPos - lastPos;
}
void InputManager::update() {
if(touch.touchEventType == Touch::NO_TOUCH_EVENT && (SDL_GetTicksNS() - touch.fingers[0].timestamp) > (100 * 1000000)) { // 100ms to determine whether touch is with a single finger
auto& prevTouch = touch.fingers[0];
Vector2f mouseNewPos = {prevTouch.x * main.window.size.x() * main.window.density, prevTouch.y * main.window.size.y() * main.window.density};
mouse.set_pos(mouseNewPos);
mouse.leftDown = true;
if((std::chrono::steady_clock::now() - touch.lastLeftClickTime) > std::chrono::milliseconds(300))
touch.leftClicksSaved = 0;
touch.leftClicksSaved++;
mouse.leftClicks = touch.leftClicksSaved;
touch.lastLeftClickTime = std::chrono::steady_clock::now();
main.input_mouse_button_callback({
.button = MouseButton::LEFT,
.down = true,
.clicks = mouse.leftClicks,
.pos = mouseNewPos
});
touch.touchEventType = Touch::ONE_FINGER_EVENT;
}
}
void InputManager::frame_reset(const Vector2i& windowSize) {
mouse.lastPos = mouse.pos;
mouse.move = Vector2f({0.0f, 0.0f});

View file

@ -21,6 +21,7 @@ class MainProgram;
struct InputManager {
InputManager(MainProgram& initMain);
void update();
void frame_reset(const Vector2i& windowSize);
// Just renamed SDL3 cursor types, but if we need to change to a different backend we can
@ -77,6 +78,13 @@ struct InputManager {
bool isDown = false;
int leftClicksSaved = 0;
std::chrono::steady_clock::time_point lastLeftClickTime;
std::vector<SDL_TouchFingerEvent> fingers;
enum TypeOfTouchEvent {
NO_TOUCH_EVENT,
ONE_FINGER_EVENT,
TWO_FINGER_EVENT,
EVENT_DONE
} touchEventType = EVENT_DONE;
} touch;
struct Pen {
@ -208,6 +216,8 @@ struct InputManager {
uint32_t make_generic_key_mod(SDL_Keymod m);
void set_key_up(const SDL_KeyboardEvent& e, KeyCode kCode);
void set_key_down(const SDL_KeyboardEvent& e, KeyCode kCode);
std::vector<Vector2f> get_multiple_finger_positions();
std::vector<Vector2f> get_multiple_finger_motions();
void backend_drop_file_event(const SDL_DropEvent& e);
void backend_drop_text_event(const SDL_DropEvent& e);
@ -223,6 +233,9 @@ struct InputManager {
void backend_pen_touch_up_update(const SDL_PenTouchEvent& e);
void backend_pen_motion_update(const SDL_PenMotionEvent& e);
void backend_pen_axis_update(const SDL_PenAxisEvent& e);
void backend_touch_finger_down_update(const SDL_TouchFingerEvent& e);
void backend_touch_finger_up_update(const SDL_TouchFingerEvent& e);
void backend_touch_finger_motion_update(const SDL_TouchFingerEvent& e);
void set_clipboard_str(std::string_view s);
void set_clipboard_plain_and_richtext_pair(const std::pair<std::string, RichText::TextData>& plainAndRichtextPair);
@ -301,6 +314,16 @@ struct InputManager {
const char* data;
};
struct MultiFingerTouchArgs {
bool down;
std::vector<Vector2f> pos;
};
struct MultiFingerMotionArgs {
std::vector<Vector2f> pos;
std::vector<Vector2f> move;
};
MainProgram& main;
};

View file

@ -68,6 +68,8 @@ void MainProgram::update() {
while(std::chrono::duration_cast<std::chrono::duration<float>>(std::chrono::steady_clock::now() - lastFrameTime).count() < 1.0 / fpsLimit);
lastFrameTime = std::chrono::steady_clock::now();
input.update();
for(auto& s : setTabsToClose)
std::erase(worlds, s.lock());
@ -378,6 +380,16 @@ void MainProgram::input_pen_axis_callback(const InputManager::PenAxisCallbackArg
world->input_pen_axis_callback(axis);
}
void MainProgram::input_multi_finger_touch_callback(const InputManager::MultiFingerTouchArgs& touch) {
if(world)
world->input_multi_finger_touch_callback(touch);
}
void MainProgram::input_multi_finger_motion_callback(const InputManager::MultiFingerMotionArgs& motion) {
if(world)
world->input_multi_finger_motion_callback(motion);
}
bool MainProgram::network_being_used() {
for(auto& w : worlds) {
if(w->netObjMan.is_connected())

View file

@ -132,6 +132,8 @@ class MainProgram {
void input_pen_touch_callback(const InputManager::PenTouchCallbackArgs& touch);
void input_pen_motion_callback(const InputManager::PenMotionCallbackArgs& motion);
void input_pen_axis_callback(const InputManager::PenAxisCallbackArgs& axis);
void input_multi_finger_touch_callback(const InputManager::MultiFingerTouchArgs& touch);
void input_multi_finger_motion_callback(const InputManager::MultiFingerMotionArgs& motion);
~MainProgram();
private:

View file

@ -2347,6 +2347,7 @@ void Toolbar::input_key_callback(const InputManager::KeyCallbackArgs& key) {
}
void Toolbar::input_mouse_button_callback(const InputManager::MouseButtonCallbackArgs& button) {
io->mouse.globalPos = button.pos / final_gui_scale();
if(button.down) {
switch(button.button) {
case InputManager::MouseButton::LEFT: {

View file

@ -325,6 +325,16 @@ void World::input_pen_axis_callback(const InputManager::PenAxisCallbackArgs& axi
drawProg.input_pen_axis_callback(axis);
}
void World::input_multi_finger_touch_callback(const InputManager::MultiFingerTouchArgs& touch) {
if(!clientStillConnecting)
drawData.cam.input_multi_finger_touch_callback(*this, touch);
}
void World::input_multi_finger_motion_callback(const InputManager::MultiFingerMotionArgs& motion) {
if(!clientStillConnecting)
drawData.cam.input_multi_finger_motion_callback(*this, motion);
}
void World::send_chat_message(const std::string& message) {
if(!clientStillConnecting)
ownClientData->send_chat_message(ownClientData, *this, message);

View file

@ -113,6 +113,8 @@ class World {
void input_pen_touch_callback(const InputManager::PenTouchCallbackArgs& touch);
void input_pen_motion_callback(const InputManager::PenMotionCallbackArgs& motion);
void input_pen_axis_callback(const InputManager::PenAxisCallbackArgs& axis);
void input_multi_finger_touch_callback(const InputManager::MultiFingerTouchArgs& touch);
void input_multi_finger_motion_callback(const InputManager::MultiFingerMotionArgs& motion);
private:
void load_empty_canvas();

View file

@ -810,28 +810,17 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
}
case SDL_EVENT_FINGER_DOWN: {
mS.m->update_scale_and_density();
mS.m->input.touch.isDown = true;
mS.m->input.mouse.leftDown = true;
if((std::chrono::steady_clock::now() - mS.m->input.touch.lastLeftClickTime) > std::chrono::milliseconds(300))
mS.m->input.touch.leftClicksSaved = 0;
mS.m->input.touch.leftClicksSaved++;
mS.m->input.mouse.leftClicks = mS.m->input.touch.leftClicksSaved;
mS.m->input.touch.lastLeftClickTime = std::chrono::steady_clock::now();
mS.m->input.mouse.set_pos({event->tfinger.x * mS.m->window.size.x() * mS.m->window.density, event->tfinger.y * mS.m->window.size.y() * mS.m->window.density});
mS.m->input.mouse.move = Vector2f{0.0f, 0.0f};
mS.m->input.backend_touch_finger_down_update(event->tfinger);
break;
}
case SDL_EVENT_FINGER_UP: {
mS.m->update_scale_and_density();
mS.m->input.touch.isDown = false;
mS.m->input.mouse.leftDown = false;
mS.m->input.mouse.set_pos({-1.0f, -1.0f});
mS.m->input.mouse.move = Vector2f{0.0f, 0.0f};
mS.m->input.backend_touch_finger_up_update(event->tfinger);
break;
}
case SDL_EVENT_FINGER_MOTION: {
mS.m->update_scale_and_density();
mS.m->input.mouse.set_pos({event->tfinger.x * mS.m->window.size.x() * mS.m->window.density, event->tfinger.y * mS.m->window.size.y() * mS.m->window.density});
mS.m->input.backend_touch_finger_motion_update(event->tfinger);
break;
}
case SDL_EVENT_DISPLAY_ORIENTATION: