mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-11 18:34:58 +01:00
Management of inputs in Control, Management of callbacks creator per input in Source, Saving and Loading in Session, Unified renaming of SourceCallbacks, User interface window for creating and editing input mapping from Keyboard and Numerical keypad, with appropriate Settings.
410 lines
14 KiB
C++
410 lines
14 KiB
C++
/*
|
|
* This file is part of vimix - video live mixer
|
|
*
|
|
* **Copyright** (C) 2019-2022 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 <glad/glad.h>
|
|
#include <glm/glm.hpp>
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
#include <glm/gtc/matrix_access.hpp>
|
|
#include <glm/gtx/vector_angle.hpp>
|
|
|
|
#include "ImGuiToolkit.h"
|
|
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
|
|
#include "Mixer.h"
|
|
#include "defines.h"
|
|
#include "Source.h"
|
|
#include "SourceCallback.h"
|
|
#include "Settings.h"
|
|
#include "Decorations.h"
|
|
#include "UserInterfaceManager.h"
|
|
#include "BoundingBoxVisitor.h"
|
|
#include "ActionManager.h"
|
|
#include "Log.h"
|
|
|
|
#include "LayerView.h"
|
|
|
|
LayerView::LayerView() : View(LAYER), aspect_ratio(1.f)
|
|
{
|
|
scene.root()->scale_ = glm::vec3(LAYER_DEFAULT_SCALE, LAYER_DEFAULT_SCALE, 1.0f);
|
|
scene.root()->translation_ = glm::vec3(2.2f, 1.2f, 0.0f);
|
|
// read default settings
|
|
if ( Settings::application.views[mode_].name.empty() )
|
|
// no settings found: store application default
|
|
saveSettings();
|
|
else
|
|
restoreSettings();
|
|
Settings::application.views[mode_].name = "Layer";
|
|
|
|
// Geometry Scene background
|
|
frame_ = new Group;
|
|
Surface *rect = new Surface;
|
|
rect->shader()->color.a = 0.3f;
|
|
frame_->attach(rect);
|
|
|
|
Frame *border = new Frame(Frame::ROUND, Frame::THIN, Frame::PERSPECTIVE);
|
|
border->color = glm::vec4( COLOR_FRAME, 0.95f );
|
|
frame_->attach(border);
|
|
scene.bg()->attach(frame_);
|
|
|
|
persp_left_ = new Mesh("mesh/perspective_axis_left.ply");
|
|
persp_left_->shader()->color = glm::vec4( COLOR_FRAME_LIGHT, 1.f );
|
|
persp_left_->scale_.x = LAYER_PERSPECTIVE;
|
|
persp_left_->translation_.z = -0.1f;
|
|
persp_left_->translation_.x = -1.f;
|
|
scene.bg()->attach(persp_left_);
|
|
|
|
persp_right_ = new Mesh("mesh/perspective_axis_right.ply");
|
|
persp_right_->shader()->color = glm::vec4( COLOR_FRAME_LIGHT, 1.f );
|
|
persp_right_->scale_.x = LAYER_PERSPECTIVE;
|
|
persp_right_->translation_.z = -0.1f;
|
|
persp_right_->translation_.x = 1.f;
|
|
scene.bg()->attach(persp_right_);
|
|
|
|
}
|
|
|
|
|
|
void LayerView::draw()
|
|
{
|
|
View::draw();
|
|
|
|
// initialize the verification of the selection
|
|
static bool candidate_flatten_group = false;
|
|
|
|
// display popup menu
|
|
if (show_context_menu_ == MENU_SELECTION) {
|
|
|
|
// initialize the verification of the selection
|
|
candidate_flatten_group = true;
|
|
// start loop on selection
|
|
SourceList::iterator it = Mixer::selection().begin();
|
|
float depth_first = (*it)->depth();
|
|
for (; it != Mixer::selection().end(); ++it) {
|
|
// test if selection is contiguous in layer (i.e. not interrupted)
|
|
SourceList::iterator inter = Mixer::manager().session()->find(depth_first, (*it)->depth());
|
|
if ( inter != Mixer::manager().session()->end() && !Mixer::selection().contains(*inter)){
|
|
// NOT a group: there is a source in the session that
|
|
// - is between two selected sources (in depth)
|
|
// - is not part of the selection
|
|
candidate_flatten_group = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ImGui::OpenPopup( "LayerSelectionContextMenu" );
|
|
show_context_menu_ = MENU_NONE;
|
|
}
|
|
if (ImGui::BeginPopup("LayerSelectionContextMenu")) {
|
|
|
|
// colored context menu
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiToolkit::HighlightColor());
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.36f, 0.36f, 0.36f, 0.44f));
|
|
|
|
// special action of Mixing view
|
|
if (candidate_flatten_group){
|
|
if (ImGui::Selectable( ICON_FA_DOWNLOAD " Flatten" )) {
|
|
Mixer::manager().groupSelection();
|
|
}
|
|
}
|
|
else {
|
|
ImGui::TextDisabled( ICON_FA_DOWNLOAD " Flatten" );
|
|
}
|
|
ImGui::Separator();
|
|
|
|
// manipulation of sources in Mixing view
|
|
if (ImGui::Selectable( ICON_FA_ALIGN_CENTER " Distribute" )){
|
|
SourceList dsl = depth_sorted(Mixer::selection().getCopy());
|
|
SourceList::iterator it = dsl.begin();
|
|
float depth = (*it)->depth();
|
|
float depth_inc = (dsl.back()->depth() - depth) / static_cast<float>(Mixer::selection().size()-1);
|
|
for (++it; it != dsl.end(); ++it) {
|
|
depth += depth_inc;
|
|
(*it)->call( new SetDepth(depth, 80.f) );
|
|
}
|
|
Action::manager().store(std::string("Selection: Layer Distribute"));
|
|
}
|
|
if (ImGui::Selectable( ICON_FA_RULER_HORIZONTAL " Compress" )){
|
|
SourceList dsl = depth_sorted(Mixer::selection().getCopy());
|
|
SourceList::iterator it = dsl.begin();
|
|
float depth = (*it)->depth();
|
|
for (++it; it != dsl.end(); ++it) {
|
|
depth += LAYER_STEP;
|
|
(*it)->call( new SetDepth(depth, 80.f) );
|
|
}
|
|
Action::manager().store(std::string("Selection: Layer Compress"));
|
|
}
|
|
if (ImGui::Selectable( ICON_FA_EXCHANGE_ALT " Reverse order" )){
|
|
SourceList dsl = depth_sorted(Mixer::selection().getCopy());
|
|
SourceList::iterator it = dsl.begin();
|
|
SourceList::reverse_iterator rit = dsl.rbegin();
|
|
for (; it != dsl.end(); ++it, ++rit) {
|
|
(*it)->call( new SetDepth((*rit)->depth(), 80.f) );
|
|
}
|
|
Action::manager().store(std::string("Selection: Layer Reverse order"));
|
|
}
|
|
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
|
|
void LayerView::update(float dt)
|
|
{
|
|
View::update(dt);
|
|
|
|
// a more complete update is requested
|
|
if (View::need_deep_update_ > 0) {
|
|
|
|
// update rendering of render frame
|
|
FrameBuffer *output = Mixer::manager().session()->frame();
|
|
if (output){
|
|
// correct with aspect ratio
|
|
aspect_ratio = output->aspectRatio();
|
|
frame_->scale_.x = aspect_ratio;
|
|
persp_left_->translation_.x = -aspect_ratio;
|
|
persp_right_->translation_.x = aspect_ratio + 0.06;
|
|
}
|
|
|
|
// prevent invalid scaling
|
|
float s = CLAMP(scene.root()->scale_.x, LAYER_MIN_SCALE, LAYER_MAX_SCALE);
|
|
scene.root()->scale_.x = s;
|
|
scene.root()->scale_.y = s;
|
|
}
|
|
|
|
if (Mixer::manager().view() == this )
|
|
{
|
|
// update the selection overlay
|
|
updateSelectionOverlay();
|
|
}
|
|
|
|
}
|
|
|
|
bool LayerView::canSelect(Source *s) {
|
|
|
|
return ( View::canSelect(s) && s->active() );
|
|
}
|
|
|
|
void LayerView::resize ( int scale )
|
|
{
|
|
float z = CLAMP(0.01f * (float) scale, 0.f, 1.f);
|
|
z *= z;
|
|
z *= LAYER_MAX_SCALE - LAYER_MIN_SCALE;
|
|
z += LAYER_MIN_SCALE;
|
|
scene.root()->scale_.x = z;
|
|
scene.root()->scale_.y = z;
|
|
|
|
// Clamp translation to acceptable area
|
|
glm::vec3 border(2.f, 1.f, 0.f);
|
|
scene.root()->translation_ = glm::clamp(scene.root()->translation_, -border, border * 2.f);
|
|
}
|
|
|
|
int LayerView::size ()
|
|
{
|
|
float z = (scene.root()->scale_.x - LAYER_MIN_SCALE) / (LAYER_MAX_SCALE - LAYER_MIN_SCALE);
|
|
return (int) ( sqrt(z) * 100.f);
|
|
}
|
|
|
|
|
|
std::pair<Node *, glm::vec2> LayerView::pick(glm::vec2 P)
|
|
{
|
|
// get picking from generic View
|
|
std::pair<Node *, glm::vec2> pick = View::pick(P);
|
|
|
|
// deal with internal interactive objects
|
|
if ( overlay_selection_icon_ != nullptr && pick.first == overlay_selection_icon_ ) {
|
|
|
|
openContextMenu(MENU_SELECTION);
|
|
}
|
|
else {
|
|
// get if a source was picked
|
|
Source *s = Mixer::manager().findSource(pick.first);
|
|
if (s != nullptr) {
|
|
// pick on the lock icon; unlock source
|
|
if ( UserInterface::manager().ctrlModifier() && pick.first == s->lock_) {
|
|
lock(s, false);
|
|
// pick = { s->locker_, pick.second };
|
|
pick = { nullptr, glm::vec2(0.f) };
|
|
}
|
|
// pick on the open lock icon; lock source and cancel pick
|
|
else if ( UserInterface::manager().ctrlModifier() && pick.first == s->unlock_ ) {
|
|
lock(s, true);
|
|
pick = { nullptr, glm::vec2(0.f) };
|
|
}
|
|
// pick a locked source; cancel pick
|
|
else if ( s->locked() ) {
|
|
pick = { nullptr, glm::vec2(0.f) };
|
|
}
|
|
// pick the symbol: ask to show editor
|
|
else if ( pick.first == s->symbol_ ) {
|
|
UserInterface::manager().showSourceEditor(s);
|
|
}
|
|
}
|
|
else
|
|
pick = { nullptr, glm::vec2(0.f) };
|
|
}
|
|
|
|
return pick;
|
|
}
|
|
|
|
|
|
float LayerView::setDepth(Source *s, float d)
|
|
{
|
|
if (!s)
|
|
return -1.f;
|
|
|
|
// move the layer node of the source
|
|
Group *sourceNode = s->group(mode_);
|
|
|
|
float depth = d < 0.f ? sourceNode->translation_.z : d;
|
|
|
|
// negative or no depth given; find the front most depth
|
|
if ( depth < 0.f ) {
|
|
// default to place visible in front of background
|
|
depth = LAYER_BACKGROUND + LAYER_STEP;
|
|
|
|
// find the front-most souce in the workspace (behind FOREGROUND)
|
|
for (NodeSet::iterator node = scene.ws()->begin(); node != scene.ws()->end(); ++node) {
|
|
// place in front of previous sources
|
|
depth = MAX(depth, (*node)->translation_.z + LAYER_STEP);
|
|
|
|
// in case node is already at max depth
|
|
if ((*node)->translation_.z + DELTA_DEPTH > MAX_DEPTH )
|
|
(*node)->translation_.z -= DELTA_DEPTH;
|
|
}
|
|
}
|
|
|
|
// change depth
|
|
sourceNode->translation_.z = CLAMP( depth, MIN_DEPTH, MAX_DEPTH);
|
|
|
|
// request reordering of scene at next update
|
|
View::need_deep_update_++;
|
|
|
|
// request update of source
|
|
s->touch();
|
|
|
|
return sourceNode->translation_.z;
|
|
}
|
|
|
|
View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair<Node *, glm::vec2>)
|
|
{
|
|
if (!s)
|
|
return Cursor();
|
|
|
|
// unproject
|
|
glm::vec3 gl_Position_from = Rendering::manager().unProject(from, scene.root()->transform_);
|
|
glm::vec3 gl_Position_to = Rendering::manager().unProject(to, scene.root()->transform_);
|
|
|
|
// compute delta translation
|
|
glm::vec3 dest_translation = s->stored_status_->translation_ + gl_Position_to - gl_Position_from;
|
|
|
|
// discretized translation with ALT
|
|
if (UserInterface::manager().altModifier())
|
|
dest_translation.x = ROUND(dest_translation.x, 5.f);
|
|
|
|
// apply change
|
|
float d = setDepth( s, MAX( -dest_translation.x, 0.f) );
|
|
|
|
// store action in history
|
|
std::ostringstream info;
|
|
info << "Depth " << std::fixed << std::setprecision(2) << d << " ";
|
|
current_action_ = s->name() + ": " + info.str();
|
|
|
|
return Cursor(Cursor_ResizeNESW, info.str() );
|
|
}
|
|
|
|
void LayerView::arrow (glm::vec2 movement)
|
|
{
|
|
static float accumulator = 0.f;
|
|
accumulator += dt_;
|
|
|
|
glm::vec3 gl_Position_from = Rendering::manager().unProject(glm::vec2(0.f), scene.root()->transform_);
|
|
glm::vec3 gl_Position_to = Rendering::manager().unProject(glm::vec2(movement.x-movement.y, 0.f), scene.root()->transform_);
|
|
glm::vec3 gl_delta = gl_Position_to - gl_Position_from;
|
|
|
|
bool first = true;
|
|
glm::vec3 delta_translation(0.f);
|
|
for (auto it = Mixer::selection().begin(); it != Mixer::selection().end(); it++) {
|
|
|
|
// individual move with SHIFT
|
|
if ( !Source::isCurrent(*it) && UserInterface::manager().shiftModifier() )
|
|
continue;
|
|
|
|
Group *sourceNode = (*it)->group(mode_);
|
|
glm::vec3 dest_translation(0.f);
|
|
|
|
if (first) {
|
|
// dest starts at current
|
|
dest_translation = sourceNode->translation_;
|
|
|
|
// + ALT : discrete displacement
|
|
if (UserInterface::manager().altModifier()) {
|
|
if (accumulator > 100.f) {
|
|
dest_translation += glm::sign(gl_delta) * 0.21f;
|
|
dest_translation.x = ROUND(dest_translation.x, 10.f);
|
|
accumulator = 0.f;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
else {
|
|
// normal case: dest += delta
|
|
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR * dt_;
|
|
accumulator = 0.f;
|
|
}
|
|
|
|
// store action in history
|
|
std::ostringstream info;
|
|
info << "Depth " << std::fixed << std::setprecision(2) << (*it)->depth() << " ";
|
|
current_action_ = (*it)->name() + ": " + info.str();
|
|
|
|
// delta for others to follow
|
|
delta_translation = dest_translation - sourceNode->translation_;
|
|
}
|
|
else {
|
|
// dest = current + delta from first
|
|
dest_translation = sourceNode->translation_ + delta_translation;
|
|
}
|
|
|
|
// apply & request update
|
|
setDepth( *it, MAX( -dest_translation.x, 0.f) );
|
|
|
|
first = false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void LayerView::updateSelectionOverlay()
|
|
{
|
|
View::updateSelectionOverlay();
|
|
|
|
if (overlay_selection_->visible_) {
|
|
// calculate bbox on selection
|
|
GlmToolkit::AxisAlignedBoundingBox selection_box = BoundingBoxVisitor::AABB(Mixer::selection().getCopy(), this);
|
|
overlay_selection_->scale_ = selection_box.scale();
|
|
overlay_selection_->translation_ = selection_box.center();
|
|
|
|
// slightly extend the boundary of the selection
|
|
overlay_selection_frame_->scale_ = glm::vec3(1.f) + glm::vec3(0.07f, 0.07f, 1.f) / overlay_selection_->scale_;
|
|
}
|
|
}
|