/* * This file is part of vimix - video live mixer * * **Copyright** (C) 2019-2023 Bruno Herbelin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . **/ #include #include #include #include #define GLM_ENABLE_EXPERIMENTAL #include #include "ImGuiToolkit.h" #include #include #include #include #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(*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(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 LayerView::pick(glm::vec2 P) { // get picking from generic View std::pair 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) { 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 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_; }