From 137f110e1d63b83cca2325561462931374b28c11 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Fri, 14 Nov 2025 16:36:42 +0100 Subject: [PATCH 01/15] BugFix Step to next frame in MediaPlayer image simulating timeline --- src/MediaPlayer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MediaPlayer.cpp b/src/MediaPlayer.cpp index f9e0423..560c75d 100644 --- a/src/MediaPlayer.cpp +++ b/src/MediaPlayer.cpp @@ -1091,7 +1091,7 @@ void MediaPlayer::step(uint milisecond) else { // step event if (milisecond < media_.dt) - milisecond = media_.dt; + milisecond = timeline_.step() < media_.dt ? timeline_.step() : media_.dt; GstEvent *stepevent = gst_event_new_step (GST_FORMAT_TIME, milisecond, ABS(rate_), TRUE, FALSE); // Metronome From 8d26f5d78a8017c1ab6704a3b5928f411b7351ca Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Sat, 15 Nov 2025 08:58:41 +0100 Subject: [PATCH 02/15] Add Flag functionality to ControlManager and InputMappingWindow with a new Flag SourceCallback. Fixed media player window control. --- src/ControlManager.cpp | 7 ++++ src/ControlManager.h | 1 + src/InputMappingWindow.cpp | 44 ++++++++++++++++++++--- src/InputMappingWindow.h | 2 +- src/MediaPlayer.cpp | 13 +++++-- src/SessionCreator.cpp | 6 ++++ src/SessionCreator.h | 1 + src/SessionVisitor.cpp | 5 +++ src/SessionVisitor.h | 1 + src/SourceCallback.cpp | 70 +++++++++++++++++++++++++++++++++++++ src/SourceCallback.h | 17 +++++++++ src/SourceControlWindow.cpp | 31 ++++++++-------- src/Visitor.h | 1 + src/defines.h | 4 +-- 14 files changed, 176 insertions(+), 27 deletions(-) diff --git a/src/ControlManager.cpp b/src/ControlManager.cpp index d6195a9..2a6dc35 100644 --- a/src/ControlManager.cpp +++ b/src/ControlManager.cpp @@ -970,6 +970,13 @@ bool Control::receiveSourceAttribute(Source *target, const std::string &attribut } target->setImageProcessingEnabled(on > 0.5f); } + else if ( attribute.compare(OSC_SOURCE_FLAG) == 0) { + float f = -1.f; + if (!arguments.Eos()) { + arguments >> f >> osc::EndMessage; + } + target->call( new Flag( f )); + } /// e.g. '/vimix/current/seek f 0.25' ; seek to 25% of duration /// e.g. '/vimix/current/seek iiii 0 0 25 500' ; seek to time else if ( attribute.compare(OSC_SOURCE_SEEK) == 0) { diff --git a/src/ControlManager.h b/src/ControlManager.h index 2ba62f5..c9e48dd 100644 --- a/src/ControlManager.h +++ b/src/ControlManager.h @@ -78,6 +78,7 @@ #define OSC_SOURCE_FILTER "/filter" #define OSC_SOURCE_UNIFORM "/uniform" #define OSC_SOURCE_BLENDING "/blending" +#define OSC_SOURCE_FLAG "/flag" #define OSC_SESSION "/session" #define OSC_SESSION_VERSION "/version" diff --git a/src/InputMappingWindow.cpp b/src/InputMappingWindow.cpp index e637e0b..376b0a6 100644 --- a/src/InputMappingWindow.cpp +++ b/src/InputMappingWindow.cpp @@ -17,6 +17,7 @@ * along with this program. If not, see . **/ +#include "MediaSource.h" #include #include @@ -39,6 +40,7 @@ #include "SourceCallback.h" #include "ControlManager.h" #include "Metronome.h" +#include "MediaPlayer.h" #include "InputMappingWindow.h" @@ -119,9 +121,9 @@ Target InputMappingWindow::ComboSelectTarget(const Target ¤t) return selected; } -uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing) +uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing, bool ismediaplayer) { - const char* callback_names[23] = { "Select", + const char* callback_names[24] = { "Select", ICON_FA_BULLSEYE " Alpha", ICON_FA_BULLSEYE " Loom", ICON_FA_OBJECT_UNGROUP " Geometry", @@ -133,6 +135,7 @@ uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing) ICON_FA_PLAY_CIRCLE " Speed", ICON_FA_PLAY_CIRCLE " Fast forward", ICON_FA_PLAY_CIRCLE " Seek", + ICON_FA_PLAY_CIRCLE " Flag", " None", " None", " None", @@ -148,7 +151,8 @@ uint InputMappingWindow::ComboSelectCallback(uint current, bool imageprocessing) uint selected = 0; if (ImGui::BeginCombo("##ComboSelectCallback", callback_names[current]) ) { - for (uint i = SourceCallback::CALLBACK_ALPHA; i <= SourceCallback::CALLBACK_SEEK; ++i){ + for (uint i = SourceCallback::CALLBACK_ALPHA; + i <= (ismediaplayer ? SourceCallback::CALLBACK_FLAG : SourceCallback::CALLBACK_PLAY) ; ++i){ if ( ImGui::Selectable( callback_names[i]) ) { selected = i; } @@ -488,6 +492,32 @@ void InputMappingWindow::SliderParametersCallback(SourceCallback *callback, cons } break; + case SourceCallback::CALLBACK_FLAG: + { + Flag *edited = static_cast(callback); + + ImGuiToolkit::Indication(press_tooltip[0], 2, 13); + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + + int max = -1; + if (Source * const* v = std::get_if(&target)) { + MediaSource *ms = dynamic_cast(*v); + if (ms) + max = ms->mediaplayer()->timeline()->numFlags() - 1; + } + int val = MIN( (int) edited->value(), max); + + ImGui::SetNextItemWidth(right_align); + ImGui::SameLine(0, IMGUI_SAME_LINE / 2); + if (ImGui::SliderInt("##CALLBACK_PLAY_FLAG", &val, -1, max, val < 0 ? "Next Flag" : "Flag <%d>")) + edited->setValue(val ); + + ImGui::SameLine(0, IMGUI_SAME_LINE / 3); + ImGuiToolkit::Indication("Flag to jump to in a video source.", 12, 6); + + } + break; + case SourceCallback::CALLBACK_BRIGHTNESS: { SetBrightness *edited = static_cast(callback); @@ -1362,17 +1392,19 @@ void InputMappingWindow::Render() } // check if target is a Source with image processing enabled + bool ismediaplayer = false; bool withimageprocessing = false; if ( target.index() == 1 ) { if (Source * const* v = std::get_if(&target)) { withimageprocessing = (*v)->imageProcessingEnabled(); + ismediaplayer = dynamic_cast(*v) != nullptr; } } // Select Reaction ImGui::SameLine(0, IMGUI_SAME_LINE); ImGui::SetNextItemWidth(w); - uint type = ComboSelectCallback( callback->type(), withimageprocessing ); + uint type = ComboSelectCallback( callback->type(), withimageprocessing, ismediaplayer ); if (type > 0) { // remove previous callback S->deleteInputCallback(callback); @@ -1431,16 +1463,18 @@ void InputMappingWindow::Render() // possible new target if (temp_new_target.index() > 0) { // check if target is a Source with image processing enabled + bool mediaplayer = false; bool withimageprocessing = false; if ( temp_new_target.index() == 1 ) { if (Source * const* v = std::get_if(&temp_new_target)) { withimageprocessing = (*v)->imageProcessingEnabled(); + mediaplayer = dynamic_cast(*v) != nullptr; } } // step 3: Get input for callback type ImGui::SameLine(0, IMGUI_SAME_LINE); ImGui::SetNextItemWidth(w); - temp_new_callback = ComboSelectCallback( temp_new_callback, withimageprocessing ); + temp_new_callback = ComboSelectCallback( temp_new_callback, withimageprocessing, mediaplayer ); // user selected a callback type if (temp_new_callback > 0) { // step 4 : create new callback and add it to source diff --git a/src/InputMappingWindow.h b/src/InputMappingWindow.h index 4b7fc99..de38163 100644 --- a/src/InputMappingWindow.h +++ b/src/InputMappingWindow.h @@ -16,7 +16,7 @@ class InputMappingWindow : public WorkspaceWindow uint current_input_; Target ComboSelectTarget(const Target ¤t); - uint ComboSelectCallback(uint current, bool imageprocessing); + uint ComboSelectCallback(uint current, bool imageprocessing, bool mediaplayer); void SliderParametersCallback(SourceCallback *callback, const Target &target); public: diff --git a/src/MediaPlayer.cpp b/src/MediaPlayer.cpp index 560c75d..e7f4a84 100644 --- a/src/MediaPlayer.cpp +++ b/src/MediaPlayer.cpp @@ -1134,6 +1134,9 @@ bool MediaPlayer::go_to(GstClockTime pos) if (ABS_DIFF (position_, jumpPts) > 2 * timeline_.step() ) { ret = true; seek( jumpPts ); + + // Revert loop status to default + loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT; } } return ret; @@ -1614,10 +1617,14 @@ bool MediaPlayer::go_to_flag(TimeInterval flag) current_flag_ = flag; // change timeline status accordingly - if ( flag.type == (int) LoopStatus::LOOP_STATUS_BLACKOUT) + if ( flag.type == (int) LoopStatus::LOOP_STATUS_BLACKOUT) { loop_status_ = LoopStatus::LOOP_STATUS_BLACKOUT; - else if ( flag.type == (int) LoopStatus::LOOP_STATUS_STOPPED) - loop_status_ = LoopStatus::LOOP_STATUS_DEFAULT; + execute_play_command(false); + } + else if ( flag.type == (int) LoopStatus::LOOP_STATUS_STOPPED) { + loop_status_ = LoopStatus::LOOP_STATUS_STOPPED; + execute_play_command(false); + } } } return ret; diff --git a/src/SessionCreator.cpp b/src/SessionCreator.cpp index 9d83aa7..83f8444 100644 --- a/src/SessionCreator.cpp +++ b/src/SessionCreator.cpp @@ -1703,6 +1703,12 @@ void SessionLoader::visit (Seek &c) xmlCurrent_->QueryBoolAttribute("bidirectional", &b); c.setBidirectional(b); } +void SessionLoader::visit (Flag &c) +{ + int v = -1; + xmlCurrent_->QueryIntAttribute("value", &v); + c.setValue(v); +} void SessionLoader::visit (SetAlpha &c) { diff --git a/src/SessionCreator.h b/src/SessionCreator.h index ba6306d..6d50809 100644 --- a/src/SessionCreator.h +++ b/src/SessionCreator.h @@ -93,6 +93,7 @@ public: void visit (Play&) override; void visit (PlayFastForward&) override; void visit (Seek&) override; + void visit (Flag&) override; static void XMLToNode(const tinyxml2::XMLElement *xml, Node &n); static void XMLToSourcecore(tinyxml2::XMLElement *xml, SourceCore &s); diff --git a/src/SessionVisitor.cpp b/src/SessionVisitor.cpp index c198f7a..d6b93a2 100644 --- a/src/SessionVisitor.cpp +++ b/src/SessionVisitor.cpp @@ -1058,6 +1058,11 @@ void SessionVisitor::visit (PlayFastForward &c) xmlCurrent_->SetAttribute("duration", c.duration()); } +void SessionVisitor::visit (Flag &c) +{ + xmlCurrent_->SetAttribute("value", (int) c.value()); +} + void SessionVisitor::visit (Seek &c) { xmlCurrent_->SetAttribute("value", (uint64_t) c.value()); diff --git a/src/SessionVisitor.h b/src/SessionVisitor.h index 87f54e1..3034659 100644 --- a/src/SessionVisitor.h +++ b/src/SessionVisitor.h @@ -99,6 +99,7 @@ public: void visit (Play&) override; void visit (PlayFastForward&) override; void visit (Seek&) override; + void visit (Flag&) override; static tinyxml2::XMLElement *NodeToXML(const Node &n, tinyxml2::XMLDocument *doc); static tinyxml2::XMLElement *ImageToXML(const FrameBufferImage *img, tinyxml2::XMLDocument *doc); diff --git a/src/SourceCallback.cpp b/src/SourceCallback.cpp index 05ea61b..28035f2 100644 --- a/src/SourceCallback.cpp +++ b/src/SourceCallback.cpp @@ -72,6 +72,9 @@ SourceCallback *SourceCallback::create(CallbackType type) case SourceCallback::CALLBACK_SEEK: loadedcallback = new Seek; break; + case SourceCallback::CALLBACK_FLAG: + loadedcallback = new Flag; + break; case SourceCallback::CALLBACK_REPLAY: loadedcallback = new RePlay; break; @@ -718,6 +721,73 @@ void Seek::accept(Visitor& v) v.visit(*this); } +Flag::Flag(int target) + : SourceCallback() + , flag_index_(target) +{} + +void Flag::update(Source *s, float dt) +{ + SourceCallback::update(s, dt); + + // access media player if target source is a media source + MediaSource *ms = dynamic_cast(s); + if (ms != nullptr) { + + // can operate on flags if there are some + int num = ms->mediaplayer()->timeline()->numFlags(); + if (num > 1) { + + // default flag index is -1 to mean next flag + if (flag_index_ < 0) { + GstClockTime _time = ms->mediaplayer()->position(); + + if( ms->mediaplayer()->playSpeed() < 0 ) { + // Go to previous flag when playing backward + TimeInterval target_flag = ms->mediaplayer()->timeline()->getPreviousFlag( _time ); + bool has_prev = target_flag.is_valid() && + ( ms->mediaplayer()->loop() == MediaPlayer::LOOP_REWIND || (target_flag.end < _time) ); + if( has_prev) + ms->mediaplayer()->go_to_flag( target_flag ); + } + else { + // go to next flag when playing forward + TimeInterval target_flag = ms->mediaplayer()->timeline()->getNextFlag( _time ); + bool has_next = target_flag.is_valid() && + ( ms->mediaplayer()->loop() == MediaPlayer::LOOP_REWIND || (target_flag.begin > _time) ); + if( has_next ) + ms->mediaplayer()->go_to_flag( target_flag ); + } + } + else if (flag_index_ < num) { + int index = 0; + const TimeIntervalSet flags = ms->mediaplayer()->timeline()->flags(); + for (const auto &flag_Interval : flags) { + if ( index == flag_index_) { + + ms->mediaplayer()->go_to_flag( flag_Interval ); + break; + } + ++index; + } + } + } + } + + status_ = FINISHED; +} + +SourceCallback *Flag::clone() const +{ + return new Flag(flag_index_); +} + +void Flag::accept(Visitor& v) +{ + SourceCallback::accept(v); + v.visit(*this); +} + SetGeometry::SetGeometry(const Group *g, float ms, bool revert) : SourceCallback(), duration_(ms), bidirectional_(revert) { diff --git a/src/SourceCallback.h b/src/SourceCallback.h index 9c37256..c39d3cd 100644 --- a/src/SourceCallback.h +++ b/src/SourceCallback.h @@ -40,6 +40,7 @@ public: CALLBACK_PLAYSPEED, CALLBACK_PLAYFFWD, CALLBACK_SEEK, + CALLBACK_FLAG, CALLBACK_REPLAY, CALLBACK_RESETGEO, CALLBACK_LOCK, @@ -295,6 +296,22 @@ public: void accept (Visitor& v) override; }; +class Flag : public SourceCallback +{ + int flag_index_; + +public: + Flag (int target = -1); + + int value () const { return flag_index_; } + void setValue (int t) { flag_index_ = t; } + + void update (Source *s, float) override; + SourceCallback *clone() const override; + CallbackType type () const override { return CALLBACK_FLAG; } + void accept (Visitor& v) override; +}; + class ResetGeometry : public SourceCallback { public: diff --git a/src/SourceControlWindow.cpp b/src/SourceControlWindow.cpp index 39bb96c..67c50e1 100644 --- a/src/SourceControlWindow.cpp +++ b/src/SourceControlWindow.cpp @@ -17,6 +17,7 @@ * along with this program. If not, see . **/ +#include #include #include #include @@ -946,6 +947,7 @@ bool TimelineSlider (const char* label, guint64 *time, TimeInterval *flag, Timel // // FLAGS // + int index = 0; bool flag_pressed = false; const TimeIntervalSet flags = tl->flags(); for (const auto &flag_Interval : flags) { @@ -972,7 +974,9 @@ bool TimelineSlider (const char* label, guint64 *time, TimeInterval *flag, Timel // show time when hovering if (hovered) - ImGui::SetTooltip(" %s ", GstToolkit::time_to_string(flag_time).c_str()); + ImGui::SetTooltip(" <%d> %s ", index, GstToolkit::time_to_string(flag_time).c_str()); + + ++index; } // @@ -2305,7 +2309,7 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) // flag buttons if (rendersize.x > buttons_height_ * 6.0f) { - if ( !mediaplayer_mode_ && mediaplayer_active_->timeline()->numFlags() > 0 ) { + if ( mediaplayer_active_->timeline()->numFlags() > 0 ) { GstClockTime _paused_time = mediaplayer_active_->position(); @@ -2317,7 +2321,7 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) !( mediaplayer_active_->currentFlag().is_valid() && mediaplayer_active_->timeline()->numFlags() == 1) && ( mediaplayer_active_->loop() == MediaPlayer::LOOP_REWIND || (target_flag.end < _paused_time) ); if( ImGuiToolkit::ButtonIcon(6, 0, "Go to previous flag", has_prev) ) - mediaplayer_active_->go_to_flag( target_flag ); + seek_flag = target_flag; } else { // go to next flag when playing forward @@ -2326,11 +2330,11 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) !( mediaplayer_active_->currentFlag().is_valid() && mediaplayer_active_->timeline()->numFlags() == 1) && ( mediaplayer_active_->loop() == MediaPlayer::LOOP_REWIND || (target_flag.begin > _paused_time) ); if( ImGuiToolkit::ButtonIcon(5, 0, "Go to next flag", has_next) ) - mediaplayer_active_->go_to_flag( target_flag ); + seek_flag = target_flag; } // if stopped at a flag, show flag menu - if (mediaplayer_active_->currentFlag().is_valid()) { + if (!mediaplayer_mode_ && mediaplayer_active_->currentFlag().is_valid()) { ImGui::SameLine(0, h_space_); if (ImGuiToolkit::IconButton(3, 0) || ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) { counter_menu_timeout=0; @@ -2396,23 +2400,18 @@ void SourceControlWindow::RenderMediaPlayer(MediaSource *ms) // play/stop command should be following the playing mode (buttons) // AND force to stop when the slider is pressed - bool media_play = mediaplayer_mode_ & (!mediaplayer_slider_pressed_); - - // Flag pressed in timeline - if (seek_flag.is_valid()) { - // go to the flag position - if ( mediaplayer_active_->go_to_flag(seek_flag) ){ - // stop if flag type is 'Stop' (1) or 'Blackout' (2) - if (seek_flag.type > 0) - media_play = false; - } - } + bool media_play = mediaplayer_mode_ & (!mediaplayer_slider_pressed_); // apply play action to media only if status should change if ( mediaplayer_active_->isPlaying() != media_play ) { mediaplayer_active_->play( media_play ); } + // Flag pressed in timeline + if (seek_flag.is_valid()) { + // go to the flag position + mediaplayer_active_->go_to_flag(seek_flag); + } } else { /// diff --git a/src/Visitor.h b/src/Visitor.h index 83d4ea1..e59f157 100644 --- a/src/Visitor.h +++ b/src/Visitor.h @@ -76,6 +76,7 @@ public: virtual void visit (class Play&) {} virtual void visit (class PlayFastForward&) {} virtual void visit (class Seek&) {} + virtual void visit (class Flag&) {} }; diff --git a/src/defines.h b/src/defines.h index a8a6fb6..a43a4b0 100644 --- a/src/defines.h +++ b/src/defines.h @@ -111,8 +111,8 @@ #define IMGUI_LABEL_RECENT_FILES " Recent files" #define IMGUI_LABEL_RECENT_RECORDS " Recent recordings" #define IMGUI_RIGHT_ALIGN -3.8f * ImGui::GetTextLineHeightWithSpacing() -#define IMGUI_SAME_LINE 8 -#define IMGUI_TOP_ALIGN 10 +#define IMGUI_SAME_LINE 8.f +#define IMGUI_TOP_ALIGN 10.f #define IMGUI_COLOR_OVERLAY IM_COL32(5, 5, 5, 150) #define IMGUI_COLOR_LIGHT_OVERLAY IM_COL32(5, 5, 5, 50) #define IMGUI_COLOR_CAPTURE 1.0, 0.55, 0.05 From 84233e46cd728136e9a99295942f804bf485d619 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Sun, 16 Nov 2025 16:36:01 +0100 Subject: [PATCH 03/15] Improved draw texture by drawing a line between mouse cursor coordinates (instead of discrete points). This allows to use smaller size of pencil. --- rsc/shaders/mask_draw.fs | 36 +++++++++++++++++++----------------- src/TextureView.cpp | 21 +++++++++++++++++---- src/TextureView.h | 1 + src/defines.h | 2 +- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/rsc/shaders/mask_draw.fs b/rsc/shaders/mask_draw.fs index 8a0c780..c2cdb7e 100644 --- a/rsc/shaders/mask_draw.fs +++ b/rsc/shaders/mask_draw.fs @@ -21,26 +21,27 @@ uniform vec3 brush; uniform int option; uniform int effect; -float sdBox( in vec2 p, in float b) +float sdBox( in vec2 v1, in vec2 v2, float r ) { - vec2 q = abs(p) - vec2(b); + vec2 ba = v2 - v1; + vec2 pa = -v1; + float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); + vec2 q = abs(pa -h*ba) - vec2(r); float d = min( max(q.x, q.y), 0.0) + length(max(q,0.0)); - return 1.0 - abs(d) * step(d, 0.0); + return 1.0 - abs( mix( 0.5, 1.0, d) ) * step(d, 0.0); } -float sdCircle( in vec2 p, in float b) +float sdSegment( in vec2 v1, in vec2 v2, float r ) { - return ( length( p ) / b ); + vec2 ba = v2 - v1; + vec2 pa = -v1; + float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); + return length(pa-h*ba) / r; } -float sdElipse( in vec2 p, in float b) -{ - return ( length( p ) / b ); -} - -const mat3 KERNEL = mat3( 0.0625, 0.125, 0.0625, - 0.125, 0.25, 0.125, - 0.0625, 0.125, 0.0625); +const mat3 KERNEL = mat3( 0.0555555, 0.111111, 0.0555555, + 0.111111, 0.333334, 0.111111, + 0.0555555, 0.111111, 0.0555555); vec3 gaussian() { @@ -71,14 +72,15 @@ void main() // fragment coordinates vec2 uv = -1.0 + 2.0 * gl_FragCoord.xy / iResolution.xy; // adjust coordinates to match scaling area - uv.x *= cursor.z ; - uv.y *= cursor.w ; + uv.x *= size.x ; + uv.y *= size.y ; // cursor coordinates - vec2 cursor = vec2(cursor.x, - cursor.y); + vec2 cur = vec2(cursor.x, - cursor.y); + vec2 cur_prev = vec2(cursor.z, - cursor.w); // use distance function relative to length brush.x (size), depending on brush.z (shape): // - brush.z = 0 : circle shape // - brush.z = 1 : square shape - float d = (1.0 -brush.z) * sdCircle(cursor-uv, brush.x) + brush.z * sdBox(uv-cursor, brush.x); + float d = (1.0 -brush.z) * sdSegment(cur_prev - uv, cur - uv, brush.x) + brush.z * sdBox(cur_prev - uv, cur - uv, brush.x); // modify only the pixels inside the brush if( d < 1.0 ) diff --git a/src/TextureView.cpp b/src/TextureView.cpp index b6bef6d..bc49767 100644 --- a/src/TextureView.cpp +++ b/src/TextureView.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #define GLM_ENABLE_EXPERIMENTAL @@ -450,6 +451,8 @@ std::pair TextureView::pick(glm::vec2 P) pick = { mask_cursor_circle_, P }; // adapt grid to prepare grab action adaptGridToSource(current); + // reset previous brush position + previous_scene_brush_pos = glm::zero(); return pick; } // special case for cropping the mask shape @@ -815,8 +818,8 @@ void TextureView::draw() ImGuiToolkit::ToolTip("Size"); if (ImGui::BeginPopup("brush_size_popup", ImGuiWindowFlags_NoMove)) { - int pixel_size_min = int(0.05 * edit_source_->frame()->height() ); - int pixel_size_max = int(2.0 * edit_source_->frame()->height() ); + int pixel_size_min = int(BRUSH_MIN_SIZE * edit_source_->frame()->height() ); + int pixel_size_max = int(BRUSH_MAX_SIZE * edit_source_->frame()->height() ); int pixel_size = int(Settings::application.brush.x * edit_source_->frame()->height() ); show_cursor_forced_ = true; ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT); @@ -1106,16 +1109,26 @@ View::Cursor TextureView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pa // set brush coordinates (used in mouse over) scene_brush_pos = scene_to; + // no previous brush position : restart at same coordinates + if ( glm::length( previous_scene_brush_pos ) < EPSILON) + previous_scene_brush_pos = scene_brush_pos; + if ( pick.first == mask_cursor_circle_ ) { // snap prush coordinates if grid is active if (grid->active()) scene_brush_pos = grid->snap(scene_brush_pos); + // inform shader of a cursor action : coordinates and crop scaling + edit_source_->maskShader()->size = edit_source_->mixingsurface_->scale_; + // inform shader of a cursor action : coordinates and crop scaling edit_source_->maskShader()->cursor = glm::vec4(scene_brush_pos.x - shift_crop_.x, scene_brush_pos.y - shift_crop_.y, - edit_source_->mixingsurface_->scale_.x, - edit_source_->mixingsurface_->scale_.y); + previous_scene_brush_pos.x - shift_crop_.x, + previous_scene_brush_pos.y - shift_crop_.y); edit_source_->touch(Source::SourceUpdate_Mask); + + previous_scene_brush_pos = scene_brush_pos; + // action label info << MaskShader::mask_names[MaskShader::PAINT] << " changed"; // cursor indication - no info, just cursor diff --git a/src/TextureView.h b/src/TextureView.h index f066d7e..4ef6f68 100644 --- a/src/TextureView.h +++ b/src/TextureView.h @@ -72,6 +72,7 @@ private: void adaptGridToSource(Source *s = nullptr, Node *picked = nullptr); glm::vec3 scene_brush_pos; + glm::vec3 previous_scene_brush_pos; TranslationGrid *translation_grid_; RotationGrid *rotation_grid_; }; diff --git a/src/defines.h b/src/defines.h index a43a4b0..2719e37 100644 --- a/src/defines.h +++ b/src/defines.h @@ -90,7 +90,7 @@ #define APPEARANCE_DEFAULT_SCALE 2.f #define APPEARANCE_MIN_SCALE 0.4f #define APPEARANCE_MAX_SCALE 7.0f -#define BRUSH_MIN_SIZE 0.05f +#define BRUSH_MIN_SIZE 0.01f #define BRUSH_MAX_SIZE 2.f #define BRUSH_MIN_PRESS 0.005f #define BRUSH_MAX_PRESS 1.f From e51781ee1dc6b3495591786e306e7493f5df60fc Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Sun, 16 Nov 2025 16:55:15 +0100 Subject: [PATCH 04/15] BugFix Source Callback (delete first in source) and improved GUI seek callback. --- src/InputMappingWindow.cpp | 70 ++++++++------------------------------ src/Source.cpp | 18 +++++----- 2 files changed, 23 insertions(+), 65 deletions(-) diff --git a/src/InputMappingWindow.cpp b/src/InputMappingWindow.cpp index 376b0a6..b09e76e 100644 --- a/src/InputMappingWindow.cpp +++ b/src/InputMappingWindow.cpp @@ -426,66 +426,24 @@ void InputMappingWindow::SliderParametersCallback(SourceCallback *callback, cons bool bd = edited->bidirectional(); if ( ImGuiToolkit::IconToggle(2, 13, 3, 13, &bd, press_tooltip ) ) edited->setBidirectional(bd); - - // get value (gst time) and convert to hh mm s.ms - guint64 ms = GST_TIME_AS_MSECONDS(edited->value()); - guint64 hh = ms / 3600000; - guint64 mm = (ms % 3600000) / 60000; - ms -= (hh * 3600000 + mm * 60000); - float sec = (float) (ms) / 1000.f; - - // filtering for reading MM:SS.MS text entry - static bool valid = true; - static std::regex RegExTime("([0-9]+\\:)?([0-9]+\\:)?([0-5][0-9]|[0-9])((\\.|\\,)[0-9]+)?"); - struct TextFilters { static int FilterTime(ImGuiInputTextCallbackData* data) { - if (data->EventChar < 256 && strchr("0123456789.,:", (char)data->EventChar)) return 0; return 1; } - }; - char buf6[64] = ""; - snprintf(buf6, 64, "%lu:%lu:%.2f", (unsigned long) hh, (unsigned long) mm, sec ); - + // Text input field for MM:SS:MS seek target time ImGui::SetNextItemWidth(right_align); ImGui::SameLine(0, IMGUI_SAME_LINE / 2); - ImGui::PushStyleColor(ImGuiCol_Text, - ImVec4(1.0f, valid ? 1.0f : 0.2f, valid ? 1.0f : 0.2f, 1.f)); - ImGui::InputText("##CALLBACK_SEEK", - buf6, - 64, - ImGuiInputTextFlags_CallbackCharFilter, - TextFilters::FilterTime); - valid = std::regex_match(buf6, RegExTime); - if (ImGui::IsItemDeactivatedAfterEdit()) { - if (valid) { - ms = 0; - sec = 0.f; - // user confirmed the entry and the input is valid - // split the "HH:MM:SS.ms" string in HH MM SS.ms - std::string time(buf6); - std::size_t found = time.find_last_of(':'); - // read the right part SS.ms as a value - if (std::string::npos != found && BaseToolkit::is_a_value(time.substr(found + 1), &sec)) { - ms = (glm::uint64)(sec * 1000.f); - // read right part MM as a number - time = time.substr(0, found); - found = time.find_last_of(':'); - int min = 0; - if (std::string::npos != found && BaseToolkit::is_a_number(time.substr(found + 1), &min)) { - ms += 60000 * (glm::uint64) min; - // read right part HH as a number - time = time.substr(0, found); - int hour = 0; - if (std::string::npos != found && BaseToolkit::is_a_number(time, &hour)) { - ms += 3600000 * (glm::uint64) hour; - } - } - } - // set time in mili seconds - edited->setValue( GST_MSECOND * ms ); - } - // force to test validity next frame - valid = false; + + guint64 duration = GST_SECOND * 1000; + if (Source * const* v = std::get_if(&target)) { + MediaSource *ms = dynamic_cast(*v); + if (ms) + duration = ms->mediaplayer()->timeline()->duration(); + } + + static bool valid = false; + guint64 target_time = edited->value(); + if ( ImGuiToolkit::InputTime("##CALLBACK_SEEK", &target_time, duration, &valid) ){ + if (valid) + edited->setValue( target_time ); } - ImGui::PopStyleColor(); ImGui::SameLine(0, IMGUI_SAME_LINE / 3); ImGuiToolkit::Indication("Target time (HH:MM:SS.MS) to set where to jump to in a video source.", 15, 7); diff --git a/src/Source.cpp b/src/Source.cpp index 19efcd0..2a918c1 100644 --- a/src/Source.cpp +++ b/src/Source.cpp @@ -406,6 +406,15 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null Source::~Source() { + // clear and delete callbacks + access_callbacks_.lock(); + for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); ) { + SourceCallback *callback = *iter; + iter = update_callbacks_.erase(iter); + delete callback; + } + access_callbacks_.unlock(); + // inform links that they lost their target while ( !links_.empty() ) links_.front()->disconnect(); @@ -431,15 +440,6 @@ Source::~Source() overlays_.clear(); frames_.clear(); handles_.clear(); - - // clear and delete callbacks - access_callbacks_.lock(); - for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); ) { - SourceCallback *callback = *iter; - iter = update_callbacks_.erase(iter); - delete callback; - } - access_callbacks_.unlock(); } void Source::setName (const std::string &name) From b382b5a204003ca1b0eb956e015d42ea6e6184f6 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Sun, 16 Nov 2025 23:19:26 +0100 Subject: [PATCH 05/15] NEW support for pen tablet pressure in Texture Painting. Applies to brush size and/or pressure. TODO: check OSX --- CMakeLists.txt | 16 +++ osx/TabletInput_macos.mm | 122 +++++++++++++++++++++++ rsc/images/icons.dds | Bin 1638528 -> 1638528 bytes rsc/shaders/mask_draw.fs | 2 +- src/CMakeLists.txt | 20 ++++ src/ImageShader.cpp | 2 +- src/RenderingManager.cpp | 14 +++ src/Settings.cpp | 2 + src/Settings.h | 2 + src/SystemToolkit.cpp | 1 - src/TabletInput.cpp | 30 ++++++ src/TabletInput.h | 92 +++++++++++++++++ src/TabletInput_linux.cpp | 205 ++++++++++++++++++++++++++++++++++++++ src/TextureView.cpp | 57 ++++++++++- 14 files changed, 557 insertions(+), 8 deletions(-) create mode 100644 osx/TabletInput_macos.mm create mode 100644 src/TabletInput.cpp create mode 100644 src/TabletInput.h create mode 100644 src/TabletInput_linux.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fdc5c2f..1932759 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,6 +103,22 @@ if(UNIX) ${X11_INCLUDE_DIR} ) + # libinput and libudev for tablet pressure support + if (PKG_CONFIG_FOUND) + pkg_check_modules(LIBINPUT libinput>=1.19) + pkg_check_modules(LIBUDEV libudev) + endif() + + if(LIBINPUT_FOUND AND LIBUDEV_FOUND) + include_directories(${LIBINPUT_INCLUDE_DIRS} ${LIBUDEV_INCLUDE_DIRS}) + link_directories(${LIBINPUT_LIBRARY_DIRS} ${LIBUDEV_LIBRARY_DIRS}) + add_definitions(-DHAVE_LIBINPUT) + macro_log_feature(LIBINPUT_FOUND "libinput" "Input device library for tablet support" "https://wayland.freedesktop.org/libinput" FALSE) + macro_log_feature(LIBUDEV_FOUND "libudev" "Device management library" "https://www.freedesktop.org/software/systemd/man/libudev.html" FALSE) + else() + message(STATUS "libinput or libudev not found - tablet pressure support will be disabled") + endif() + endif() add_definitions(-DUNIX) elseif(WIN32) diff --git a/osx/TabletInput_macos.mm b/osx/TabletInput_macos.mm new file mode 100644 index 0000000..67435c9 --- /dev/null +++ b/osx/TabletInput_macos.mm @@ -0,0 +1,122 @@ +/* + * This file is part of vimix - video live mixer + * + * **Copyright** (C) 2019-2025 Bruno Herbelin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +**/ + +#ifdef APPLE + +#include "TabletInput.h" +#include "Log.h" +#import + +TabletInput::TabletInput() + : data_({0.0f, false, 0.0f, 0.0f, false, false}) + , active_(false) + , monitor_(nullptr) +{ +} + +TabletInput::~TabletInput() +{ + terminate(); +} + +bool TabletInput::init(void* platform_handle) +{ + // Create event monitor for tablet events + NSEventMask mask = NSEventMaskTabletPoint | + NSEventMaskTabletProximity | + NSEventMaskLeftMouseDown | + NSEventMaskLeftMouseUp | + NSEventMaskLeftMouseDragged; + + // Capture reference to data_ for the block + TabletData *dataPtr = &data_; + + id monitor = [NSEvent addLocalMonitorForEventsMatchingMask:mask + handler:^NSEvent *(NSEvent *event) { + + // Check for tablet point events + if (event.type == NSEventTypeTabletPoint || + (event.subtype == NSEventSubtypeTabletPoint)) { + + dataPtr->has_pressure = true; + dataPtr->pressure = event.pressure; + dataPtr->tilt_x = event.tilt.x; + dataPtr->tilt_y = event.tilt.y; + dataPtr->tip_down = (event.pressure > 0.0f); + dataPtr->in_proximity = true; + } + // Handle proximity events + else if (event.type == NSEventTypeTabletProximity) { + dataPtr->in_proximity = event.isEnteringProximity; + if (!dataPtr->in_proximity) { + dataPtr->pressure = 0.0f; + dataPtr->tilt_x = 0.0f; + dataPtr->tilt_y = 0.0f; + dataPtr->tip_down = false; + } + } + // Fallback to regular mouse events if tablet is in proximity + else if (event.type == NSEventTypeLeftMouseDown) { + if (dataPtr->in_proximity && dataPtr->pressure == 0.0f) { + // Set minimal pressure if tablet is near but not reporting pressure + dataPtr->pressure = 0.1f; + dataPtr->tip_down = true; + } + } + else if (event.type == NSEventTypeLeftMouseUp) { + if (dataPtr->in_proximity) { + dataPtr->tip_down = false; + if (!dataPtr->in_proximity) { + dataPtr->pressure = 0.0f; + } + } + } + + return event; + }]; + + monitor_ = (__bridge_retained void*)monitor; + active_ = true; + + Log::Info("TabletInput: macOS tablet input initialized (NSEvent)"); + return true; +} + +void TabletInput::pollEvents() +{ + // Events are handled automatically by the monitor callback + // Nothing to do here for macOS +} + +void TabletInput::terminate() +{ + if (monitor_) { + id monitor = (__bridge_transfer id)monitor_; + [NSEvent removeMonitor:monitor]; + monitor_ = nullptr; + } + data_.pressure = 0.0f; + data_.tilt_x = 0.0f; + data_.tilt_y = 0.0f; + data_.in_proximity = false; + data_.tip_down = false; + active_ = false; +} + +#endif // APPLE diff --git a/rsc/images/icons.dds b/rsc/images/icons.dds index ca11b46eff85e6933114267cd7f00dfafb5f1e7d..02e16135e4827d775e02e84a08c3424fe911b5d2 100644 GIT binary patch delta 7537 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 Blt};p delta 3026 zcmbtVeQXnD7=Nz4_O4@vTf2@Wbfa?!1C*6ufRVsP#2_EZ8ZwFU2SFWX7;&H^gh)~v zMHFh_sN@9_Qc%Mh5?JRYmPla5XdpYJHt+B^_>es6)2k@c3O)cDwoqV&~ z2b|s7xo5JnnwdBucFL4{!xoRCwkQEi?4*0!7E&(M^yIR(ceuxY#~FA zd7HsAd`-X;CdbIw2}uFx^IQ&SXJ=>Ss1e?duD}LJLysBZv1v}f+(X`prC5)Rrra=Z zVx#e~gQIbqb2J7ko|>KQ`1~L{zd~ry;!V8dnb8P5h~Q0bp@-C83J;zFz2>oUn&kp< z=Sw^du42erQls%O#t^=+<^0W9T;ByzKo`#79FEoWMy+dUk1QFr0ZZK#g3$rIp__0G z8mF~5HpYR>?a3axp=DBlcm2bF!JyZSpSvqp9uPuG%ysgIw{)g73aBJ114 z!-*rU%d~hKPqrpB2ty%^aG6eng;tcieS0hibd0O{R#R>4@D{&Veq-d2C6sAX^i#INCU6-)yy*cXNUVZJ(RyXK^~K`Wn=aVyI4;~A{p@p$XSm!r`y4L@JZ@TW3@``qw81ejKm&AJ z)fk0Z)v4Vs=hbUNtYeul-?dgx zcFr*|B5_)6OrvZ6q|L?!cb5Kuf@us5*|S7;d8II^v=f7-%LUrT>mg5VFP({D&>KC2$FYmS~=f5XU0XuigeTe;(7RyR^*}^1Sc)=g0 z*ls~%I4J3{>8AN3r)(wuU84wzjxTqPzXxl0k1XfGoMQ=LHT6O!E(@=EobPYG&oVza zX9#oDqaS2cpMWykmXjZ6;z6OsBmYI$y9wbz;&%H~sgW+X7$xH^8FjJ{@iTRT3{feOmuM`}I3k6pjL1jiCo0dx<^{x(%-TNx z6i4QrK7Z(mo@{GQzSj}E?G`ex_xYzkk&W&Bp3o6{JK%U4(7EMz|18yGG+ z*+Q8|i_f)S`EBu*Ja^upOMzn2rFjI%=z2gbqN@d?{--gLD~gJAvt)vh*%7RG=qx_J gaB-oGPAn?Qhterminate(); diff --git a/src/Settings.cpp b/src/Settings.cpp index 10df361..f9b0d58 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -247,6 +247,7 @@ void Settings::Save(uint64_t runtime, const std::string &filename) // Brush XMLElement *BrushNode = xmlDoc.NewElement( "Brush" ); BrushNode->InsertEndChild( XMLElementFromGLM(&xmlDoc, application.brush) ); + BrushNode->SetAttribute("brush_pressure_mode", application.brush_pressure_mode); pRoot->InsertEndChild(BrushNode); // Pointer @@ -700,6 +701,7 @@ void Settings::Load(const std::string &filename) XMLElement * brushnode = pRoot->FirstChildElement("Brush"); if (brushnode != nullptr) { tinyxml2::XMLElementToGLM( brushnode->FirstChildElement("vec3"), application.brush); + brushnode->QueryIntAttribute("brush_pressure_mode", &application.brush_pressure_mode); } // Pointer diff --git a/src/Settings.h b/src/Settings.h index 4b0a7be..b8b779b 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -325,6 +325,7 @@ struct Application // settings brush texture paint glm::vec3 brush; + int brush_pressure_mode; // settings render RenderConfig render; @@ -396,6 +397,7 @@ struct Application current_view = 1; current_workspace= 3; brush = glm::vec3(0.5f, 0.1f, 0.f); + brush_pressure_mode = 0; num_output_windows = 1; windows = std::vector(1+MAX_OUTPUT_WINDOW); windows[0].w = 1600; diff --git a/src/SystemToolkit.cpp b/src/SystemToolkit.cpp index cc12a61..e8c19eb 100644 --- a/src/SystemToolkit.cpp +++ b/src/SystemToolkit.cpp @@ -26,7 +26,6 @@ #include #include #include -#include using namespace std; diff --git a/src/TabletInput.cpp b/src/TabletInput.cpp new file mode 100644 index 0000000..0d6fa94 --- /dev/null +++ b/src/TabletInput.cpp @@ -0,0 +1,30 @@ +/* + * This file is part of vimix - video live mixer + * + * **Copyright** (C) 2019-2023 Bruno Herbelin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +**/ + +#include "TabletInput.h" + +TabletInput& TabletInput::instance() +{ + static TabletInput instance; + return instance; +} + +// Platform-specific implementations are in separate files: +// - TabletInput_linux.cpp (Linux with libinput) +// - TabletInput_macos.mm (macOS with NSEvent) diff --git a/src/TabletInput.h b/src/TabletInput.h new file mode 100644 index 0000000..70f7e6b --- /dev/null +++ b/src/TabletInput.h @@ -0,0 +1,92 @@ +/* + * This file is part of vimix - video live mixer + * + * **Copyright** (C) 2019-2023 Bruno Herbelin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +**/ + +#ifndef TABLETINPUT_H +#define TABLETINPUT_H + +#include + +// Platform-specific forward declarations +#if defined(LINUX) && defined(HAVE_LIBINPUT) +struct libinput; +struct udev; +#elif defined(APPLE) +#ifdef __OBJC__ +@class NSEvent; +#else +class NSEvent; +#endif +#endif + +/** + * @brief Cross-platform tablet/stylus input manager + * + * Provides normalized pressure values (0.0-1.0) from pen/stylus devices + * across Linux (libinput) and macOS (NSEvent) + */ +class TabletInput { +public: + struct TabletData { + float pressure; // 0.0 - 1.0 + bool has_pressure; // Stylius has pressure + float tilt_x; // -1.0 to 1.0 (optional) + float tilt_y; // -1.0 to 1.0 (optional) + bool in_proximity; // Is stylus near/touching surface + bool tip_down; // Is stylus tip pressed + }; + + static TabletInput& instance(); + + // Initialize tablet input system + bool init(void* platform_handle = nullptr); + + // Poll for new tablet events (call once per frame) + void pollEvents(); + + // Clean up resources + void terminate(); + + // Get current tablet data + const TabletData& getData() const { return data_; } + + // Quick accessors + float getPressure() const { return data_.pressure; } + bool isPressed() const { return data_.tip_down || data_.in_proximity; } + + // status + bool isEnabled() const { return active_; } + bool hasPressure() const { return isEnabled() && data_.has_pressure; } + +private: + TabletInput(); + ~TabletInput(); + + TabletData data_; + bool active_; + +#if defined(LINUX) && defined(HAVE_LIBINPUT) + struct udev *udev_; + struct libinput *li_; + int fd_; +#elif defined(APPLE) + void* monitor_; // Event monitor handle +#endif +}; + +#endif // TABLETINPUT_H diff --git a/src/TabletInput_linux.cpp b/src/TabletInput_linux.cpp new file mode 100644 index 0000000..ee6027e --- /dev/null +++ b/src/TabletInput_linux.cpp @@ -0,0 +1,205 @@ +/* + * This file is part of vimix - video live mixer + * + * **Copyright** (C) 2019-2025 Bruno Herbelin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +**/ + +#ifdef LINUX + +#include "TabletInput.h" +#include "Log.h" + +#ifdef HAVE_LIBINPUT +#include +#include +#include +#include +#include + +static int open_restricted(const char *path, int flags, void *user_data) +{ + int fd = open(path, flags); + return fd < 0 ? -errno : fd; +} + +static void close_restricted(int fd, void *user_data) +{ + close(fd); +} + +static const struct libinput_interface interface = { + .open_restricted = open_restricted, + .close_restricted = close_restricted, +}; + +TabletInput::TabletInput() + : data_({0.0f, false, 0.0f, 0.0f, false, false}) + , active_(false) + , udev_(nullptr) + , li_(nullptr) + , fd_(-1) +{ +} + +TabletInput::~TabletInput() +{ + terminate(); +} + +bool TabletInput::init(void* platform_handle) +{ + udev_ = udev_new(); + if (!udev_) { + Log::Info("TabletInput: Failed to initialize udev"); + return false; + } + + li_ = libinput_udev_create_context(&interface, nullptr, udev_); + if (!li_) { + Log::Info("TabletInput: Failed to create libinput context"); + udev_unref(udev_); + udev_ = nullptr; + return false; + } + + if (libinput_udev_assign_seat(li_, "seat0") != 0) { + Log::Info("TabletInput: Failed to assign seat"); + libinput_unref(li_); + li_ = nullptr; + udev_unref(udev_); + udev_ = nullptr; + return false; + } + + fd_ = libinput_get_fd(li_); + + // Set non-blocking + int flags = fcntl(fd_, F_GETFL, 0); + fcntl(fd_, F_SETFL, flags | O_NONBLOCK); + + active_ = true; + Log::Info("TabletInput: Linux tablet input initialized (libinput)"); + return true; +} + +void TabletInput::pollEvents() +{ + if (!li_) return; + + libinput_dispatch(li_); + + struct libinput_event *event; + while ((event = libinput_get_event(li_)) != nullptr) { + enum libinput_event_type type = libinput_event_get_type(event); + + switch (type) { + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: + case LIBINPUT_EVENT_TABLET_TOOL_TIP: + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: { + struct libinput_event_tablet_tool *tablet_event = + libinput_event_get_tablet_tool_event(event); + + // Update pressure + if (libinput_event_tablet_tool_pressure_has_changed(tablet_event)) { + data_.has_pressure = true; + data_.pressure = libinput_event_tablet_tool_get_pressure(tablet_event); + } + + // Update tilt (if available) + if (libinput_event_tablet_tool_tilt_x_has_changed(tablet_event)) { + data_.tilt_x = libinput_event_tablet_tool_get_tilt_x(tablet_event) / 90.0f; + } + if (libinput_event_tablet_tool_tilt_y_has_changed(tablet_event)) { + data_.tilt_y = libinput_event_tablet_tool_get_tilt_y(tablet_event) / 90.0f; + } + + // Update proximity + if (type == LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY) { + data_.in_proximity = (libinput_event_tablet_tool_get_proximity_state(tablet_event) + == LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + if (!data_.in_proximity) { + data_.has_pressure = true; + data_.pressure = 0.0f; + data_.tip_down = false; + } + } + + // Update tip state + if (type == LIBINPUT_EVENT_TABLET_TOOL_TIP) { + data_.tip_down = (libinput_event_tablet_tool_get_tip_state(tablet_event) + == LIBINPUT_TABLET_TOOL_TIP_DOWN); + if (!data_.tip_down) { + data_.has_pressure = true; + data_.pressure = 0.0f; + } + } + break; + } + default: + break; + } + + libinput_event_destroy(event); + } +} + +void TabletInput::terminate() +{ + if (li_) { + libinput_unref(li_); + li_ = nullptr; + } + if (udev_) { + udev_unref(udev_); + udev_ = nullptr; + } + active_ = false; +} + +#else // !HAVE_LIBINPUT + +// Stub implementation when libinput is not available + +TabletInput::TabletInput() + : data_({0.0f, false, 0.0f, 0.0f, false, false}) + , active_(false) +{ +} + +TabletInput::~TabletInput() +{ +} + +bool TabletInput::init(void* platform_handle) +{ + Log::Info("TabletInput: libinput not available - tablet pressure support disabled"); + active_ = false; + return false; +} + +void TabletInput::pollEvents() +{ + // No-op when libinput is not available +} + +void TabletInput::terminate() +{ + active_ = false; +} + +#endif // HAVE_LIBINPUT + +#endif // LINUX diff --git a/src/TextureView.cpp b/src/TextureView.cpp index bc49767..4f3c697 100644 --- a/src/TextureView.cpp +++ b/src/TextureView.cpp @@ -17,7 +17,10 @@ * along with this program. If not, see . **/ +#include "IconsFontAwesome5.h" #include +#include +#include #include #include #include @@ -43,6 +46,14 @@ #include "ActionManager.h" #include "DialogToolkit.h" #include "MousePointer.h" +#include "TabletInput.h" + +enum TabletInputFlags_ +{ + TabletInput_none = 0, + TabletInput_brush_size = 1 << 1, + TabletInput_brush_pressure = 1 << 2 +}; #include "TextureView.h" @@ -227,7 +238,8 @@ TextureView::TextureView() : View(TEXTURE), edit_source_(nullptr), need_edit_upd stored_mask_size_ = glm::vec3(0.f); show_cursor_forced_ = false; - scene_brush_pos = glm::vec3(0.f); + scene_brush_pos = glm::vec3(100.f); + previous_scene_brush_pos = glm::vec3(0.f); // replace grid with appropriate one translation_grid_ = new TranslationGrid(scene.root()); @@ -795,7 +807,7 @@ void TextureView::draw() if (mask_cursor_paint_ > 0) { ImGui::SameLine(0, 50); - if (ImGui::Button(ICON_FA_PEN ICON_FA_SORT_DOWN )) + if (ImGui::Button(ICON_FA_PEN_NIB ICON_FA_SORT_DOWN )) ImGui::OpenPopup("brush_shape_popup"); if (ImGui::IsItemHovered()) ImGuiToolkit::ToolTip("Shape"); @@ -823,6 +835,17 @@ void TextureView::draw() int pixel_size = int(Settings::application.brush.x * edit_source_->frame()->height() ); show_cursor_forced_ = true; ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT); + // toggle to enable tablet input on brush size + if(TabletInput::instance().isEnabled() && TabletInput::instance().hasPressure()) { + static bool enable_tablet_input = false; + enable_tablet_input = Settings::application.brush_pressure_mode & TabletInput_brush_size; + ImGuiToolkit::ButtonIconToggle(13, 0, &enable_tablet_input, "Tablet pressure sensitive"); + if (enable_tablet_input) + Settings::application.brush_pressure_mode |= TabletInput_brush_size; + else + Settings::application.brush_pressure_mode &= ~TabletInput_brush_size; + } + // max brush size ImGuiToolkit::Indication("Large ", 16, 1); if (ImGui::VSliderInt("##BrushSize", ImVec2(30,260), &pixel_size, pixel_size_min, pixel_size_max, "") ){ Settings::application.brush.x = CLAMP(float(pixel_size) / edit_source_->frame()->height(), BRUSH_MIN_SIZE, BRUSH_MAX_SIZE); @@ -849,6 +872,17 @@ void TextureView::draw() if (ImGui::BeginPopup("brush_pressure_popup", ImGuiWindowFlags_NoMove)) { ImGuiToolkit::PushFont(ImGuiToolkit::FONT_DEFAULT); + // toggle to enable tablet input on brush pressure + if(TabletInput::instance().isEnabled() && TabletInput::instance().hasPressure()) { + static bool enable_tablet_input = false; + enable_tablet_input = Settings::application.brush_pressure_mode & TabletInput_brush_pressure; + ImGuiToolkit::ButtonIconToggle(13, 0, &enable_tablet_input, "Tablet pressure sensitive"); + if (enable_tablet_input) + Settings::application.brush_pressure_mode |= TabletInput_brush_pressure; + else + Settings::application.brush_pressure_mode &= ~TabletInput_brush_pressure; + } + // max brush pressure ImGuiToolkit::Indication("Light ", ICON_FA_FEATHER_ALT); ImGui::VSliderFloat("##BrushPressure", ImVec2(30,260), &Settings::application.brush.y, BRUSH_MAX_PRESS, BRUSH_MIN_PRESS, "", 0.3f); if (ImGui::IsItemHovered() || ImGui::IsItemActive() ) { @@ -892,7 +926,7 @@ void TextureView::draw() } if (e>0) { edit_source_->maskShader()->effect = e; - edit_source_->maskShader()->cursor = glm::vec4(100.0, 100.0, 0.f, 0.f); + edit_source_->maskShader()->cursor = glm::vec4(100.0, 100.0,100.f, 100.f); edit_source_->touch(Source::SourceUpdate_Mask); Action::manager().store(oss.str()); } @@ -1119,7 +1153,19 @@ View::Cursor TextureView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pa scene_brush_pos = grid->snap(scene_brush_pos); // inform shader of a cursor action : coordinates and crop scaling edit_source_->maskShader()->size = edit_source_->mixingsurface_->scale_; - + + // Apply tablet pressure to brush if available + if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) { + float tablet_pressure = TabletInput::instance().getPressure(); + // apply pressure depending on mode + // scale size of brush + if (Settings::application.brush_pressure_mode & TabletInput_brush_size) + edit_source_->maskShader()->brush.x = CLAMP( Settings::application.brush.x * tablet_pressure, BRUSH_MIN_SIZE, BRUSH_MAX_SIZE); + // transparency pressure + if (Settings::application.brush_pressure_mode & TabletInput_brush_pressure) + edit_source_->maskShader()->brush.y = CLAMP( Settings::application.brush.x * tablet_pressure, BRUSH_MIN_PRESS, BRUSH_MAX_PRESS); + } + // inform shader of a cursor action : coordinates and crop scaling edit_source_->maskShader()->cursor = glm::vec4(scene_brush_pos.x - shift_crop_.x, scene_brush_pos.y - shift_crop_.y, @@ -1560,7 +1606,8 @@ void TextureView::terminate(bool force) // special case for texture paint: store image on mouse release (end of action PAINT) if ( edit_source_ != nullptr && current_action_.find(MaskShader::mask_names[MaskShader::PAINT]) != std::string::npos ) { edit_source_->storeMask(); - edit_source_->maskShader()->cursor = glm::vec4(100.0, 100.0, 0.f, 0.f); + edit_source_->maskShader()->cursor = glm::vec4(100.0, 100.0, 100.f, 100.f); + edit_source_->maskShader()->size = glm::vec2(stored_mask_size_); } // View default termination of action From 574058bec21f4ae41e5d0caf28d8e67b7de7fbc4 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 11:16:51 +0100 Subject: [PATCH 06/15] Re-implementation of TabletInput for Linux with X11 input for compatibility with Flatpak --- CMakeLists.txt | 34 ++-- osx/TabletInput_macos.mm | 2 +- src/CMakeLists.txt | 17 +- src/TabletInput.h | 21 ++- src/TabletInput_linux.cpp | 205 ------------------------ src/TabletInput_linux_libinput.cpp | 206 ++++++++++++++++++++++++ src/TabletInput_x11.cpp | 249 +++++++++++++++++++++++++++++ 7 files changed, 510 insertions(+), 224 deletions(-) create mode 100644 src/TabletInput_linux_libinput.cpp create mode 100644 src/TabletInput_x11.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1932759..64cfc4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,20 +103,28 @@ if(UNIX) ${X11_INCLUDE_DIR} ) - # libinput and libudev for tablet pressure support - if (PKG_CONFIG_FOUND) - pkg_check_modules(LIBINPUT libinput>=1.19) - pkg_check_modules(LIBUDEV libudev) - endif() - - if(LIBINPUT_FOUND AND LIBUDEV_FOUND) - include_directories(${LIBINPUT_INCLUDE_DIRS} ${LIBUDEV_INCLUDE_DIRS}) - link_directories(${LIBINPUT_LIBRARY_DIRS} ${LIBUDEV_LIBRARY_DIRS}) - add_definitions(-DHAVE_LIBINPUT) - macro_log_feature(LIBINPUT_FOUND "libinput" "Input device library for tablet support" "https://wayland.freedesktop.org/libinput" FALSE) - macro_log_feature(LIBUDEV_FOUND "libudev" "Device management library" "https://www.freedesktop.org/software/systemd/man/libudev.html" FALSE) + # XInput2 for tablet pressure support (optional) + find_package(X11 COMPONENTS Xi) + if(X11_Xi_FOUND) + add_definitions(-DHAVE_X11TABLETINPUT) + macro_log_feature(X11_Xi_FOUND "XInput2" "X11 Input extension for tablet support" "https://www.x.org/wiki/" FALSE) else() - message(STATUS "libinput or libudev not found - tablet pressure support will be disabled") + + # alternatively libinput and libudev for tablet pressure support + if (PKG_CONFIG_FOUND) + pkg_check_modules(LIBINPUT libinput>=1.19) + pkg_check_modules(LIBUDEV libudev) + endif() + + if(LIBINPUT_FOUND AND LIBUDEV_FOUND) + include_directories(${LIBINPUT_INCLUDE_DIRS} ${LIBUDEV_INCLUDE_DIRS}) + link_directories(${LIBINPUT_LIBRARY_DIRS} ${LIBUDEV_LIBRARY_DIRS}) + add_definitions(-DHAVE_LIBINPUT) + macro_log_feature(LIBINPUT_FOUND "libinput" "Input device library for tablet support" "https://wayland.freedesktop.org/libinput" FALSE) + macro_log_feature(LIBUDEV_FOUND "libudev" "Device management library" "https://www.freedesktop.org/software/systemd/man/libudev.html" FALSE) + else() + message(STATUS "XInput2 (Xi) or libinput or libudev not found - tablet pressure support will be disabled") + endif() endif() endif() diff --git a/osx/TabletInput_macos.mm b/osx/TabletInput_macos.mm index 67435c9..7d4411b 100644 --- a/osx/TabletInput_macos.mm +++ b/osx/TabletInput_macos.mm @@ -35,7 +35,7 @@ TabletInput::~TabletInput() terminate(); } -bool TabletInput::init(void* platform_handle) +bool TabletInput::init() { // Create event monitor for tablet events NSEventMask mask = NSEventMaskTabletPoint | diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f129b9c..7cedef9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -120,7 +120,15 @@ IF(APPLE) ELSE(APPLE) # Add Linux tablet input implementation - list(APPEND VMIX_SRCS TabletInput_linux.cpp) + if(X11_Xi_FOUND) + # Use X11/XInput2 version (works in Flatpak and everywhere) + list(APPEND VMIX_SRCS TabletInput_x11.cpp) + else() + if(LIBINPUT_FOUND AND LIBUDEV_FOUND) + # Use X11/XInput2 version (works in Flatpak and everywhere) + list(APPEND VMIX_SRCS TabletInput_linux_libinput.cpp) + endif() + endif() ENDIF(APPLE) @@ -164,7 +172,12 @@ ELSE(APPLE) X11::xcb ) - # Add libinput and libudev for tablet support on Linux + # Add XInput2 library for tablet support on Linux (if available) + if(X11_Xi_FOUND) + list(APPEND PLATFORM_LIBS ${X11_Xi_LIB}) + endif() + + # Add libinput and libudev for tablet support on Linux (if available) if(LIBINPUT_FOUND AND LIBUDEV_FOUND) list(APPEND PLATFORM_LIBS ${LIBINPUT_LIBRARIES} ${LIBUDEV_LIBRARIES}) endif() diff --git a/src/TabletInput.h b/src/TabletInput.h index 70f7e6b..281a206 100644 --- a/src/TabletInput.h +++ b/src/TabletInput.h @@ -23,9 +23,13 @@ #include // Platform-specific forward declarations -#if defined(LINUX) && defined(HAVE_LIBINPUT) +#ifdef LINUX +// X11 forward declarations (for XInput2) +typedef struct _XDisplay Display; +#if defined(HAVE_LIBINPUT) struct libinput; struct udev; +#endif #elif defined(APPLE) #ifdef __OBJC__ @class NSEvent; @@ -54,7 +58,7 @@ public: static TabletInput& instance(); // Initialize tablet input system - bool init(void* platform_handle = nullptr); + bool init(); // Poll for new tablet events (call once per frame) void pollEvents(); @@ -80,10 +84,21 @@ private: TabletData data_; bool active_; -#if defined(LINUX) && defined(HAVE_LIBINPUT) +#ifdef LINUX +#if defined(HAVE_X11TABLETINPUT) + // X11/XInput2 members (used when libinput is not available or in Flatpak) + Display *display_; + int xi_opcode_; + int pressure_valuator_; + int tilt_x_valuator_; + int tilt_y_valuator_; +#endif +#if defined(HAVE_LIBINPUT) + // libinput members (used for native builds with libinput) struct udev *udev_; struct libinput *li_; int fd_; +#endif #elif defined(APPLE) void* monitor_; // Event monitor handle #endif diff --git a/src/TabletInput_linux.cpp b/src/TabletInput_linux.cpp index ee6027e..e69de29 100644 --- a/src/TabletInput_linux.cpp +++ b/src/TabletInput_linux.cpp @@ -1,205 +0,0 @@ -/* - * This file is part of vimix - video live mixer - * - * **Copyright** (C) 2019-2025 Bruno Herbelin - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -**/ - -#ifdef LINUX - -#include "TabletInput.h" -#include "Log.h" - -#ifdef HAVE_LIBINPUT -#include -#include -#include -#include -#include - -static int open_restricted(const char *path, int flags, void *user_data) -{ - int fd = open(path, flags); - return fd < 0 ? -errno : fd; -} - -static void close_restricted(int fd, void *user_data) -{ - close(fd); -} - -static const struct libinput_interface interface = { - .open_restricted = open_restricted, - .close_restricted = close_restricted, -}; - -TabletInput::TabletInput() - : data_({0.0f, false, 0.0f, 0.0f, false, false}) - , active_(false) - , udev_(nullptr) - , li_(nullptr) - , fd_(-1) -{ -} - -TabletInput::~TabletInput() -{ - terminate(); -} - -bool TabletInput::init(void* platform_handle) -{ - udev_ = udev_new(); - if (!udev_) { - Log::Info("TabletInput: Failed to initialize udev"); - return false; - } - - li_ = libinput_udev_create_context(&interface, nullptr, udev_); - if (!li_) { - Log::Info("TabletInput: Failed to create libinput context"); - udev_unref(udev_); - udev_ = nullptr; - return false; - } - - if (libinput_udev_assign_seat(li_, "seat0") != 0) { - Log::Info("TabletInput: Failed to assign seat"); - libinput_unref(li_); - li_ = nullptr; - udev_unref(udev_); - udev_ = nullptr; - return false; - } - - fd_ = libinput_get_fd(li_); - - // Set non-blocking - int flags = fcntl(fd_, F_GETFL, 0); - fcntl(fd_, F_SETFL, flags | O_NONBLOCK); - - active_ = true; - Log::Info("TabletInput: Linux tablet input initialized (libinput)"); - return true; -} - -void TabletInput::pollEvents() -{ - if (!li_) return; - - libinput_dispatch(li_); - - struct libinput_event *event; - while ((event = libinput_get_event(li_)) != nullptr) { - enum libinput_event_type type = libinput_event_get_type(event); - - switch (type) { - case LIBINPUT_EVENT_TABLET_TOOL_AXIS: - case LIBINPUT_EVENT_TABLET_TOOL_TIP: - case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: { - struct libinput_event_tablet_tool *tablet_event = - libinput_event_get_tablet_tool_event(event); - - // Update pressure - if (libinput_event_tablet_tool_pressure_has_changed(tablet_event)) { - data_.has_pressure = true; - data_.pressure = libinput_event_tablet_tool_get_pressure(tablet_event); - } - - // Update tilt (if available) - if (libinput_event_tablet_tool_tilt_x_has_changed(tablet_event)) { - data_.tilt_x = libinput_event_tablet_tool_get_tilt_x(tablet_event) / 90.0f; - } - if (libinput_event_tablet_tool_tilt_y_has_changed(tablet_event)) { - data_.tilt_y = libinput_event_tablet_tool_get_tilt_y(tablet_event) / 90.0f; - } - - // Update proximity - if (type == LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY) { - data_.in_proximity = (libinput_event_tablet_tool_get_proximity_state(tablet_event) - == LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); - if (!data_.in_proximity) { - data_.has_pressure = true; - data_.pressure = 0.0f; - data_.tip_down = false; - } - } - - // Update tip state - if (type == LIBINPUT_EVENT_TABLET_TOOL_TIP) { - data_.tip_down = (libinput_event_tablet_tool_get_tip_state(tablet_event) - == LIBINPUT_TABLET_TOOL_TIP_DOWN); - if (!data_.tip_down) { - data_.has_pressure = true; - data_.pressure = 0.0f; - } - } - break; - } - default: - break; - } - - libinput_event_destroy(event); - } -} - -void TabletInput::terminate() -{ - if (li_) { - libinput_unref(li_); - li_ = nullptr; - } - if (udev_) { - udev_unref(udev_); - udev_ = nullptr; - } - active_ = false; -} - -#else // !HAVE_LIBINPUT - -// Stub implementation when libinput is not available - -TabletInput::TabletInput() - : data_({0.0f, false, 0.0f, 0.0f, false, false}) - , active_(false) -{ -} - -TabletInput::~TabletInput() -{ -} - -bool TabletInput::init(void* platform_handle) -{ - Log::Info("TabletInput: libinput not available - tablet pressure support disabled"); - active_ = false; - return false; -} - -void TabletInput::pollEvents() -{ - // No-op when libinput is not available -} - -void TabletInput::terminate() -{ - active_ = false; -} - -#endif // HAVE_LIBINPUT - -#endif // LINUX diff --git a/src/TabletInput_linux_libinput.cpp b/src/TabletInput_linux_libinput.cpp new file mode 100644 index 0000000..eb4d881 --- /dev/null +++ b/src/TabletInput_linux_libinput.cpp @@ -0,0 +1,206 @@ +/* + * This file is part of vimix - video live mixer + * + * **Copyright** (C) 2019-2025 Bruno Herbelin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +**/ + +#include +#ifdef LINUX + +#include "TabletInput.h" +#include "Log.h" + +#ifdef HAVE_LIBINPUT +#include +#include +#include +#include +#include + +static int open_restricted(const char *path, int flags, void *user_data) +{ + int fd = open(path, flags); + return fd < 0 ? -errno : fd; +} + +static void close_restricted(int fd, void *user_data) +{ + close(fd); +} + +static const struct libinput_interface interface = { + .open_restricted = open_restricted, + .close_restricted = close_restricted, +}; + +TabletInput::TabletInput() + : data_({0.0f, false, 0.0f, 0.0f, false, false}) + , active_(false) + , udev_(nullptr) + , li_(nullptr) + , fd_(-1) +{ +} + +TabletInput::~TabletInput() +{ + terminate(); +} + +bool TabletInput::init() +{ + udev_ = udev_new(); + if (!udev_) { + Log::Info("TabletInput: Failed to initialize udev"); + return false; + } + + li_ = libinput_udev_create_context(&interface, nullptr, udev_); + if (!li_) { + Log::Info("TabletInput: Failed to create libinput context"); + udev_unref(udev_); + udev_ = nullptr; + return false; + } + + if (libinput_udev_assign_seat(li_, "seat0") != 0) { + Log::Info("TabletInput: Failed to assign seat"); + libinput_unref(li_); + li_ = nullptr; + udev_unref(udev_); + udev_ = nullptr; + return false; + } + + fd_ = libinput_get_fd(li_); + + // Set non-blocking + int flags = fcntl(fd_, F_GETFL, 0); + fcntl(fd_, F_SETFL, flags | O_NONBLOCK); + + active_ = true; + Log::Info("TabletInput: Linux tablet input initialized (libinput)"); + return true; +} + +void TabletInput::pollEvents() +{ + if (!li_) return; + + libinput_dispatch(li_); + + struct libinput_event *event; + while ((event = libinput_get_event(li_)) != nullptr) { + enum libinput_event_type type = libinput_event_get_type(event); + + switch (type) { + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: + case LIBINPUT_EVENT_TABLET_TOOL_TIP: + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: { + struct libinput_event_tablet_tool *tablet_event = + libinput_event_get_tablet_tool_event(event); + + // Update pressure + if (libinput_event_tablet_tool_pressure_has_changed(tablet_event)) { + data_.has_pressure = true; + data_.pressure = libinput_event_tablet_tool_get_pressure(tablet_event); + } + + // Update tilt (if available) + if (libinput_event_tablet_tool_tilt_x_has_changed(tablet_event)) { + data_.tilt_x = libinput_event_tablet_tool_get_tilt_x(tablet_event) / 90.0f; + } + if (libinput_event_tablet_tool_tilt_y_has_changed(tablet_event)) { + data_.tilt_y = libinput_event_tablet_tool_get_tilt_y(tablet_event) / 90.0f; + } + + // Update proximity + if (type == LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY) { + data_.in_proximity = (libinput_event_tablet_tool_get_proximity_state(tablet_event) + == LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN); + if (!data_.in_proximity) { + data_.has_pressure = true; + data_.pressure = 0.0f; + data_.tip_down = false; + } + } + + // Update tip state + if (type == LIBINPUT_EVENT_TABLET_TOOL_TIP) { + data_.tip_down = (libinput_event_tablet_tool_get_tip_state(tablet_event) + == LIBINPUT_TABLET_TOOL_TIP_DOWN); + if (!data_.tip_down) { + data_.has_pressure = true; + data_.pressure = 0.0f; + } + } + break; + } + default: + break; + } + + libinput_event_destroy(event); + } +} + +void TabletInput::terminate() +{ + if (li_) { + libinput_unref(li_); + li_ = nullptr; + } + if (udev_) { + udev_unref(udev_); + udev_ = nullptr; + } + active_ = false; +} + +#else // !HAVE_LIBINPUT + +// Stub implementation when libinput is not available + +TabletInput::TabletInput() + : data_({0.0f, false, 0.0f, 0.0f, false, false}) + , active_(false) +{ +} + +TabletInput::~TabletInput() +{ +} + +bool TabletInput::init() +{ + Log::Info("TabletInput: libinput not available - tablet pressure support disabled"); + active_ = false; + return false; +} + +void TabletInput::pollEvents() +{ + // No-op when libinput is not available +} + +void TabletInput::terminate() +{ + active_ = false; +} + +#endif // HAVE_LIBINPUT + +#endif // LINUX diff --git a/src/TabletInput_x11.cpp b/src/TabletInput_x11.cpp new file mode 100644 index 0000000..4150831 --- /dev/null +++ b/src/TabletInput_x11.cpp @@ -0,0 +1,249 @@ +/* + * This file is part of vimix - video live mixer + * + * **Copyright** (C) 2019-2023 Bruno Herbelin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +**/ + +#ifdef LINUX + +#include "TabletInput.h" +#include "Log.h" + +#ifdef HAVE_X11TABLETINPUT +#include +#include +#include +#endif + +TabletInput::TabletInput() + : data_({0.0f, false, 0.0f, 0.0f, false, false}) + , active_(false) +#ifdef HAVE_X11TABLETINPUT + , display_(nullptr) + , xi_opcode_(-1) + , pressure_valuator_(-1) + , tilt_x_valuator_(-1) + , tilt_y_valuator_(-1) +#endif +{ +} + +TabletInput::~TabletInput() +{ + terminate(); +} + +bool TabletInput::init() +{ +#ifdef HAVE_X11TABLETINPUT + // Open X11 display connection + display_ = XOpenDisplay(nullptr); + if (!display_) { + Log::Info("TabletInput: Failed to open X11 display"); + return false; + } + + // Check for XInput2 extension + int event, error; + if (!XQueryExtension(display_, "XInputExtension", &xi_opcode_, &event, &error)) { + Log::Info("TabletInput: XInput extension not available"); + XCloseDisplay(display_); + display_ = nullptr; + return false; + } + + // Check XInput2 version + int major = 2, minor = 2; + if (XIQueryVersion(display_, &major, &minor) != Success) { + Log::Info("TabletInput: XInput2 2.2 not available"); + XCloseDisplay(display_); + display_ = nullptr; + return false; + } + + // Select events for all devices + XIEventMask eventmask; + unsigned char mask[XIMaskLen(XI_LASTEVENT)] = {0}; + + XISetMask(mask, XI_Motion); + XISetMask(mask, XI_ButtonPress); + XISetMask(mask, XI_ButtonRelease); + + eventmask.deviceid = XIAllDevices; + eventmask.mask_len = sizeof(mask); + eventmask.mask = mask; + + Window root = DefaultRootWindow(display_); + XISelectEvents(display_, root, &eventmask, 1); + + // Find tablet devices and their pressure valuators + int ndevices; + XIDeviceInfo *devices = XIQueryDevice(display_, XIAllDevices, &ndevices); + + for (int i = 0; i < ndevices; i++) { + XIDeviceInfo *device = &devices[i]; + + // Look for devices that have valuators (tablets, styluses) + if (device->use == XISlavePointer || device->use == XIFloatingSlave) { + // Check valuator classes for pressure + for (int j = 0; j < device->num_classes; j++) { + if (device->classes[j]->type == XIValuatorClass) { + XIValuatorClassInfo *v = (XIValuatorClassInfo*)device->classes[j]; + + // Try to identify pressure axis + // Pressure is usually valuator 2, but we check the label + Atom pressure_atom = XInternAtom(display_, "Abs Pressure", True); + Atom tilt_x_atom = XInternAtom(display_, "Abs Tilt X", True); + Atom tilt_y_atom = XInternAtom(display_, "Abs Tilt Y", True); + + if (v->label == pressure_atom) { + pressure_valuator_ = v->number; + Log::Info("TabletInput: Found pressure valuator %d on device '%s'", + pressure_valuator_, device->name); + } + else if (v->label == tilt_x_atom) { + tilt_x_valuator_ = v->number; + } + else if (v->label == tilt_y_atom) { + tilt_y_valuator_ = v->number; + } + // Fallback: assume valuator 2 is pressure if we haven't found it + else if (pressure_valuator_ == -1 && v->number == 2) { + pressure_valuator_ = v->number; + Log::Info("TabletInput: Using valuator 2 as pressure (fallback)"); + } + } + } + } + } + + XIFreeDeviceInfo(devices); + + if (pressure_valuator_ == -1) { + Log::Info("TabletInput: No pressure valuator found - tablet may not be connected"); + // Don't fail init, just continue without pressure detection + } + else { + data_.has_pressure = true; + } + + XFlush(display_); + active_ = true; + Log::Info("TabletInput: X11/XInput2 tablet input initialized"); + return true; +#else + Log::Info("TabletInput: XInput2 not available - tablet support disabled"); + return false; +#endif +} + +void TabletInput::pollEvents() +{ +#ifdef HAVE_X11TABLETINPUT + if (!display_) return; + + // Process all pending X11 events + while (XPending(display_) > 0) { + XEvent ev; + XNextEvent(display_, &ev); + + // Check for XInput2 events + if (ev.type == GenericEvent && ev.xcookie.extension == xi_opcode_) { + if (XGetEventData(display_, &ev.xcookie)) { + XIDeviceEvent *device_event = (XIDeviceEvent*)ev.xcookie.data; + + switch (ev.xcookie.evtype) { + case XI_Motion: + case XI_ButtonPress: + case XI_ButtonRelease: { + // Extract pressure from valuators + if (pressure_valuator_ >= 0) { + double *values = device_event->valuators.values; + unsigned char *mask = device_event->valuators.mask; + + int val_index = 0; + for (int i = 0; i <= pressure_valuator_; i++) { + if (XIMaskIsSet(mask, i)) { + if (i == pressure_valuator_) { + // Normalize pressure (typically 0-65535 range) + data_.pressure = values[val_index] / 65535.0f; + if (data_.pressure > 1.0f) data_.pressure = 1.0f; + if (data_.pressure < 0.0f) data_.pressure = 0.0f; + } + val_index++; + } + } + } + + // Extract tilt + if (tilt_x_valuator_ >= 0 || tilt_y_valuator_ >= 0) { + double *values = device_event->valuators.values; + unsigned char *mask = device_event->valuators.mask; + + int val_index = 0; + int max_valuator = tilt_x_valuator_ > tilt_y_valuator_ ? + tilt_x_valuator_ : tilt_y_valuator_; + + for (int i = 0; i <= max_valuator; i++) { + if (XIMaskIsSet(mask, i)) { + if (i == tilt_x_valuator_) { + data_.tilt_x = (values[val_index] - 32767.5f) / 32767.5f; + } + if (i == tilt_y_valuator_) { + data_.tilt_y = (values[val_index] - 32767.5f) / 32767.5f; + } + val_index++; + } + } + } + + // Update button state + if (ev.xcookie.evtype == XI_ButtonPress) { + data_.tip_down = true; + data_.in_proximity = true; + } + else if (ev.xcookie.evtype == XI_ButtonRelease) { + data_.tip_down = false; + if (data_.pressure < 0.01f) { + data_.in_proximity = false; + } + } + else if (ev.xcookie.evtype == XI_Motion) { + data_.in_proximity = true; + } + break; + } + } + + XFreeEventData(display_, &ev.xcookie); + } + } + } +#endif +} + +void TabletInput::terminate() +{ +#ifdef HAVE_X11TABLETINPUT + if (display_) { + XCloseDisplay(display_); + display_ = nullptr; + } +#endif + active_ = false; +} + +#endif // LINUX From 08d6d60f57cb71ccb97b915d116c9b5f9c55b4fe Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 12:01:40 +0100 Subject: [PATCH 07/15] BugFix End property frame grabber on pipeline error --- src/FrameGrabber.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/FrameGrabber.cpp b/src/FrameGrabber.cpp index 02e901b..031fd1d 100644 --- a/src/FrameGrabber.cpp +++ b/src/FrameGrabber.cpp @@ -441,9 +441,15 @@ GstBusSyncReply FrameGrabber::signal_handler(GstBus *, GstMessage *msg, gpointer // inform user GError *error; gst_message_parse_error(msg, &error, NULL); - Log::Warning("FrameGrabber Error %s : %s", - std::to_string(reinterpret_cast(ptr)->id()).c_str(), - error->message); + FrameGrabber *fg = reinterpret_cast(ptr); + if (fg) { + Log::Warning("FrameGrabber Error %s : %s", + std::to_string(fg->id()).c_str(), + error->message); + fg->endofstream_=true; + } + else + Log::Warning("FrameGrabber Error : %s", error->message); g_error_free(error); // } else { // g_printerr("FrameGrabber msg %s \n", GST_MESSAGE_TYPE_NAME(msg)); From 9807da71dcf2d123085515a0a68f987bb424c39d Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 12:03:04 +0100 Subject: [PATCH 08/15] x265 is not available most of time so use va h265 encoder instead (available in flatpak). May not work always, to be improved. --- src/Recorder.cpp | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/Recorder.cpp b/src/Recorder.cpp index f0aa4cb..c7f1954 100644 --- a/src/Recorder.cpp +++ b/src/Recorder.cpp @@ -164,25 +164,18 @@ const std::vector VideoRecorder::profile_description { // fast (5) "video/x-raw, format=I420 ! x264enc tune=\"zerolatency\" pass=4 quantizer=22 speed-preset=2 ! video/x-h264, profile=baseline ! h264parse ! ", "video/x-raw, format=Y444_10LE ! x264enc tune=\"zerolatency\" pass=4 quantizer=18 speed-preset=3 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ", - // Control x265 encoder quality : - // NB: apparently x265 only accepts I420 format :( - // speed-preset - // superfast (2) - // veryfast (3) - // faster (4) - // fast (5) - // Tune - // psnr (1) - // ssim (2) DEFAULT - // grain (3) - // zerolatency (4) Encoder latency is removed - // fastdecode (5) - // animation (6) optimize the encode quality for animation content without impacting the encode speed - // crf Quality-controlled variable bitrate [0 51] - // default 28 - // 24 for x265 should be visually transparent; anything lower will probably just waste file size - "video/x-raw, format=I420 ! x265enc tune=\"zerolatency\" speed-preset=2 option-string=\"crf=24\" ! video/x-h265, profile=(string)main ! h265parse ! ", - "video/x-raw, format=I420 ! x265enc tune=\"zerolatency\" speed-preset=5 option-string=\"crf=12\" ! video/x-h265, profile=(string)main ! h265parse ! ", + // Control vah265enc encoder quality : + // target-usage : The target usage to control and balance the encoding speed/quality + // The lower value has better quality but slower speed, the higher value has faster speed but lower quality. + // Unsigned Integer. Range: 1 - 7 Default: 4 + // max-qp : Maximum quantizer value for each frame + // Unsigned Integer. Range: 0 - 51 Default: 51 + // rate-control : The desired rate control mode for the encoder + // (2): cbr - Constant Bitrate + // (4): vbr - Variable Bitrate + // (16): cqp - Constant Quantizer + "video/x-raw, format=NV12 ! vah265enc rate-control=\"cqp\" target-usage=5 ! video/x-h265, profile=(string)main ! h265parse ! ", + "video/x-raw, format=NV12 ! vah265enc rate-control=\"cqp\" max-qp=18 target-usage=2 ! video/x-h265, profile=(string)main ! h265parse ! ", // Apple ProRes encoding parameters // pass // cbr (0) – Constant Bitrate Encoding @@ -249,8 +242,8 @@ std::vector nvidia_profile_description { "video/x-raw, format=RGBA ! nvh264enc rc-mode=1 zerolatency=true ! video/x-h264, profile=(string)main ! h264parse ! ", "video/x-raw, format=RGBA ! nvh264enc rc-mode=1 qp-const=18 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ", // Control nvh265enc encoder - "video/x-raw, format=RGBA ! nvh265enc rc-mode=1 zerolatency=true ! video/x-h265, profile=(string)main-10 ! h265parse ! ", - "video/x-raw, format=RGBA ! nvh265enc rc-mode=1 qp-const=18 ! video/x-h265, profile=(string)main-444 ! h265parse ! ", + "video/x-raw, format=RGBA ! nvh265enc rc-mode=1 zerolatency=true ! video/x-h265, profile=(string)main ! h265parse ! ", + "video/x-raw, format=RGBA ! nvh265enc rc-mode=1 qp-const=18 ! video/x-h265, profile=(string)main ! h265parse ! ", "", "", "", "" }; From fcdfd34914d2167322b2c2c8b51600fd59dc822e Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 12:36:39 +0100 Subject: [PATCH 09/15] BugFix detection of GNOME desktop to force X11 --- rsc/launch_vimix.sh | 30 ++++++++++++++++-------------- src/RenderingManager.cpp | 21 ++++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/rsc/launch_vimix.sh b/rsc/launch_vimix.sh index 83c676b..615f62a 100644 --- a/rsc/launch_vimix.sh +++ b/rsc/launch_vimix.sh @@ -1,19 +1,21 @@ #!/bin/bash -# Test if running under wayland -if [ -z "$WAYLAND_DISPLAY" ]; then - # not Wayland, nothing special - vimix "$@" -else - # Wayland: test if there is an nvidia GPU - gpu=$(lspci | grep -i '.* vga .* nvidia .*') - shopt -s nocasematch - if [[ $gpu == *' nvidia '* ]]; then - # with nvidia, request Wayland render offload - printf 'Nvidia GPU present: %s\n' "$gpu" - __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia vimix "$@" +# test if there is an nvidia GPU +gpu=$(lspci | grep -i '.* vga .* nvidia .*') +shopt -s nocasematch +if [[ $gpu == *' nvidia '* ]]; then + # with nvidia, request Wayland render offload + printf 'Nvidia GPU present: %s\n' "$gpu" + # Test if running under wayland + if [ -z "$WAYLAND_DISPLAY" ]; then + # not Wayland + __GLX_VENDOR_LIBRARY_NAME=nvidia vimix "$@" else - # otherwise, nothing special - vimix "$@" + # Wayland: + __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia vimix "$@" fi +else + # otherwise, nothing special + vimix "$@" fi + diff --git a/src/RenderingManager.cpp b/src/RenderingManager.cpp index df62c5d..1b35e9f 100644 --- a/src/RenderingManager.cpp +++ b/src/RenderingManager.cpp @@ -439,15 +439,18 @@ Rendering::Rendering() bool Rendering::init() { #if GLFW_VERSION_MAJOR > 2 && GLFW_VERSION_MINOR > 3 - // Forcing X11 on Gnome makes the server use xWayland which has proper Server Side Decorations as opposed to Wayland. - if (strcmp(getenv("XDG_CURRENT_DESKTOP"), "GNOME") == 0 || - strcmp(getenv("XDG_CURRENT_DESKTOP"), "Unity") == 0 ){ - g_printerr("Forcing X11 on GNOME desktop\n"); - glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); - } - else { - g_printerr("Detected %s desktop\n", getenv("XDG_CURRENT_DESKTOP")); - glfwInitHint(GLFW_PLATFORM, GLFW_ANY_PLATFORM); + const char* desktop = getenv("XDG_CURRENT_DESKTOP"); + if (desktop) { + // Forcing X11 on Gnome makes the server use xWayland which has proper Server Side Decorations as opposed to Wayland. + if (strstr(desktop, "GNOME") != nullptr || + strstr(desktop, "Unity") != nullptr ){ + g_printerr("Forcing X11 / xWayland on %s desktop\n", desktop); + glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); + } + else { + g_printerr("Detected %s desktop\n", desktop); + glfwInitHint(GLFW_PLATFORM, GLFW_ANY_PLATFORM); + } } #endif // From cc1a6debb1a456d1faf8ccb99857a1f31c209133 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 12:40:18 +0100 Subject: [PATCH 10/15] Change flatpak to freedesktop platform. In latest 25.08 there is already a recent gstreamer, no need to recompile it. Added --socket=session-bus to support prevention of screen suspend. Add socket x11 to be sure... --- flatpak/io.github.brunoherbelin.Vimix.json | 76 ++----------------- ...io.github.brunoherbelin.Vimix.metainfo.xml | 6 ++ snap/snapcraft.yaml | 2 +- 3 files changed, 12 insertions(+), 72 deletions(-) diff --git a/flatpak/io.github.brunoherbelin.Vimix.json b/flatpak/io.github.brunoherbelin.Vimix.json index 1bdf6c4..1e315de 100644 --- a/flatpak/io.github.brunoherbelin.Vimix.json +++ b/flatpak/io.github.brunoherbelin.Vimix.json @@ -1,15 +1,17 @@ { "app-id": "io.github.brunoherbelin.Vimix", - "runtime": "org.gnome.Platform", - "runtime-version": "49", - "sdk": "org.gnome.Sdk", + "runtime": "org.freedesktop.Platform", + "runtime-version": "25.08", + "sdk": "org.freedesktop.Sdk", "command": "launch_vimix.sh", "rename-desktop-file": "vimix.desktop", "rename-icon": "vimix", "finish-args": [ + "--socket=x11", "--socket=wayland", "--socket=fallback-x11", "--socket=pulseaudio", + "--socket=session-bus", "--share=ipc", "--share=network", "--device=dri", @@ -49,35 +51,6 @@ } ] }, - { - "name": "x264", - "config-opts": [ - "--enable-shared" - ], - "sources": [ - { - "type": "git", - "commit": "b35605ace3ddf7c1a5d67a2eb553f034aef41d55", - "url": "https://code.videolan.org/videolan/x264.git" - } - ] - }, - { - "name": "x265", - "buildsystem": "cmake-ninja", - "subdir": "source", - "config-opts": [ - "-DCMAKE_BUILD_TYPE=Release", - "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" - ], - "sources": [ - { - "type": "git", - "commit": "b858f483959b805895f77e700d81544be84e4c6b", - "url": "https://bitbucket.org/multicoreware/x265_git.git" - } - ] - }, { "name": "srt", "buildsystem": "cmake-ninja", @@ -110,45 +83,6 @@ } ] }, - { - "name": "gstreamer", - "buildsystem": "meson", - "builddir": true, - "config-opts": [ - "-Ddoc=disabled", - "-Dpython=disabled", - "-Dges=disabled", - "-Drs=disabled", - "-Dgst-examples=disabled", - "-Drtsp_server=disabled", - "-Ddevtools=disabled", - "-Dqt5=disabled", - "-Dlibav=enabled", - "-Dbase=enabled", - "-Dgood=enabled", - "-Dgst-plugins-good:v4l2=enabled", - "-Dgpl=enabled", - "-Dbad=enabled", - "-Dgst-plugins-bad:x265=enabled", - "-Dgst-plugins-bad:srt=enabled", - "-Dgst-plugins-bad:shm=enabled", - "-Dugly=enabled", - "-Dgst-plugins-ugly:x264=enabled", - "-Dvaapi=enabled" - ], - "build-options": { - "build-args": [ "--share=network" ] - }, - "sources": [ - { - "type": "git", - "tag": "1.26.8", - "commit": "16d77e12ad213ef24e76a8cc34d347b8221c9975", - "url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git", - "disable-submodules": false - } - ] - }, { "name": "shmdata", "buildsystem": "cmake-ninja", diff --git a/share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml b/share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml index 7d26634..e0c90d0 100644 --- a/share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml +++ b/share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml @@ -33,6 +33,12 @@ vimix + + +

Version 0.8.5

+
+ https://github.com/brunoherbelin/vimix/releases/tag/0.8.5 +

Version 0.8.4

diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 61bad10..7b97beb 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: vimix base: core24 -version: '0.8.4' +version: '0.8.5' summary: Video live mixer title: vimix description: | From 3651ce2659777c675efd8c52dfc205137845d457 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 13:04:13 +0100 Subject: [PATCH 11/15] Update README to match change in runtime flatpak --- flatpak/README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/flatpak/README.md b/flatpak/README.md index 00c5c6b..e0994b1 100644 --- a/flatpak/README.md +++ b/flatpak/README.md @@ -32,11 +32,8 @@ If not already installed, install the builder and the flathub repository: Install the runtime environments: - flatpak install org.gnome.Sdk/x86_64 - flatpak install org.gnome.Platform - - -_Select version **49** in the list of proposed versions_ + flatpak install org.freedesktop.Sdk/x86_64/25.08 + flatpak install org.freedesktop.Platform/25.08 ### 2. Build vimix flatpak From 97a58c29888c23675087735981585e21657effc8 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 15:19:10 +0100 Subject: [PATCH 12/15] Use TabletInput with MousePointer to make Spring and Wiggly cursors pressure sentivite --- src/MousePointer.cpp | 30 ++++++++++++++++++++++-------- src/MousePointer.h | 2 ++ src/TabletInput.h | 2 +- src/TabletInput_x11.cpp | 9 ++++----- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/MousePointer.cpp b/src/MousePointer.cpp index 326785b..75cc0ca 100644 --- a/src/MousePointer.cpp +++ b/src/MousePointer.cpp @@ -24,6 +24,8 @@ #include "Metronome.h" #include "View.h" #include "Mixer.h" +#include "TabletInput.h" + //#include "RenderingManager.h" #include "MousePointer.h" @@ -111,10 +113,14 @@ void PointerLinear::draw() void PointerWiggly::update(const glm::vec2 &pos, float) { current_ = pos; - float radius = POINTER_WIGGLY_MIN_RADIUS + (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_; + radius_ = (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_; + if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) { + radius_ *= TabletInput::instance().getPressure(); + } + radius_ += POINTER_WIGGLY_MIN_RADIUS; // change pos to a random point in a close radius - glm::vec2 p = pos + glm::diskRand( radius ); + glm::vec2 p = pos + glm::diskRand( radius_ ); // smooth a little and apply const float emaexp = 2.0 / float( POINTER_WIGGLY_SMOOTHING + 1); @@ -126,8 +132,10 @@ void PointerWiggly::draw() const ImU32 color = ImGui::GetColorU32(ImGuiCol_HeaderActive); ImGui::GetBackgroundDrawList()->AddLine(IMVEC_IO(current_), IMVEC_IO(target_), color, 5.f); - const float radius = POINTER_WIGGLY_MIN_RADIUS + (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_; - ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), radius * 0.5f, color, 0, 2.f + 4.f * strength_); + const float max = POINTER_WIGGLY_MIN_RADIUS + (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_; + if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) + ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), radius_ * 0.5f, color, 0); + ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), max * 0.5f, color, 0, 2.f + 4.f * strength_); } #define POINTER_METRONOME_RADIUS 36.f @@ -186,13 +194,17 @@ void PointerSpring::update(const glm::vec2 &pos, float dt) // damping : opposite direction of force, non proportional to mass const float damping = 60.0; // mass as a percentage of min to max - const float mass = POINTER_SPRING_MIN_MASS + (POINTER_SPRING_MAX_MASS - POINTER_SPRING_MIN_MASS) * strength_; + mass_ = (POINTER_SPRING_MAX_MASS - POINTER_SPRING_MIN_MASS) * strength_; + if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) { + mass_ *= 1.f - TabletInput::instance().getPressure(); + } + mass_ += POINTER_SPRING_MIN_MASS; // compute delta betwen initial and current position glm::vec2 delta = pos - target_; if ( glm::length(delta) > 0.0001f ) { // apply force on velocity : spring stiffness / mass - velocity_ += delta * ( (POINTER_SPRING_MAX_MASS * stiffness) / mass ); + velocity_ += delta * ( (POINTER_SPRING_MAX_MASS * stiffness) / mass_ ); // apply damping dynamics velocity_ -= damping * glm::max(dt,0.001f) * glm::normalize(delta); // compute new position : add velocity x time @@ -230,8 +242,10 @@ void PointerSpring::draw() ImGui::GetBackgroundDrawList()->AddBezierCurve(IMVEC_IO(current_), IMVEC_IO(_third), IMVEC_IO(_twothird), IMVEC_IO(_end), color, 5.f); // represent the weight with a filled circle - const float mass = POINTER_SPRING_MIN_MASS + (POINTER_SPRING_MAX_MASS - POINTER_SPRING_MIN_MASS) * strength_; - ImGui::GetBackgroundDrawList()->AddCircleFilled(IMVEC_IO(_end), mass, color, 0); + const float max = POINTER_SPRING_MIN_MASS + (POINTER_SPRING_MAX_MASS - POINTER_SPRING_MIN_MASS) * strength_; + if (TabletInput::instance().hasPressure()) + ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(_end), max, color, 0); + ImGui::GetBackgroundDrawList()->AddCircleFilled(IMVEC_IO(_end), mass_, color, 0); } diff --git a/src/MousePointer.h b/src/MousePointer.h index 51afd48..73fc9d1 100644 --- a/src/MousePointer.h +++ b/src/MousePointer.h @@ -87,6 +87,7 @@ public: /// class PointerSpring : public Pointer { + float mass_; glm::vec2 velocity_; public: PointerSpring() {} @@ -101,6 +102,7 @@ public: /// class PointerWiggly : public Pointer { + float radius_; public: PointerWiggly() {} void update(const glm::vec2 &pos, float) override; diff --git a/src/TabletInput.h b/src/TabletInput.h index 281a206..6653432 100644 --- a/src/TabletInput.h +++ b/src/TabletInput.h @@ -71,7 +71,7 @@ public: // Quick accessors float getPressure() const { return data_.pressure; } - bool isPressed() const { return data_.tip_down || data_.in_proximity; } + bool isPressed() const { return data_.tip_down && data_.in_proximity; } // status bool isEnabled() const { return active_; } diff --git a/src/TabletInput_x11.cpp b/src/TabletInput_x11.cpp index 4150831..56f07d3 100644 --- a/src/TabletInput_x11.cpp +++ b/src/TabletInput_x11.cpp @@ -17,6 +17,7 @@ * along with this program. If not, see . **/ +#include #ifdef LINUX #include "TabletInput.h" @@ -213,16 +214,14 @@ void TabletInput::pollEvents() // Update button state if (ev.xcookie.evtype == XI_ButtonPress) { data_.tip_down = true; - data_.in_proximity = true; + data_.in_proximity = data_.pressure > 0.005f; } else if (ev.xcookie.evtype == XI_ButtonRelease) { data_.tip_down = false; - if (data_.pressure < 0.01f) { - data_.in_proximity = false; - } + data_.in_proximity = false; } else if (ev.xcookie.evtype == XI_Motion) { - data_.in_proximity = true; + data_.in_proximity = data_.pressure > 0.005f; } break; } From cc6189e162c620c82faa4a6881d0b35b114ca726 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 17:13:27 +0100 Subject: [PATCH 13/15] New MousePointer Brownian --- rsc/images/icons.dds | Bin 1638528 -> 1638528 bytes src/MousePointer.cpp | 47 +++++++++++++++++++++++++++++++++++++++++++ src/MousePointer.h | 18 ++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/rsc/images/icons.dds b/rsc/images/icons.dds index 02e16135e4827d775e02e84a08c3424fe911b5d2..532257197379779de107952409ea863e85a92e27 100644 GIT binary patch delta 4663 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

ii4HnAzl zkFSvSs^pf?>R@lHzQ@{-xhoPH}Qy zr9(Y@G?D&U*3;^|alE;s5TFYG0uOTNiE$*Y8O|swbWb&M3zYZFu}+Vl5aZj0)n%t#tw+6 z|6`>5E0yC4*wlZSprk<~_<$YcCKqy}f^r{P6XJ^ee&L6F~6|2VQdM}|ybslQ{IA)T1^P4x}gxJ0)(8>!8h!3(MD-~buG z$DVwwChZiVS3u8qmhon({deg~PUx&zVVY;r$%lxnQeJ%nXa4haY5e=%2U9ue=nEve z7iB9oad*;fs`*E}P-;wX<dHRJf^e z(*rH3 zC=AmPIz5cXpCNO^yV9=vCOQPtA%vEnO=2;dV0Df^oH{|L;HyZ*vAxkL^6F`pDGo(+ z+pALbBTzEQB0!=gE#rwPQ0qn-QI-8whpQA#zIJSd1-~DK$Dj*6$ zy0#)Ge5Xdx=TrPbSPc8>*6Jw^Tb7>>TWns6m1gx^;JrB%t3%FB25(R%n-my>_tbi0 zls8cM>R5d&7&DqEj<@E1_xhE)J=j?svqu5Ewe?jS>t>hBpI^fE> z*2WK4)UcB}=4h>ND;DL0i!2z-uk<;>N=B|x71J7d8#9Ol4X#Yqn@c9G4!Byz{Wv<6 zz@jG-^MrKHFiyc_os*nvT+Ptpq1d+Huwz!kvTp~G*#q!uL&C)gMXhjy6;*p6;|CCBNtm#~}8XIE_ouc3!IbQSbCJ}Yl7p*Akj zvL|Ppi|b)EtOVwa#Z}^45s0wxb6Xsqhc`Lw)cAsryrzaiFJ@zCO91D(caA;Oy0Ziu zyW!u-AzW+pc?%<679EOupHw$gA<^;0Wk>&G;J-FKOL5%F#loMzwt=x>2&bKWV3+Gn zs^D0d@7nUTqlB>eDPEvV>GkZa_pp4LRbCc&tZQXG)wh+D+c$11jZhpk7>wsU@3P{P z4-Tlnd``%3UalX;x$c8~U?+y4IZjNGr;%@h=Dig(ZwOfxa8qqr`*1Pup;-y|4D=rRy#*2>Fga;e%FybKkNR<; zN;0hD9Kn}X`&eoz>Q^`#_pNbIdCcT99z7OjxeYppzwLPkD|`#VKvBptY#Jg2i`ksK|c*c9E58y90cNyDSR2pvP{}HrR|fcTW$S-JUb1 zf7R&hEhd}~r-vJ6rqA?qma6R`FAnF6!{qAK;|hn?e`)`cj`u?t?uCbL2BT8n4B^!y z&6je-_y9Zr=Rgh%5{FgJ{jU2K7+KB~yZ3O#Y7aXPOV`odI z3!EDDT2?f8wb84pHZgrQZ__9_JKKf0P6rSaLIU+t5L7msDo01Lh1ZK(U(Gs-b%Ii9 zv$$AMDApN^IY+IZ$~A)3p#mG*Mx)*83Fs_lDlY*)E^5A;$l`U(Vi$PvCE+MtB(l=z zP>E|;Xhb3&gTuR`h00gvIPBhh-w}fGPD3PYFcoXIyhU+q#A9?gHf^Wa9kbg6Yso%} z>nrUh&_pKBoed$cn$zf5`MA#`>a38@YONzsQ>Qcp%g6*|o6~nXMP;B%q_{u~h4aJJ zELJOa2AzQ_7u)uqbh^#8R*9WUZP?#}$(#E>Qz|E7F(X%uTcHk~f~Tla#R{X?u&A8H zqR;CH{if+u1CbNfJYK(R-ihfLFMAZe{`#!fXvryg#5f-qlHEJe+8~Eq6qSm%&}I09YkzjQ(3^Ym1uY%tjA-u-;}25Icz-=1OBl z-D*$T`&!2X#_T(_)L~i~AT274zB#90h?Eav>nCau4Yc*O; zRw~!~H$Gx9@kWA)!FqL1y~SS4Q>=ldPzowJsj>K~IS$ z^MttSufga4E4__!J#@~dr z_TjWH##_=l$zn8$A1tc(l4mY5E1^y2X4}_IK1<2danOkOkjj-BJRZL&uEGB!r(&^l zD%NPH;&>20ASEw4!TkEExc1?xIH;eB)jN{-^f0b7_=%~_sZ^03gf_n~aCRYNR88Qd zLw?L+QZ8d@@L*Q$KYnIG|0(=_O?UE%Mp8&h<^fJAo!=w`zF+. **/ +#include #include // for diskRand #include "imgui.h" @@ -37,6 +38,7 @@ std::vector< std::tuple > Pointer::Modes = { { ICON_POINTER_LINEAR, "Line", "Speed" }, { ICON_POINTER_SPRING, "Spring", "Mass" }, { ICON_POINTER_WIGGLY, "Wiggly", "Radius" }, + { ICON_POINTER_BROWNIAN, "Brownian", "Radius" }, { ICON_POINTER_METRONOME, "Metronome", "Jump" } }; @@ -138,6 +140,49 @@ void PointerWiggly::draw() ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), max * 0.5f, color, 0, 2.f + 4.f * strength_); } +void PointerBrownian::update(const glm::vec2 &pos, float) +{ + current_ = pos; + radius_ = (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_; + radius_ += POINTER_WIGGLY_MIN_RADIUS; + + // Brownian motion: add small random displacement in 2D + // Generate random step using gaussian distribution for each axis + glm::vec2 random_step = glm::gaussRand(glm::vec2(0.0f), glm::vec2(1.f) ); + + // Scale by radius and apply damping to keep motion bounded + float factor = 0.3f; + if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) { + factor *= TabletInput::instance().getPressure(); + } + float damping = 0.92f; + brownian_offset_ = brownian_offset_ * damping + random_step * radius_ * factor; + + // Clamp offset to stay within maximum radius + float offset_length = glm::length(brownian_offset_); + if (offset_length > radius_) { + brownian_offset_ = brownian_offset_ * (radius_ / offset_length); + } + + glm::vec2 p = pos + brownian_offset_; + + // smooth a little and apply + const float emaexp = 2.0 / float( POINTER_WIGGLY_SMOOTHING + 1); + target_ = emaexp * p + (1.f - emaexp) * target_; +} + +void PointerBrownian::draw() +{ + const ImU32 color = ImGui::GetColorU32(ImGuiCol_HeaderActive); + ImGui::GetBackgroundDrawList()->AddLine(IMVEC_IO(current_), IMVEC_IO(target_), color, 5.f); + + const float max = POINTER_WIGGLY_MIN_RADIUS + (POINTER_WIGGLY_MAX_RADIUS - POINTER_WIGGLY_MIN_RADIUS) * strength_; + if (TabletInput::instance().hasPressure() && TabletInput::instance().isPressed()) + ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), radius_ * 0.8f, color, 0); + ImGui::GetBackgroundDrawList()->AddCircle(IMVEC_IO(current_), max * 0.8f, color, 0, 2.f + 4.f * strength_); +} + + #define POINTER_METRONOME_RADIUS 36.f void PointerMetronome::initiate(const glm::vec2 &pos) @@ -256,6 +301,7 @@ MousePointer::MousePointer() : mode_(Pointer::POINTER_DEFAULT) pointer_[Pointer::POINTER_LINEAR] = new PointerLinear; pointer_[Pointer::POINTER_SPRING] = new PointerSpring; pointer_[Pointer::POINTER_WIGGLY] = new PointerWiggly; + pointer_[Pointer::POINTER_BROWNIAN] = new PointerBrownian; pointer_[Pointer::POINTER_METRONOME] = new PointerMetronome; } @@ -266,5 +312,6 @@ MousePointer::~MousePointer() delete pointer_[Pointer::POINTER_LINEAR]; delete pointer_[Pointer::POINTER_SPRING]; delete pointer_[Pointer::POINTER_WIGGLY]; + delete pointer_[Pointer::POINTER_BROWNIAN]; delete pointer_[Pointer::POINTER_METRONOME]; } diff --git a/src/MousePointer.h b/src/MousePointer.h index 73fc9d1..e2d524d 100644 --- a/src/MousePointer.h +++ b/src/MousePointer.h @@ -12,6 +12,7 @@ #define ICON_POINTER_LINEAR 14, 9 #define ICON_POINTER_GRID 15, 9 #define ICON_POINTER_WIGGLY 16, 9 +#define ICON_POINTER_BROWNIAN 11, 9 #define ICON_POINTER_METRONOME 6, 13 /// @@ -33,6 +34,7 @@ public: POINTER_LINEAR, POINTER_SPRING, POINTER_WIGGLY, + POINTER_BROWNIAN, POINTER_METRONOME, POINTER_INVALID } Mode; @@ -104,7 +106,21 @@ class PointerWiggly : public Pointer { float radius_; public: - PointerWiggly() {} + PointerWiggly() : radius_(0.0f) {} + void update(const glm::vec2 &pos, float) override; + void draw() override; +}; + +/// +/// \brief The PointerBrownian moves with a Brownian movement +/// Strength modulates the radius of the movement +/// +class PointerBrownian : public Pointer +{ + float radius_; + glm::vec2 brownian_offset_; +public: + PointerBrownian() : brownian_offset_(0.0f, 0.0f) {} void update(const glm::vec2 &pos, float) override; void draw() override; }; From 9c8ccf7a9f2cc0327e3e07ac2feccb47c04b1570 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 17:13:45 +0100 Subject: [PATCH 14/15] Not ready for CANVAS : work in progress --- src/GeometryView.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/GeometryView.h b/src/GeometryView.h index 2305547..5bdeb90 100644 --- a/src/GeometryView.h +++ b/src/GeometryView.h @@ -1,8 +1,6 @@ #ifndef GEOMETRYVIEW_H #define GEOMETRYVIEW_H -#define ENABLE_CANVAS - #include "View.h" struct Canvas From 03a3113d348865c9ac9e8ab9e90b443ea470fda8 Mon Sep 17 00:00:00 2001 From: brunoherbelin Date: Tue, 18 Nov 2025 17:45:07 +0100 Subject: [PATCH 15/15] Ready for 0.8.5 --- docs/notes.txt | 2 +- share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/notes.txt b/docs/notes.txt index 1c5fd3b..6da0fc9 100644 --- a/docs/notes.txt +++ b/docs/notes.txt @@ -17,7 +17,7 @@ Publishing new a release https://github.com/brunoherbelin/vimix/tree/master/flatpak - Test the snap for Beta $ snap remove vimix - $ snapcraft (from vimix base dir) + $ snapcraft pack (from vimix base dir) $ snap install ./vimix_0.X.Y_amd64.snap --devmode $ snap connections vimix diff --git a/share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml b/share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml index e0c90d0..29ad888 100644 --- a/share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml +++ b/share/metainfo/io.github.brunoherbelin.Vimix.metainfo.xml @@ -33,7 +33,7 @@ vimix - +

Version 0.8.5