Files
vimix/src/LayerView.cpp
T
Bruno Herbelin 28943adac8 Layers Selection Blending and Bundle
Avoid creation of bundle from selection that contains clones
2025-04-21 15:15:10 +02:00

614 lines
21 KiB
C++

/*
* 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 <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/matrix_access.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/vector_angle.hpp>
#include "ImGuiToolkit.h"
#include <algorithm>
#include <string>
#include <sstream>
#include <iomanip>
#include "Mixer.h"
#include "defines.h"
#include "Source.h"
#include "CloneSource.h"
#include "SourceCallback.h"
#include "Settings.h"
#include "Decorations.h"
#include "UserInterfaceManager.h"
#include "BoundingBoxVisitor.h"
#include "ActionManager.h"
#include "MousePointer.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 = "Layers";
// 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_);
// replace grid with appropriate one
if (grid) delete grid;
grid = new LayerGrid(scene.root());
}
void LayerView::draw()
{
// Display grid
grid->root()->visible_ = (grid->active() && current_action_ongoing_);
View::draw();
// initialize the verification of the selection
static bool candidate_flatten_group = false;
// display popup menu source
if (show_context_menu_ == MENU_SOURCE) {
ImGui::OpenPopup("LayerSourceContextMenu");
show_context_menu_ = MENU_NONE;
}
if (ImGui::BeginPopup("LayerSourceContextMenu")) {
// work on the current source
Source *s = Mixer::manager().currentSource();
if (s != nullptr) {
for (auto bmode = Shader::blendingFunction.cbegin();
bmode != Shader::blendingFunction.cend();
++bmode) {
int index = bmode - Shader::blendingFunction.cbegin();
if (ImGuiToolkit::MenuItemIcon(std::get<0>(*bmode),
std::get<1>(*bmode),
std::get<2>(*bmode).c_str(),
nullptr,
s->blendingShader()->blending == index)) {
s->blendingShader()->blending = Shader::BlendMode(index);
s->touch();
Action::manager().store(s->name() + ": Blending " + std::get<2>(*bmode));
}
}
}
ImGui::EndPopup();
}
// display popup menu selection
if (show_context_menu_ == MENU_SELECTION) {
// initialize the verification of the selection
candidate_flatten_group = true;
SourceList _selected = Mixer::selection().getCopy();
// 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)){
// CANNOT 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;
}
// test if the source is a clone
CloneSource *_cs = dynamic_cast<CloneSource *>(*it);
if (_cs != nullptr) {
uint64_t _id_cloned = (*_cs).origin()->id();
SourceList::const_iterator _it = std::find_if(_selected.begin(),
_selected.end(),
Source::hasId(_id_cloned));
// CANNOT group: there is a clone selected and its origin is not selected
if (_it == _selected.end()) {
candidate_flatten_group = false;
break;
}
}
// test if the selected source is cloned
if ((*it)->cloned()) {
SourceList _clones = (*it)->clones();
SourceListCompare _diff = compare(_selected, _clones);
// CANNOT group: there all clones are not included in selection
if (_diff != SOURCELIST_SECOND_IN_FIRST){
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(COLOR_MENU_HOVERED, 0.5f));
// special action of Mixing view
if (candidate_flatten_group){
if (ImGui::Selectable( ICON_FA_SIGN_IN_ALT " Bundle" )) {
Mixer::manager().groupSelection();
}
}
else {
ImGui::TextDisabled( ICON_FA_SIGN_IN_ALT " Bundle" );
}
// Blending all selection
if (ImGuiToolkit::BeginMenuIcon( 5, 6, "Blending" )) {
for (auto bmode = Shader::blendingFunction.cbegin();
bmode != Shader::blendingFunction.cend();
++bmode) {
int index = bmode - Shader::blendingFunction.cbegin();
if (ImGuiToolkit::MenuItemIcon(std::get<0>(*bmode),
std::get<1>(*bmode),
std::get<2>(*bmode).c_str())) {
SourceList dsl = depth_sorted(Mixer::selection().getCopy());
for (SourceList::iterator it = dsl.begin(); it != dsl.end(); ++it) {
(*it)->blendingShader()->blending = Shader::BlendMode(index);
(*it)->touch();
}
Action::manager().store(std::string("Blending selected " ICON_FA_LAYER_GROUP));
}
}
ImGui::EndMenu();
}
ImGui::Separator();
// manipulation of sources in Mixing view
if (ImGui::Selectable( ICON_FA_GRIP_LINES_VERTICAL ICON_FA_GRIP_LINES_VERTICAL " 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("Distribute selected " ICON_FA_LAYER_GROUP));
}
if (ImGui::Selectable( ICON_FA_CARET_RIGHT ICON_FA_CARET_LEFT " 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("Compress selected " ICON_FA_LAYER_GROUP));
}
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("Reverse order selected " ICON_FA_LAYER_GROUP));
}
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;
// change grid color
ImVec4 c = ImGuiToolkit::HighlightColor();
grid->setColor( glm::vec4(c.x, c.y, c.z, 0.3) );
}
if (Mixer::manager().view() == this )
{
// update the selection overlay
const ImVec4 c = ImGuiToolkit::HighlightColor();
updateSelectionOverlay(glm::vec4(c.x, c.y, c.z, c.w));
}
}
bool LayerView::canSelect(Source *s) {
return ( View::canSelect(s) );
}
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 ( !UserInterface::manager().ctrlModifier() && 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);
}
// pick the initials: show in panel
else if ( pick.first == s->initial_1_ ) {
UserInterface::manager().showPannel(Mixer::manager().indexCurrentSource());
UserInterface::manager().setSourceInPanel(s);
}
// pick blending icon
else if (pick.first == s->blendmode_->activeChild()) {
openContextMenu(MENU_SOURCE);
}
}
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) {
// discard foreground
if ((*node)->translation_.z > LAYER_FOREGROUND )
break;
// 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;
// snap to grid (polar)
if (grid->active())
dest_translation = grid->snap(dest_translation * 0.5f) * 2.f;
// apply change
float d = setDepth( s, MAX( -dest_translation.x, 0.f) );
//
// grab all others in selection
//
// compute effective depth translation of current source s
float dp = s->group(mode_)->translation_.z - s->stored_status_->translation_.z;
// loop over selection
for (auto it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
if ( *it != s && !(*it)->locked() ) {
// set depth and request update
setDepth( *it, (*it)->stored_status_->translation_.z + dp);
}
}
// store action in history
std::ostringstream info;
info << "Depth " << std::fixed << std::setprecision(2) << d << " ";
current_action_ = s->name() + ": " + info.str();
if ( d > LAYER_FOREGROUND )
info << "\n (Foreground layer)";
else if ( d < LAYER_BACKGROUND )
info << "\n (Background layer)";
else
info << "\n (Workspace layer)";
return Cursor(Cursor_ResizeNESW, info.str() );
}
View::Cursor LayerView::over (glm::vec2 pos)
{
View::Cursor ret = Cursor();
std::pair<Node *, glm::vec2> pick = View::pick(pos);
//
// mouse over source
//
// Source *s = Mixer::manager().findSource(pick.first);
Source *s = Mixer::manager().currentSource();
if (s != nullptr && s->ready()) {
s->symbol_->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f );
s->initial_0_->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f );
s->initial_1_->color = glm::vec4( COLOR_HIGHLIGHT_SOURCE, 1.f );
const ImVec4 h = ImGuiToolkit::HighlightColor();
// overlay symbol
if ( pick.first == s->symbol_ )
s->symbol_->color = glm::vec4( h.x, h.y, h.z, 1.f );
// overlay initials
else if ( pick.first == s->initial_1_ ) {
s->initial_1_->color = glm::vec4( h.x, h.y, h.z, 1.f );
s->initial_0_->color = glm::vec4( h.x, h.y, h.z, 1.f );
}
}
return ret;
}
void LayerView::arrow (glm::vec2 movement)
{
static glm::vec2 _from(0.f);
static glm::vec2 _displacement(0.f);
Source *current = Mixer::manager().currentSource();
if (!current && !Mixer::selection().empty())
Mixer::manager().setCurrentSource( Mixer::selection().back() );
if (current) {
if (current_action_ongoing_) {
// add movement to displacement
movement.x += movement.y * -0.5f;
_displacement += glm::vec2(movement.x, -0.5f * movement.x) * dt_ * 0.2f;
// set coordinates of target
glm::vec2 _to = _from + _displacement;
// update mouse pointer action
MousePointer::manager().active()->update(_to, dt_ / 1000.f);
// simulate mouse grab
grab(current, _from, MousePointer::manager().active()->target(),
std::make_pair(current->group(mode_), glm::vec2(0.f) ) );
// draw mouse pointer effect
MousePointer::manager().active()->draw();
}
else {
if (UserInterface::manager().altModifier() || Settings::application.mouse_pointer_lock)
MousePointer::manager().setActiveMode( (Pointer::Mode) Settings::application.mouse_pointer );
else
MousePointer::manager().setActiveMode( Pointer::POINTER_DEFAULT );
// initiate view action and store status of source
initiate();
// get coordinates of source and set this as start of mouse position
_from = glm::vec2( Rendering::manager().project(current->group(mode_)->translation_, scene.root()->transform_) );
_displacement = glm::vec2(0.f);
// Initiate mouse pointer action
MousePointer::manager().active()->initiate(_from);
}
}
else {
terminate(true);
_from = glm::vec2(0.f);
_displacement = glm::vec2(0.f);
}
}
void LayerView::updateSelectionOverlay(glm::vec4 color)
{
View::updateSelectionOverlay(color);
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_;
}
}
LayerGrid::LayerGrid(Group *parent) : Grid(parent)
{
root_ = new Group;
root_->visible_ = false;
parent_->attach(root_);
// create custom grids specific for layers in diagonal
perspective_grids_ = new Switch;
root_->attach(perspective_grids_);
// Generate groups for all units
for (uint u = UNIT_PRECISE; u <= UNIT_ONE; u = u + 1) {
Group *g = new Group;
float d = MIN_DEPTH;
// Fill background
for (; d < LAYER_BACKGROUND ; d += Grid::ortho_units_[u] * 2.f) {
HLine *l = new HLine(3.f);
l->translation_.x = -d +1.f;
l->translation_.y = -d / LAYER_PERSPECTIVE - 1.f;
l->scale_.x = 3.5f;
g->attach(l);
}
// Fill workspace
for (; d < LAYER_FOREGROUND ; d += Grid::ortho_units_[u] * 2.f) {
HLine *l = new HLine(3.f);
l->translation_.x = -d +1.f;
l->translation_.y = -d / LAYER_PERSPECTIVE - 1.15f;
l->scale_.x = 3.5f;
g->attach(l);
}
// Fill foreground
for (; d < MAX_DEPTH ; d += Grid::ortho_units_[u] * 2.f) {
HLine *l = new HLine(3.f);
l->translation_.x = -d +1.f;
l->translation_.y = -d / LAYER_PERSPECTIVE - 1.3f;
l->scale_.x = 3.5f;
g->attach(l);
}
// add this group to the grids
perspective_grids_->attach(g);
}
// not visible at init
// setColor( glm::vec4(0.f) );
}
Group *LayerGrid::root ()
{
//adjust the grid to the unit scale
perspective_grids_->setActive(unit_);
// return the node to draw
return root_;
}