mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-05 15:30:00 +01:00
Merge remote-tracking branch 'origin/beta'
This commit is contained in:
@@ -103,6 +103,30 @@ if(UNIX)
|
||||
${X11_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
# 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()
|
||||
|
||||
# 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()
|
||||
add_definitions(-DUNIX)
|
||||
elseif(WIN32)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
122
osx/TabletInput_macos.mm
Normal file
122
osx/TabletInput_macos.mm
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2025 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#ifdef APPLE
|
||||
|
||||
#include "TabletInput.h"
|
||||
#include "Log.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
TabletInput::TabletInput()
|
||||
: data_({0.0f, false, 0.0f, 0.0f, false, false})
|
||||
, active_(false)
|
||||
, monitor_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
TabletInput::~TabletInput()
|
||||
{
|
||||
terminate();
|
||||
}
|
||||
|
||||
bool TabletInput::init()
|
||||
{
|
||||
// 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
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
<binary>vimix</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="0.8.5" date="2025-11-18">
|
||||
<description>
|
||||
<p>Version 0.8.5</p>
|
||||
</description>
|
||||
<url>https://github.com/brunoherbelin/vimix/releases/tag/0.8.5</url>
|
||||
</release>
|
||||
<release version="0.8.4" date="2025-01-04">
|
||||
<description>
|
||||
<p>Version 0.8.4</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: vimix
|
||||
base: core24
|
||||
version: '0.8.4'
|
||||
version: '0.8.5'
|
||||
summary: Video live mixer
|
||||
title: vimix
|
||||
description: |
|
||||
|
||||
@@ -103,12 +103,35 @@ set(VMIX_SRCS
|
||||
Audio.cpp
|
||||
TextSource.cpp
|
||||
ShaderSource.cpp
|
||||
TabletInput.cpp
|
||||
)
|
||||
|
||||
#####
|
||||
##### DEFINE THE TARGET (OS specific)
|
||||
#####
|
||||
|
||||
IF(APPLE)
|
||||
|
||||
# Add macOS tablet input implementation
|
||||
list(APPEND VMIX_SRCS ${CMAKE_SOURCE_DIR}/osx/TabletInput_macos.mm)
|
||||
# Enable Objective-C++
|
||||
set_source_files_properties(${CMAKE_SOURCE_DIR}/osx/TabletInput_macos.mm PROPERTIES COMPILE_FLAGS "-x objective-c++")
|
||||
|
||||
ELSE(APPLE)
|
||||
|
||||
# Add Linux tablet input implementation
|
||||
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)
|
||||
|
||||
IF(APPLE)
|
||||
|
||||
# set icon
|
||||
@@ -149,6 +172,16 @@ ELSE(APPLE)
|
||||
X11::xcb
|
||||
)
|
||||
|
||||
# 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()
|
||||
|
||||
ENDIF(APPLE)
|
||||
|
||||
#####
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<FrameGrabber *>(ptr)->id()).c_str(),
|
||||
error->message);
|
||||
FrameGrabber *fg = reinterpret_cast<FrameGrabber *>(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));
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#ifndef GEOMETRYVIEW_H
|
||||
#define GEOMETRYVIEW_H
|
||||
|
||||
#define ENABLE_CANVAS
|
||||
|
||||
#include "View.h"
|
||||
|
||||
struct Canvas
|
||||
|
||||
@@ -140,7 +140,7 @@ void MaskShader::reset()
|
||||
size = glm::vec2(1.f, 1.f);
|
||||
|
||||
// default brush
|
||||
cursor = glm::vec4(-10.f, -10.f, 1.f, 1.f);
|
||||
cursor = glm::vec4(-10.f, -10.f, -10.f, -10.f);
|
||||
brush = glm::vec3(0.5f, 0.1f, 0.f);
|
||||
option = 0;
|
||||
effect = 0;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "MediaSource.h"
|
||||
#include <string>
|
||||
#include <regex>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -422,72 +426,56 @@ 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<Source *>(&target)) {
|
||||
MediaSource *ms = dynamic_cast<MediaSource*>(*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);
|
||||
}
|
||||
break;
|
||||
|
||||
case SourceCallback::CALLBACK_FLAG:
|
||||
{
|
||||
Flag *edited = static_cast<Flag*>(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<Source *>(&target)) {
|
||||
MediaSource *ms = dynamic_cast<MediaSource*>(*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<SetBrightness*>(callback);
|
||||
@@ -1362,17 +1350,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<Source *>(&target)) {
|
||||
withimageprocessing = (*v)->imageProcessingEnabled();
|
||||
ismediaplayer = dynamic_cast<MediaSource*>(*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 +1421,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<Source *>(&temp_new_target)) {
|
||||
withimageprocessing = (*v)->imageProcessingEnabled();
|
||||
mediaplayer = dynamic_cast<MediaSource*>(*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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <glm/geometric.hpp>
|
||||
#include <glm/gtc/random.hpp> // for diskRand
|
||||
|
||||
#include "imgui.h"
|
||||
@@ -24,6 +25,8 @@
|
||||
#include "Metronome.h"
|
||||
#include "View.h"
|
||||
#include "Mixer.h"
|
||||
#include "TabletInput.h"
|
||||
|
||||
//#include "RenderingManager.h"
|
||||
#include "MousePointer.h"
|
||||
|
||||
@@ -35,6 +38,7 @@ std::vector< std::tuple<int, int, std::string, std::string> > 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" }
|
||||
};
|
||||
|
||||
@@ -111,10 +115,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,10 +134,55 @@ 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_);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -186,13 +239,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 +287,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -242,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;
|
||||
}
|
||||
|
||||
@@ -252,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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -87,6 +89,7 @@ public:
|
||||
///
|
||||
class PointerSpring : public Pointer
|
||||
{
|
||||
float mass_;
|
||||
glm::vec2 velocity_;
|
||||
public:
|
||||
PointerSpring() {}
|
||||
@@ -101,8 +104,23 @@ public:
|
||||
///
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -164,25 +164,18 @@ const std::vector<std::string> 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<std::string> 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 ! ",
|
||||
"", "", "", ""
|
||||
};
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
#include "Primitives.h"
|
||||
|
||||
#include "RenderingManager.h"
|
||||
#include "TabletInput.h"
|
||||
|
||||
// GDBus for screensaver inhibition (works on both X11 and Wayland)
|
||||
#ifdef GLFW_EXPOSE_NATIVE_GLX
|
||||
@@ -438,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
|
||||
//
|
||||
@@ -567,6 +571,9 @@ bool Rendering::init()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Initialize tablet input for pressure support
|
||||
TabletInput::instance().init();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -620,6 +627,11 @@ void Rendering::draw()
|
||||
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
|
||||
glfwPollEvents();
|
||||
|
||||
// Poll tablet input for pressure support
|
||||
#ifndef WIN32
|
||||
TabletInput::instance().pollEvents();
|
||||
#endif
|
||||
|
||||
// change windows fullscreen mode if requested
|
||||
main_.changeFullscreen_();
|
||||
for (auto it = outputs_.begin(); it != outputs_.end(); ++it)
|
||||
@@ -675,6 +687,11 @@ void Rendering::draw()
|
||||
|
||||
void Rendering::terminate()
|
||||
{
|
||||
// Terminate tablet input
|
||||
#ifndef WIN32
|
||||
TabletInput::instance().terminate();
|
||||
#endif
|
||||
|
||||
// terminate all windows
|
||||
for (auto it = outputs_.begin(); it != outputs_.end(); ++it)
|
||||
it->terminate();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<WindowConfig>(1+MAX_OUTPUT_WINDOW);
|
||||
windows[0].w = 1600;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<MediaSource *>(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)
|
||||
{
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <glib.h>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <thread>
|
||||
@@ -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 {
|
||||
///
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <climits>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
30
src/TabletInput.cpp
Normal file
30
src/TabletInput.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2023 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#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)
|
||||
107
src/TabletInput.h
Normal file
107
src/TabletInput.h
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2023 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#ifndef TABLETINPUT_H
|
||||
#define TABLETINPUT_H
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
// Platform-specific forward declarations
|
||||
#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;
|
||||
#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();
|
||||
|
||||
// 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_;
|
||||
|
||||
#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
|
||||
};
|
||||
|
||||
#endif // TABLETINPUT_H
|
||||
0
src/TabletInput_linux.cpp
Normal file
0
src/TabletInput_linux.cpp
Normal file
206
src/TabletInput_linux_libinput.cpp
Normal file
206
src/TabletInput_linux_libinput.cpp
Normal file
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2025 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <glib.h>
|
||||
#ifdef LINUX
|
||||
|
||||
#include "TabletInput.h"
|
||||
#include "Log.h"
|
||||
|
||||
#ifdef HAVE_LIBINPUT
|
||||
#include <libinput.h>
|
||||
#include <libudev.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
|
||||
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
|
||||
248
src/TabletInput_x11.cpp
Normal file
248
src/TabletInput_x11.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2023 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <glib.h>
|
||||
#ifdef LINUX
|
||||
|
||||
#include "TabletInput.h"
|
||||
#include "Log.h"
|
||||
|
||||
#ifdef HAVE_X11TABLETINPUT
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <cstring>
|
||||
#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 = data_.pressure > 0.005f;
|
||||
}
|
||||
else if (ev.xcookie.evtype == XI_ButtonRelease) {
|
||||
data_.tip_down = false;
|
||||
data_.in_proximity = false;
|
||||
}
|
||||
else if (ev.xcookie.evtype == XI_Motion) {
|
||||
data_.in_proximity = data_.pressure > 0.005f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XFreeEventData(display_, &ev.xcookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void TabletInput::terminate()
|
||||
{
|
||||
#ifdef HAVE_X11TABLETINPUT
|
||||
if (display_) {
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
#endif // LINUX
|
||||
@@ -17,8 +17,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "IconsFontAwesome5.h"
|
||||
#include <glad/glad.h>
|
||||
#include <glm/common.hpp>
|
||||
#include <glm/ext/vector_float2.hpp>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
#define GLM_ENABLE_EXPERIMENTAL
|
||||
@@ -42,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"
|
||||
|
||||
@@ -226,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());
|
||||
@@ -450,6 +463,8 @@ std::pair<Node *, glm::vec2> 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<glm::vec3>();
|
||||
return pick;
|
||||
}
|
||||
// special case for cropping the mask shape
|
||||
@@ -792,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");
|
||||
@@ -815,11 +830,22 @@ 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);
|
||||
// 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);
|
||||
@@ -846,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() ) {
|
||||
@@ -889,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());
|
||||
}
|
||||
@@ -1106,16 +1143,38 @@ 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_;
|
||||
|
||||
// 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,
|
||||
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
|
||||
@@ -1547,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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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&) {}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user