From b80a0db027fa4c02b34d208141dc575443cc1a44 Mon Sep 17 00:00:00 2001 From: lachrymaLF Date: Thu, 20 Jun 2024 03:59:11 -0400 Subject: [PATCH] good graph editor --- Keishiki/Keishiki.cpp | 4 +- Keishiki/UI.cpp | 393 +++++++++++++++++++++--------- Keishiki/include/PlugboardNodes.h | 2 +- TODO.md | 4 +- 4 files changed, 281 insertions(+), 122 deletions(-) diff --git a/Keishiki/Keishiki.cpp b/Keishiki/Keishiki.cpp index c73b3aa..03b60a2 100644 --- a/Keishiki/Keishiki.cpp +++ b/Keishiki/Keishiki.cpp @@ -130,13 +130,13 @@ namespace K { const bgfx::Stats *stat = bgfx::getStats(); const bgfx::Caps *caps = bgfx::getCaps(); bgfx::dbgTextClear(); - bgfx::dbgTextPrintf(0, app_state.window_height/16 - 4, 0xf8, + bgfx::dbgTextPrintf(0, app_state.window_height/16 - 8, 0xf8, " lachrymal.net :: %s Renderer :: Max Views %u :: Max FBs %u :: Max Texture Samplers %u :: Blitting %s :: %u FPS", caps->supported & BGFX_CAPS_RENDERER_MULTITHREADED ? "Multithreaded" : "Singlethreaded", caps->limits.maxViews, caps->limits.maxFrameBuffers, caps->limits.maxTextureSamplers, caps->supported & BGFX_CAPS_TEXTURE_BLIT ? "OK" : "NO", stat->cpuTimerFreq / stat->cpuTimeFrame); - bgfx::dbgTextPrintf(0, app_state.window_height/16 - 3, 0xf8, + bgfx::dbgTextPrintf(0, app_state.window_height/16 - 7, 0xf8, " Project Keishiki :: %s :: Build %s %s :: SDL %i.%i.%i :: bgfx 1.%i :: Dear ImGui %s :: %s", BX_COMPILER_NAME, __DATE__, __TIME__, SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL, BGFX_API_VERSION, ImGui::GetVersion(), bgfx::getRendererName(bgfx::getRendererType())); diff --git a/Keishiki/UI.cpp b/Keishiki/UI.cpp index 7ba1802..00263d7 100644 --- a/Keishiki/UI.cpp +++ b/Keishiki/UI.cpp @@ -403,6 +403,13 @@ namespace K::UI { return std::clamp(static_cast(std::round((view / view_width * view_amt + view_left) * static_cast(frame_max + 1))), static_cast(0), frame_max); } + struct PlotInfo { + f32 begin, end; + i32 samples; + const CompState &s; + PlugboardGraph::ConnectInfo &connected_v; + }; + void Composition(CompState& s) { if (!ImGui::Begin("Composition", &draw_comp, ImGuiWindowFlags_NoScrollbar)) { ImGui::End(); @@ -481,8 +488,9 @@ namespace K::UI { ImGui::Checkbox("Chain Editor", &show_curve_editor); ImGuiIO& io = ImGui::GetIO(); + ImRect drag_rect{io.MousePos, io.MousePos}; + drag_rect.Add(io.MouseClickedPos[ImGuiMouseButton_Left]); ImGuiStyle style = ImGui::GetStyle(); - ImVec2 avail = ImGui::GetContentRegionAvail(); static f32 view_scale = 1.0f; static ImVec2 view_pos{}, old_view_pos{}, nodes_begin{}; @@ -491,8 +499,8 @@ namespace K::UI { ImVec2 drag_source{}; ImGui::PushStyleColor(ImGuiCol_ChildBg, 0xFF100500); - ImGui::SetNextWindowSizeConstraints({avail.x * .1f, -1}, { avail.x * .8f, -1 }); - if (ImGui::BeginChild("Nodes", {avail.x * .4f, avail.y}, ImGuiChildFlags_ResizeX, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar)) { + ImGui::SetNextWindowSizeConstraints({ImGui::GetContentRegionAvail().x * .1f, -1}, { ImGui::GetContentRegionAvail().x * .8f, -1 }); + if (ImGui::BeginChild("Nodes", {ImGui::GetContentRegionAvail().x * .4f, ImGui::GetContentRegionAvail().y}, ImGuiChildFlags_ResizeX, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar)) { static u32 selected; ImGuiWindow *window = ImGui::GetCurrentWindow(); @@ -548,6 +556,7 @@ namespace K::UI { ImGui::SameLine(0.0f, 0.5f); + // Timeline setup f32 table_left = ImGui::GetCursorScreenPos().x; ImVec2 tl_init_pos, tl_end_begin; f32 view_width, @@ -562,18 +571,28 @@ namespace K::UI { f32 view_amt = view_right - view_left, mouse_tl_x; f32 fr_step; + static Vector selected_chains{}; + // Mouse Handlers // Drag playhead/pan static bool tl_clear_selection_request = false; bool tl_clear_selection_request_this_frame = tl_clear_selection_request; - auto draw_tl_bg_drag_area = [&view_width, &style, &view_amt, &s, &mouse_tl_x](f32 h = 0.0f) { + static bool bg_drag_select_active = false; + static Vector keys_selected_by_bg_drag{}; + auto draw_tl_bg_drag_area = [&view_width, &style, &view_amt, &s, &mouse_tl_x, &io](f32 h = 0.0f, f32 w = 0.0f) { static f32 pan_x, view_left_old, view_right_old; - ImGui::InvisibleButton("##TL_BG", ImVec2{ view_width + style.CellPadding.x * 2, h == 0.0f ? row_height + style.CellPadding.y : h}); + ImGui::InvisibleButton("##TL_BG", ImVec2{ w == 0.0f ? view_width + style.CellPadding.x * 2 : w, h == 0.0f ? row_height + style.CellPadding.y : h}); if (ImGui::IsItemClicked()) { - tl_clear_selection_request = true; + tl_clear_selection_request = !(io.KeyCtrl || io.KeyShift); + bg_drag_select_active = !io.KeyShift; + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + bg_drag_select_active = false; + keys_selected_by_bg_drag.clear(); } if (ImGui::IsItemActive()) { - s.current_frame = TimelineScreenViewToFrame(view_left, view_amt, view_width, std::clamp(mouse_tl_x, 0.0f, view_width), s.frame_max); + if (io.KeyShift) + s.current_frame = TimelineScreenViewToFrame(view_left, view_amt, view_width, std::clamp(mouse_tl_x, 0.0f, view_width), s.frame_max); } if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) { pan_x = mouse_tl_x; @@ -596,25 +615,60 @@ namespace K::UI { } }; - // Zoom - auto tl_scroll_zoom = [&mouse_tl_x, &view_width, &io, &view_amt]() { - if (ImGui::IsWindowHovered() && ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && mouse_tl_x >= 0 && mouse_tl_x < view_width && io.MouseWheel != 0.0f) { - f32 center = mouse_tl_x / view_width * view_amt + view_left; - f32 new_view_amt = std::clamp(view_amt - 0.05f * io.MouseWheel, 0.05f, 1.0f); - view_left = center - new_view_amt / 2.0f - (mouse_tl_x / view_width - .5f) * new_view_amt; - view_right = center + new_view_amt / 2.0f - (mouse_tl_x / view_width - .5f) * new_view_amt; - if (view_left < 0.0f) { - view_right -= view_left; - view_left = 0.0f; + constexpr static f32 knob_width = 8.0f; + constexpr static f32 kf_tab_width = 6.0f; + + auto kf_drag_btn_handler = [&io, &drag_rect]( + PlugboardNodes::ChainSel *key_loop_target, bool dragging, PlugboardNodes::ChainSel& chain, i32 frame, bool is_sel, + Vector::iterator sel_it, const PlugboardNodes::InterpolationExtra& segment, f64 v, u32 ii, bool& started_dragging, const ImVec2& k_pos) { + if (dragging) { + auto pos = std::lower_bound(chain.segments.begin(), chain.segments.end(), frame, + [](const PlugboardNodes::ChainSegment &a, i32 b) { + return a.frame < b; + }); + + auto pos_index = std::distance(chain.segments.begin(), pos); + + if (pos != chain.segments.end() && pos->frame == frame) { // overlapping kf's + if (is_sel) + pos->value = static_cast(v); + } else { + for (auto &sel: chain.selected) { + if (sel >= pos_index) + sel++; + } + chain.segments.emplace(pos, frame, v, segment); } - else if (view_right > 1.0f) { - view_left -= (view_right - 1.0f); - view_right = 1.0f; + + if (is_sel) + chain.selected.push_back(pos_index); + } else { + if (ImGui::IsItemClicked()) { + started_dragging = true; // setup dragging & start on next frame + + if (!is_sel || ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + if (!io.KeyCtrl) + chain.selected.clear(); + chain.selected.push_back(ii); + } + } + if (!is_sel && bg_drag_select_active && + drag_rect.Contains(k_pos)) { + keys_selected_by_bg_drag.push_back( + &(key_loop_target->segments)[ii]); // This sucks pretty bad... + chain.selected.push_back(ii); + } + auto bg_drag_sel_it = std::find(keys_selected_by_bg_drag.begin(), + keys_selected_by_bg_drag.end(), + &(key_loop_target->segments)[ii]); + if (is_sel && bg_drag_sel_it != keys_selected_by_bg_drag.end() && + !drag_rect.Contains(k_pos)) { + chain.selected.erase(sel_it); + keys_selected_by_bg_drag.erase(bg_drag_sel_it); } } }; - constexpr static f32 knob_width = 8.0f; if (ImGui::BeginTable("Layers", 5, ImGuiTableFlags_BordersInner | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) { ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible ImGui::TableSetupColumn("#", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_IndentDisable); @@ -704,7 +758,6 @@ namespace K::UI { ImGui::TableHeader(""); // Main rows - tl_scroll_zoom(); i32 move_from = -1, move_to = -1; bool after{}; @@ -929,7 +982,7 @@ namespace K::UI { ImGui::SetNextItemWidth(-FLT_MIN); - if (connected_v.index() == 0) { + if (connected_v.index() == 0) std::visit([](auto&& arg) { using T = std::decay_t; if constexpr (std::is_same_v::type>) @@ -943,7 +996,7 @@ namespace K::UI { else if constexpr (std::is_same_v::type>) ImGui::DragFloat3("##", &arg.x, 0.005f); }, std::get<0>(connected_v)); - } + ImGui::TableSetColumnIndex(3); ImGui::TableSetColumnIndex(4); auto p = ImGui::GetCursorScreenPos(); @@ -957,11 +1010,10 @@ namespace K::UI { if (connected_v.index() == 1 && ImPlot::BeginPlot("uniform", {view_width, row_height}, ImPlotFlags_CanvasOnly | ImPlotFlags_NoFrame | ImPlotFlags_NoInputs)) { ImPlot::SetupAxis(ImAxis_X1, "time", ImPlotAxisFlags_NoDecorations | ImPlotAxisFlags_NoHighlight); ImPlot::SetupAxis(ImAxis_Y1, "val", ImPlotAxisFlags_NoDecorations | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_AutoFit); - struct PlotInfo { f32 begin, end; i32 samples; const CompState &s; PlugboardGraph::ConnectInfo &connected_v; }; struct PlotInfo info { .begin = view_left * static_cast(s.frame_max + 1), .end = view_right * static_cast(s.frame_max + 1), - .samples = 200, + .samples = 100, .s = s, .connected_v = std::get<1>(connected_v) }; @@ -994,10 +1046,19 @@ namespace K::UI { ImGui::TableSetColumnIndex(0); ImGui::TableSetColumnIndex(1); ImGui::Indent(); - ImGui::TreeNodeEx(info.p->node->name.c_str(), - ImGuiTreeNodeFlags_SpanFullWidth | - ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_Leaf | - ImGuiTreeNodeFlags_FramePadding); + auto n_flags = ImGuiTreeNodeFlags_SpanFullWidth | + ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_Leaf | + ImGuiTreeNodeFlags_FramePadding; + auto chain_sel_it = std::find(selected_chains.begin(), selected_chains.end(), info.p); + if (chain_sel_it != selected_chains.end()) + n_flags |= ImGuiTreeNodeFlags_Selected; + ImGui::TreeNodeEx(info.p->node->name.c_str(), n_flags); + if (ImGui::IsItemClicked()) { + if (chain_sel_it != selected_chains.end()) + selected_chains.erase(chain_sel_it); + else + selected_chains.push_back(info.p); + } ImGui::TableSetColumnIndex(2); auto& [chain, chain_copy] = *std::any_cast(&info.p->extra); @@ -1022,6 +1083,9 @@ namespace K::UI { f32 v = std::get::type>( PlugboardGraph::Eval(s, info)); if (ImGui::DragFloat("##value", &v, 0.005f)) { + if (std::find(selected_chains.begin(), selected_chains.end(), info.p) == selected_chains.end()) + selected_chains.push_back(info.p); + auto l = std::lower_bound(chain.segments.begin(), chain.segments.end(), s.current_frame, [](const PlugboardNodes::ChainSegment& a, i32 b) { return a.frame < b; }); if (l != chain.segments.end() && l->frame == static_cast(s.current_frame)) @@ -1052,7 +1116,6 @@ namespace K::UI { if (started_dragging) chain_copy = chain; - const static f32 kf_tab_width = 6.0f; auto key_loop_target = &chain; if (dragging) { chain.segments.clear(); @@ -1071,48 +1134,17 @@ namespace K::UI { frame += static_cast(ImGui::GetMouseDragDelta().x / tl_width * view_amt * static_cast(s.frame_max + 1)); } + ImVec2 draw_pos = {TimelineFrameToScreenView(view_left, view_amt, view_width, frame, + s.frame_max) + begin_tl.x - kf_tab_width / 2.0f, + begin_tl.y}; - ImGui::SetCursorScreenPos( - {TimelineFrameToScreenView(view_left, view_amt, view_width, frame, - s.frame_max) + begin_tl.x - kf_tab_width / 2.0f, - begin_tl.y}); + ImGui::SetCursorScreenPos(draw_pos); ImGui::Button(is_sel ? "s" : "k", {kf_tab_width, row_height * .8f}); - if (dragging) { - auto pos = std::lower_bound(chain.segments.begin(), chain.segments.end(), frame, - [](const PlugboardNodes::ChainSegment& a, i32 b) { - return a.frame < b; - }); + kf_drag_btn_handler(key_loop_target, dragging, chain, frame, is_sel, + sel_it, segment, val, ii, started_dragging, {draw_pos.x + kf_tab_width / 2.0f, draw_pos.y}); - auto pos_index = std::distance(chain.segments.begin(), pos); - - if (pos != chain.segments.end() && pos->frame == frame) { // overlapping kf's - if (is_sel) - pos->value = val; - } - else { - for (auto &sel: chain.selected) { - if (sel >= pos_index) - sel++; - } - chain.segments.emplace(pos, frame, val, segment); - } - - if (is_sel) - chain.selected.push_back(pos_index); - } - else { - if (ImGui::IsItemClicked()) { - started_dragging = true; // setup dragging & start on next frame - - if (!is_sel || ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - if (!io.KeyCtrl) - chain.selected.clear(); - chain.selected.push_back(ii); - } - } - } ImGui::PopID(); } for (auto it = chain.segments.begin(); it != chain.segments.end(); it++) { @@ -1138,12 +1170,12 @@ namespace K::UI { ImGui::TreePop(); } else for (auto &u: current_layer.track.uniforms) - std::visit([table_left, &s](auto &&arg) { - using T = std::decay_t; - if constexpr (std::is_same_v) - s.plugboard.links_pos[arg].sinks.push_back( - {table_left, ImGui::GetCursorScreenPos().y - row_height / 2.0f}); - }, u.connection.p->inputs_fed[u.connection.index]); + std::visit([table_left, &s](auto &&arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) + s.plugboard.links_pos[arg].sinks.push_back( + {table_left, ImGui::GetCursorScreenPos().y - row_height / 2.0f}); + }, u.connection.p->inputs_fed[u.connection.index]); ImGui::PopStyleVar(); // FramePadding @@ -1184,7 +1216,7 @@ namespace K::UI { } ImGui::SetCursorScreenPos(nodes_begin); - if (ImGui::BeginChild("Overlay", {table_left - ImGui::GetWindowPos().x, 0.0f}, false, ImGuiWindowFlags_NoInputs)) { + if (ImGui::BeginChild("Node Connection Overlay", {table_left - ImGui::GetWindowPos().x, 0.0f}, false, ImGuiWindowFlags_NoInputs)) { auto *window = ImGui::GetCurrentWindow(); for (auto &[_, link]: s.plugboard.links_pos) for (auto &sink: link.sinks) @@ -1201,7 +1233,6 @@ namespace K::UI { ImGui::SetCursorScreenPos(tl_end_begin); if (ImGui::BeginChild("TL Bottom Unfilled Drag Overlay")) { draw_tl_bg_drag_area(ImGui::GetContentRegionAvail().y); - tl_scroll_zoom(); } ImGui::EndChild(); @@ -1210,8 +1241,6 @@ namespace K::UI { ImGui::SetCursorScreenPos(tl_grid_cursor_pos); ImGui::PushStyleColor(ImGuiCol_ChildBg, 0xFF111111); if (ImGui::BeginChild("Curve Editor", ImGui::GetContentRegionAvail())) { - draw_tl_bg_drag_area(ImGui::GetContentRegionAvail().y); - tl_scroll_zoom(); ImGui::SetCursorScreenPos(tl_grid_cursor_pos); ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, {0.0f, 0.0f}); @@ -1220,48 +1249,148 @@ namespace K::UI { ImPlot::PushStyleColor(ImPlotCol_PlotBg, 0); ImPlot::PushStyleColor(ImPlotCol_PlotBorder, 0); if (ImPlot::BeginPlot("uniform", {view_width, ImGui::GetContentRegionAvail().y}, ImPlotFlags_CanvasOnly | ImPlotFlags_NoFrame | ImPlotFlags_NoInputs)) { - ImPlot::SetupAxis(ImAxis_X1, "time", ImPlotAxisFlags_NoDecorations | ImPlotAxisFlags_NoHighlight); - ImPlot::SetupAxis(ImAxis_Y1, "val", ImPlotAxisFlags_NoDecorations | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_AutoFit); -// struct PlotInfo { f32 begin, end; i32 samples; const CompState &s; PlugboardGraph::ConnectInfo &connected_v; }; -// struct PlotInfo info { -// .begin = view_left * static_cast(s.frame_max + 1), -// .end = view_right * static_cast(s.frame_max + 1), -// .samples = 200, -// .s = s, -// .connected_v = std::get<1>(connected_v) -// }; -// ImPlot::SetupAxisLimits(ImAxis_X1, info.begin, info.end, ImGuiCond_Always); -// ImPlot::SetupFinish(); -// ImPlot::PlotLineG("v", [](int idx, void* user_data) -> ImPlotPoint { -// PlotInfo i = *(PlotInfo*)user_data; -// f32 x = i.begin + (i.end - i.begin) * static_cast(idx) / static_cast(i.samples); -// CompState ss = i.s; -// ss.current_frame = static_cast(std::round(x)); -// bool good; -// Vector info; -// return {x, std::get::type>(PlugboardGraph::ConvertValue(PlugboardGraph::Eval(ss, i.connected_v, &info), PlugboardGraph::T_Float, good))}; -// }, &info, info.samples); - static float xs1[100], ys1[100]; - for (int i = 0; i < 100; ++i) { - xs1[i] = i * 0.01f; - ys1[i] = xs1[i] + 0.1f * ((float)rand() / (float)RAND_MAX); - } - static float xs2[50], ys2[50]; - for (int i = 0; i < 50; i++) { - xs2[i] = 0.25f + 0.2f * ((float)rand() / (float)RAND_MAX); - ys2[i] = 0.75f + 0.2f * ((float)rand() / (float)RAND_MAX); - } + ImPlot::SetupAxis(ImAxis_X1, "time", ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_NoLabel | ImPlotAxisFlags_NoGridLines); + ImPlot::SetupAxis(ImAxis_Y1, "val", ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_NoLabel | ImPlotAxisFlags_Opposite | ImPlotAxisFlags_NoTickLabels | ImPlotAxisFlags_NoTickMarks | ImPlotAxisFlags_AutoFit); - ImPlot::PlotScatter("Data 1", xs1, ys1, 100); - ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.25f); - ImPlot::SetNextMarkerStyle(ImPlotMarker_Square, 6, ImPlot::GetColormapColor(1), IMPLOT_AUTO, ImPlot::GetColormapColor(1)); - ImPlot::PlotScatter("Data 2", xs2, ys2, 50); - ImPlot::PopStyleVar(); + const f32 begin = view_left * static_cast(s.frame_max + 1), end = view_right * static_cast(s.frame_max + 1); + ImPlot::SetupAxisLimits(ImAxis_X1, begin, end, ImGuiCond_Always); + ImPlot::SetupFinish(); + static bool started_dragging = false, dragging = false; + if (started_dragging) { + dragging = true; + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + dragging = false; + } + u32 keys = 0; + for (const auto& n : selected_chains) { + PlugboardGraph::ConnectInfo i {n, 0}; // it's a chain -- 0 is out + auto& [chain, chain_copy] = *std::any_cast(&n->extra); + + if (started_dragging) + chain_copy = chain; + + auto key_loop_target = &chain; + if (dragging) { + chain.segments.clear(); + chain.selected.clear(); + key_loop_target = &chain_copy; + } + + for (u32 ii = 0; ii < key_loop_target->segments.size(); ii++) { + auto& [k, val, segment] = (key_loop_target->segments)[ii]; + ImGui::PushID(keys++); + + auto sel_it = std::find(key_loop_target->selected.begin(), key_loop_target->selected.end(), ii); + bool is_sel = sel_it != key_loop_target->selected.end(); + bool is_nxt_sel = ii + 1 < key_loop_target->segments.size() && std::find(key_loop_target->selected.begin(), key_loop_target->selected.end(), ii + 1) != key_loop_target->selected.end(); + + i32 frame = k; + f64 v = val; + i32 drag_off_x; // data coords + f64 drag_off_y; // data coords + ImPlotPoint mouse_pos = ImPlot::PixelsToPlot(io.MousePos), + mouse_clicked_pos = ImPlot::PixelsToPlot(io.MouseClickedPos[ImGuiMouseButton_Left]); + drag_off_x = static_cast(std::round(mouse_pos.x - mouse_clicked_pos.x)); + drag_off_y = mouse_pos.y - mouse_clicked_pos.y; + if (dragging && is_sel) { + frame += drag_off_x; + v += drag_off_y; + } + + f64 x = frame, y = v; + ImVec2 k_pos = ImPlot::PlotToPixels(x, y); + + ImPlot::Annotation(x, y, {1.0f, 1.0f, 1.0f, 1.0f}, {15.0f, 0.0f}, true); + ImGui::SetCursorScreenPos(k_pos - ImVec2{10.0f / 2.0f, 10.0f / 2.0f}); + + ImGui::PushStyleColor(ImGuiCol_Button, is_sel ? 0xFFFFFF88 : 0xFFAA7777); + ImGui::Button("##k", {10.0f, 10.0f}); + ImGui::PopStyleColor(); + + kf_drag_btn_handler(key_loop_target, dragging, chain, frame, is_sel, + sel_it, segment, v, ii, started_dragging, k_pos); + + if (segment.interp == PlugboardNodes::K_I_CubicBezier && ii + 1 < key_loop_target->segments.size()) { + const auto& [k_nxt, val_nxt, _] = (key_loop_target->segments)[ii + 1]; + i32 frame_nxt = k_nxt; + f64 v_nxt = val_nxt; + if (dragging && is_nxt_sel) { + frame_nxt += drag_off_x; + v_nxt += drag_off_y; + } + + f64 p2x = std::lerp(frame, frame_nxt, segment.v[0]), p2y = std::lerp(v, v_nxt, segment.v[1]), + p3x = std::lerp(frame, frame_nxt, segment.v[2]), p3y = std::lerp(v, v_nxt, segment.v[3]); + + ImPlot::Annotation(p2x, p2y, {1.0f, 1.0f, 1.0f, 1.0f}, {15.0f, 0.0f}, true); + ImPlot::Annotation(p3x, p3y, {1.0f, 1.0f, 1.0f, 1.0f}, {15.0f, 0.0f}, true); + + static f32 x_tmp, y_tmp; + + ImGui::SetCursorScreenPos(ImPlot::PlotToPixels(p2x, p2y) - ImVec2{10.0f / 2.0f, 10.0f / 2.0f}); + ImGui::Button("##p2", {10.0f, 10.0f}); + if (ImGui::IsItemClicked()) { + x_tmp = segment.v[0]; + y_tmp = segment.v[1]; + } + if (ImGui::IsItemActive()) { + segment.v[0] = std::clamp(x_tmp + static_cast(drag_off_x) / (static_cast(frame_nxt) - static_cast(frame)), 0.0f, 1.0f); + segment.v[1] = y_tmp + static_cast(drag_off_y / (v_nxt - v)); + } + + ImGui::SetCursorScreenPos(ImPlot::PlotToPixels(p3x, p3y) - ImVec2{10.0f / 2.0f, 10.0f / 2.0f}); + ImGui::Button("##p3", {10.0f, 10.0f}); + if (ImGui::IsItemClicked()) { + x_tmp = segment.v[2]; + y_tmp = segment.v[3]; + } + if (ImGui::IsItemActive()) { + segment.v[2] = std::clamp(x_tmp + static_cast(drag_off_x) / (static_cast(frame_nxt) - static_cast(frame)), 0.0f, 1.0f); + segment.v[3] = y_tmp + static_cast(drag_off_y / (v_nxt - v)); + } + + f64 xs[2] = {static_cast(frame), p2x}, + ys[2] = {v, p2y}; + + ImPlot::PlotLine("cb tangent", xs, ys, 2); + xs[0] = p3x; xs[1] = static_cast(frame_nxt); + ys[0] = p3y; ys[1] = v_nxt; + ImPlot::PlotLine("cb tangent", xs, ys, 2); + } + + ImGui::PopID(); + } + + struct PlotInfo info{ + .begin = begin, + .end = end, + .samples = 100, + .s = s, + .connected_v = i + }; + ImPlot::PlotLineG("v", [](int idx, void *user_data) -> ImPlotPoint { + PlotInfo i = *(PlotInfo *) user_data; + f32 x = i.begin + (i.end - i.begin) * static_cast(idx) / static_cast(i.samples); + CompState ss = i.s; + ss.current_frame = static_cast(x); + bool good; + Vector info; + return {x, std::get::type>( + PlugboardGraph::ConvertValue(PlugboardGraph::Eval(ss, i.connected_v, &info), + PlugboardGraph::T_Float, good))}; + }, &info, info.samples); + } ImPlot::EndPlot(); + + if (started_dragging && dragging) + started_dragging = false; } ImPlot::PopStyleColor(3); ImPlot::PopStyleVar(2); + ImGui::SetCursorScreenPos(ImGui::GetCursorStartPos()); + draw_tl_bg_drag_area(ImGui::GetContentRegionAvail().y, ImGui::GetContentRegionAvail().x); } ImGui::EndChild(); ImGui::PopStyleColor(); @@ -1298,6 +1427,24 @@ namespace K::UI { {fr, tl_init_pos.y + row_height}, {fr, ImGui::GetWindowSize().y + tl_init_pos.y}, 0x773333FF, 1.0f); } + if (bg_drag_select_active) { + window->DrawList->AddRectFilled(io.MouseClickedPos[ImGuiMouseButton_Left], io.MousePos, 0x55AA5555); + window->DrawList->AddRect(io.MouseClickedPos[ImGuiMouseButton_Left], io.MousePos, 0xAAAA7777); + } + if (window->Rect().Contains(io.MousePos) && ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && mouse_tl_x >= 0 && mouse_tl_x < view_width && io.MouseWheel != 0.0f) { + f32 center = mouse_tl_x / view_width * view_amt + view_left; + f32 new_view_amt = std::clamp(view_amt - 0.05f * io.MouseWheel, 0.05f, 1.0f); + view_left = center - new_view_amt / 2.0f - (mouse_tl_x / view_width - .5f) * new_view_amt; + view_right = center + new_view_amt / 2.0f - (mouse_tl_x / view_width - .5f) * new_view_amt; + if (view_left < 0.0f) { + view_right -= view_left; + view_left = 0.0f; + } + else if (view_right > 1.0f) { + view_left -= (view_right - 1.0f); + view_right = 1.0f; + } + } } ImGui::EndChild(); @@ -1425,7 +1572,7 @@ namespace K::UI { static PlugboardNodes::K_Interpolation type_current = PlugboardNodes::K_I_CubicBezier; static const ImVec2 p1 { 0.0f, 0.0f}, p4 {1.0f, 1.0f}; - static ImVec2 p2 { 1.0f, 0.0f }, p3 { 0.0f, 1.0f }, p2_old = p2, p3_old = p3; + static ImVec2 p2{1.0f, 0.0f}, p3{0.0f, 1.0f}, p2_old = p2, p3_old = p3; if (ImGui::Button("Apply")) for (auto& layer : s.layers) @@ -1517,10 +1664,24 @@ namespace K::UI { ImPlot::SetupAxis(ImAxis_Y1, "val", ImPlotAxisFlags_Opposite | ImPlotAxisFlags_NoLabel | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_AutoFit); ImPlot::SetupFinish(); constexpr static u32 samples = 100; + + if (type_current == PlugboardNodes::K_I_CubicBezier) { + f64 p2x = p2.x, p2y = p2.y, p3x = p3.x, p3y= p3.y; + if (ImPlot::DragPoint(0, &p2x, &p2y, {1.0f, 1.0f, 1.0f, 1.0f})) { + p2.x = static_cast(std::clamp(p2x, 0.0, 1.0)); + p2.y = static_cast(p2y); + } + if (ImPlot::DragPoint(1, &p3x, &p3y, {1.0f, 1.0f, 1.0f, 1.0f})) { + p3.x = static_cast(std::clamp(p3x, 0.0, 1.0)); + p3.y = static_cast(p3y); + } + } + ImPlot::PlotLineG("v", [](i32 idx, void* user_data) { f32 t = static_cast(idx) / static_cast(samples); - return ImPlotPoint{t, PlugboardNodes::EvalInterpolation(t, {.interp = type_current})}; + return ImPlotPoint{t, PlugboardNodes::EvalInterpolation(t, {.interp = type_current, .v = {p2.x,p2.y, p3.x, p3.y}})}; }, nullptr, samples); + ImPlot::EndPlot(); } ImPlot::PopStyleColor(3); diff --git a/Keishiki/include/PlugboardNodes.h b/Keishiki/include/PlugboardNodes.h index 068e47e..b704095 100644 --- a/Keishiki/include/PlugboardNodes.h +++ b/Keishiki/include/PlugboardNodes.h @@ -99,7 +99,7 @@ namespace K::PlugboardNodes { struct InterpolationExtra { K_Interpolation interp; - f32 v[4]; + f32 v[4]; // currently used only for bezier tangent points -- between [0,1]^2 fitted to segment }; constexpr f32 EvalInterpolation(f32 x, const InterpolationExtra& extra) { diff --git a/TODO.md b/TODO.md index e9c5424..8af0362 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,6 @@ - copy & paste - Undo's - Once this is done we can move the plugboard loop detection/chain finding out of the eval loop -- Graph editor - Node groups - Simple export @@ -18,8 +17,7 @@ - Data models - Dump and read back state, (de)serialization!!! - Pre-compose/Layer Groups (jokes -- can be completely UI side) -- Resource - - Deletion +- Deleting layers/nodes/keys/etc upkeep - Motion blur - Text (idea: index-based evaluation in plugboard) - Shapes (idea: embed glisp :mmtroll:, need to inquire -- still a lot of friction for simple shapes if we don't also get the glisp gizmos)