From d11d15aa1e5b152862ffdc3ae6ca93b5ef8f8364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Fri, 30 Apr 2021 15:58:25 +0200 Subject: [PATCH] Rework of MMU segmentation gizmo to support more than three colors. --- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 381 +++++++++++++++++- .../GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 28 +- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 11 +- 3 files changed, 405 insertions(+), 15 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index cc5d672bb6..f069cd2f72 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -6,13 +6,14 @@ #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/BitmapCache.hpp" #include "libslic3r/PresetBundle.hpp" #include - namespace Slic3r::GUI { void GLGizmoMmuSegmentation::on_shutdown() @@ -34,6 +35,31 @@ bool GLGizmoMmuSegmentation::on_is_selectable() const && wxGetApp().get_mode() != comSimple && wxGetApp().extruders_cnt() > 1); } +static std::vector> get_extruders_colors() +{ + unsigned char rgb_color[3] = {}; + std::vector colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); + std::vector> colors_out(colors.size()); + for (const std::string &color : colors) { + Slic3r::GUI::BitmapCache::parse_color(color, rgb_color); + size_t color_idx = &color - &colors.front(); + colors_out[color_idx] = {rgb_color[0], rgb_color[1], rgb_color[2]}; + } + + return colors_out; +} + +static std::vector get_extruders_names() +{ + size_t extruders_count = wxGetApp().extruders_cnt(); + std::vector extruders_out; + extruders_out.reserve(extruders_count); + for (size_t extruder_idx = 1; extruder_idx <= extruders_count; ++extruder_idx) + extruders_out.emplace_back("Extruder " + std::to_string(extruder_idx)); + + return extruders_out; +} + bool GLGizmoMmuSegmentation::on_init() { // FIXME Lukas H.: Discuss and change shortcut @@ -54,6 +80,9 @@ bool GLGizmoMmuSegmentation::on_init() m_desc["sphere"] = _L("Sphere"); m_desc["seed_fill_angle"] = _L("Seed fill angle"); + m_extruders_names = get_extruders_names(); + m_extruders_colors = get_extruders_colors(); + return true; } @@ -72,6 +101,249 @@ void GLGizmoMmuSegmentation::render_painter_gizmo() const glsafe(::glDisable(GL_BLEND)); } +bool GLGizmoMmuSegmentation::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) +{ + if (action == SLAGizmoEventType::MouseWheelUp + || action == SLAGizmoEventType::MouseWheelDown) { + if (control_down) { + double pos = m_c->object_clipper()->get_position(); + pos = action == SLAGizmoEventType::MouseWheelDown + ? std::max(0., pos - 0.01) + : std::min(1., pos + 0.01); + m_c->object_clipper()->set_position(pos, true); + return true; + } + else if (alt_down) { + m_cursor_radius = action == SLAGizmoEventType::MouseWheelDown + ? std::max(m_cursor_radius - CursorRadiusStep, CursorRadiusMin) + : std::min(m_cursor_radius + CursorRadiusStep, CursorRadiusMax); + m_parent.set_as_dirty(); + return true; + } + } + + if (action == SLAGizmoEventType::ResetClippingPlane) { + m_c->object_clipper()->set_position(-1., false); + return true; + } + + if (action == SLAGizmoEventType::LeftDown + || action == SLAGizmoEventType::RightDown + || (action == SLAGizmoEventType::Dragging && m_button_down != Button::None)) { + + if (m_triangle_selectors.empty()) + return false; + + EnforcerBlockerType new_state = EnforcerBlockerType::NONE; + if (! shift_down) { + if (action == SLAGizmoEventType::Dragging) + new_state = m_button_down == Button::Left + ? EnforcerBlockerType(m_first_selected_extruder_idx) + : EnforcerBlockerType(m_second_selected_extruder_idx); + else + new_state = action == SLAGizmoEventType::LeftDown + ? EnforcerBlockerType(m_first_selected_extruder_idx) + : EnforcerBlockerType(m_second_selected_extruder_idx); + } + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Selection& selection = m_parent.get_selection(); + const ModelObject* mo = m_c->selection_info()->model_object(); + const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + const Transform3d& instance_trafo = mi->get_transformation().get_matrix(); + + // List of mouse positions that will be used as seeds for painting. + std::vector mouse_positions{mouse_position}; + + // In case current mouse position is far from the last one, + // add several positions from between into the list, so there + // are no gaps in the painted region. + { + if (m_last_mouse_click == Vec2d::Zero()) + m_last_mouse_click = mouse_position; + // resolution describes minimal distance limit using circle radius + // as a unit (e.g., 2 would mean the patches will be touching). + double resolution = 0.7; + double diameter_px = resolution * m_cursor_radius * camera.get_zoom(); + int patches_in_between = int(((mouse_position - m_last_mouse_click).norm() - diameter_px) / diameter_px); + if (patches_in_between > 0) { + Vec2d diff = (mouse_position - m_last_mouse_click)/(patches_in_between+1); + for (int i=1; i<=patches_in_between; ++i) + mouse_positions.emplace_back(m_last_mouse_click + i*diff); + } + } + m_last_mouse_click = Vec2d::Zero(); // only actual hits should be saved + + // Precalculate transformations of individual meshes. + std::vector trafo_matrices; + for (const ModelVolume* mv : mo->volumes) { + if (mv->is_model_part()) + trafo_matrices.emplace_back(instance_trafo * mv->get_matrix()); + } + + // Now "click" into all the prepared points and spill paint around them. + for (const Vec2d& mp : mouse_positions) { + update_raycast_cache(mp, camera, trafo_matrices); + + bool dragging_while_painting = (action == SLAGizmoEventType::Dragging && m_button_down != Button::None); + + // The mouse button click detection is enabled when there is a valid hit. + // Missing the object entirely + // shall not capture the mouse. + if (m_rr.mesh_id != -1) { + if (m_button_down == Button::None) + m_button_down = ((action == SLAGizmoEventType::LeftDown) ? Button::Left : Button::Right); + } + + if (m_rr.mesh_id == -1) { + // In case we have no valid hit, we can return. The event will be stopped when + // dragging while painting (to prevent scene rotations and moving the object) + return dragging_while_painting; + } + + const Transform3d& trafo_matrix = trafo_matrices[m_rr.mesh_id]; + + // Calculate direction from camera to the hit (in mesh coords): + Vec3f camera_pos = (trafo_matrix.inverse() * camera.get_position()).cast(); + + assert(m_rr.mesh_id < int(m_triangle_selectors.size())); + if (m_seed_fill_enabled) + m_triangle_selectors[m_rr.mesh_id]->seed_fill_apply_on_triangles(new_state); + else + m_triangle_selectors[m_rr.mesh_id]->select_patch(m_rr.hit, m_rr.facet, camera_pos, m_cursor_radius, m_cursor_type, + new_state, trafo_matrix, m_triangle_splitting_enabled); + m_last_mouse_click = mouse_position; + } + + return true; + } + + if (action == SLAGizmoEventType::Moving && m_seed_fill_enabled) { + if (m_triangle_selectors.empty()) + return false; + + const Camera & camera = wxGetApp().plater()->get_camera(); + const Selection & selection = m_parent.get_selection(); + const ModelObject * mo = m_c->selection_info()->model_object(); + const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; + const Transform3d & instance_trafo = mi->get_transformation().get_matrix(); + + // Precalculate transformations of individual meshes. + std::vector trafo_matrices; + for (const ModelVolume *mv : mo->volumes) + if (mv->is_model_part()) + trafo_matrices.emplace_back(instance_trafo * mv->get_matrix()); + + // Now "click" into all the prepared points and spill paint around them. + update_raycast_cache(mouse_position, camera, trafo_matrices); + + if (m_rr.mesh_id == -1) { + // Clean selected by seed fill for all triangles + for (auto &triangle_selector : m_triangle_selectors) + triangle_selector->seed_fill_unselect_all_triangles(); + + // In case we have no valid hit, we can return. + return false; + } + + assert(m_rr.mesh_id < int(m_triangle_selectors.size())); + m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, m_rr.facet, m_seed_fill_angle); + return true; + } + + if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::RightUp) + && m_button_down != Button::None) { + // Take snapshot and update ModelVolume data. + wxString action_name; + if (get_painter_type() == PainterGizmoType::FDM_SUPPORTS) { + if (shift_down) + action_name = _L("Remove selection"); + else { + if (m_button_down == Button::Left) + action_name = _L("Add supports"); + else + action_name = _L("Block supports"); + } + } + if (get_painter_type() == PainterGizmoType::SEAM) { + if (shift_down) + action_name = _L("Remove selection"); + else { + if (m_button_down == Button::Left) + action_name = _L("Enforce seam"); + else + action_name = _L("Block seam"); + } + } + + activate_internal_undo_redo_stack(true); + Plater::TakeSnapshot(wxGetApp().plater(), action_name); + update_model_object(); + + m_button_down = Button::None; + m_last_mouse_click = Vec2d::Zero(); + return true; + } + + return false; +} + +static void render_extruders_combo(const std::string &label, + const std::vector &extruders, + const std::vector> &extruders_colors, + size_t &selection_idx) +{ + assert(!extruders_colors.empty()); + assert(extruders_colors.size() == extruders_colors.size()); + + size_t selection_out = selection_idx; + + // It is necessary to use BeginGroup(). Otherwise, when using SameLine() is called, then other items will be drawn inside the combobox. + ImGui::BeginGroup(); + ImVec2 combo_pos = ImGui::GetCursorScreenPos(); + if (ImGui::BeginCombo(label.c_str(), "")) { + for (size_t extruder_idx = 0; extruder_idx < extruders.size(); ++extruder_idx) { + ImGui::PushID(extruder_idx); + ImVec2 start_position = ImGui::GetCursorScreenPos(); + + if (ImGui::Selectable("", extruder_idx == selection_idx)) + selection_out = extruder_idx; + + ImGui::SameLine(); + ImGuiStyle &style = ImGui::GetStyle(); + float height = ImGui::GetTextLineHeight(); + ImGui::GetWindowDrawList()->AddRectFilled(start_position, ImVec2(start_position.x + height + height / 2, start_position.y + height), + IM_COL32(extruders_colors[extruder_idx][0], extruders_colors[extruder_idx][1], extruders_colors[extruder_idx][2], 255)); + ImGui::GetWindowDrawList()->AddRect(start_position, ImVec2(start_position.x + height + height / 2, start_position.y + height), IM_COL32_BLACK); + + ImGui::SetCursorScreenPos(ImVec2(start_position.x + height + height / 2 + style.FramePadding.x, start_position.y)); + ImGui::Text("%s", extruders[extruder_idx].c_str()); + ImGui::PopID(); + } + + ImGui::EndCombo(); + } + + ImVec2 backup_pos = ImGui::GetCursorScreenPos(); + ImGuiStyle &style = ImGui::GetStyle(); + + ImGui::SetCursorScreenPos(ImVec2(combo_pos.x + style.FramePadding.x, combo_pos.y + style.FramePadding.y)); + ImVec2 p = ImGui::GetCursorScreenPos(); + float height = ImGui::GetTextLineHeight(); + + ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + height + height / 2, p.y + height), + IM_COL32(extruders_colors[selection_idx][0], extruders_colors[selection_idx][1], + extruders_colors[selection_idx][2], 255)); + ImGui::GetWindowDrawList()->AddRect(p, ImVec2(p.x + height + height / 2, p.y + height), IM_COL32_BLACK); + + ImGui::SetCursorScreenPos(ImVec2(p.x + height + height / 2 + style.FramePadding.x, p.y)); + ImGui::Text("%s", extruders[selection_out].c_str()); + ImGui::SetCursorScreenPos(backup_pos); + ImGui::EndGroup(); + + selection_idx = selection_out; +} + void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bottom_limit) { if (!m_c->selection_info()->model_object()) @@ -94,6 +366,9 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float button_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); const float buttons_width = m_imgui->scaled(0.5f); const float minimal_slider_width = m_imgui->scaled(4.f); + const float color_button_width = m_imgui->calc_text_size("").x + m_imgui->scaled(1.75f); + const float combo_label_width = std::max(m_imgui->calc_text_size(m_desc.at("first_color")).x, + m_imgui->calc_text_size(m_desc.at("second_color")).x) + m_imgui->scaled(1.f); float caption_max = 0.f; float total_text_max = 0.; @@ -122,6 +397,30 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott m_imgui->text(""); ImGui::Separator(); + const std::array &select_first_color = m_extruders_colors[m_first_selected_extruder_idx]; + const std::array &select_second_color = m_extruders_colors[m_second_selected_extruder_idx]; + + m_imgui->text(m_desc.at("first_color")); + ImGui::SameLine(combo_label_width); + ImGui::PushItemWidth(window_width - combo_label_width - color_button_width); + render_extruders_combo("##first_color_combo", m_extruders_names, get_extruders_colors(), m_first_selected_extruder_idx); + ImGui::SameLine(); + + ImVec4 first_color = ImVec4(float(select_first_color[0]) / 255.0f, float(select_first_color[1]) / 255.0f, float(select_first_color[2]) / 255.0f, 1.0f); + ImVec4 second_color = ImVec4(float(select_second_color[0]) / 255.0f, float(select_second_color[1]) / 255.0f, float(select_second_color[2]) / 255.0f, 1.0f); + if(ImGui::ColorEdit4("First color##color_picker", (float*)&first_color, ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) + m_extruders_colors[m_first_selected_extruder_idx] = {uint8_t(first_color.x * 255.0f), uint8_t(first_color.y * 255.0f), uint8_t(first_color.z * 255.0f)}; + + m_imgui->text(m_desc.at("second_color")); + ImGui::SameLine(combo_label_width); + ImGui::PushItemWidth(window_width - combo_label_width - color_button_width); + render_extruders_combo("##second_color_combo", m_extruders_names, get_extruders_colors(), m_second_selected_extruder_idx); + ImGui::SameLine(); + if(ImGui::ColorEdit4("Second color##color_picker", (float*)&second_color, ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) + m_extruders_colors[m_second_selected_extruder_idx] = {uint8_t(second_color.x * 255.0f), uint8_t(second_color.y * 255.0f), uint8_t(second_color.z * 255.0f)}; + + ImGui::Separator(); + if (m_imgui->checkbox(_L("Seed fill"), m_seed_fill_enabled)) if (!m_seed_fill_enabled) for (auto &triangle_selector : m_triangle_selectors) @@ -134,12 +433,9 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGui::SameLine(autoset_slider_left); ImGui::PushItemWidth(window_width - autoset_slider_left); m_imgui->disabled_begin(!m_seed_fill_enabled); - m_imgui->slider_float("", &m_seed_fill_angle, 0.f, 90.f, format_str.data()); + m_imgui->slider_float("##seed_fill_angle", &m_seed_fill_angle, 0.f, 90.f, format_str.data()); m_imgui->disabled_end(); - ImGui::NewLine(); - ImGui::SameLine(window_width - 2.f * buttons_width - m_imgui->scaled(0.5f)); - ImGui::Separator(); if (m_imgui->button(m_desc.at("remove_all"))) { @@ -265,7 +561,7 @@ void GLGizmoMmuSegmentation::update_from_model_object() // This mesh does not account for the possible Z up SLA offset. const TriangleMesh* mesh = &mv->mesh(); - m_triangle_selectors.emplace_back(std::make_unique(*mesh)); + m_triangle_selectors.emplace_back(std::make_unique(*mesh, wxGetApp().extruders_cnt(), m_extruders_colors)); m_triangle_selectors.back()->deserialize(mv->mmu_segmentation_facets.get_data()); } } @@ -275,5 +571,78 @@ PainterGizmoType GLGizmoMmuSegmentation::get_painter_type() const return PainterGizmoType::MMU_SEGMENTATION; } +void TriangleSelectorMmuGui::render(ImGuiWrapper *imgui) +{ + std::vector color_cnt(m_iva_colors.size()); + int seed_fill_cnt = 0; + for (auto &iva_color : m_iva_colors) + iva_color.release_geometry(); + m_iva_seed_fill.release_geometry(); + + for (size_t color_idx = 0; color_idx < m_iva_colors.size(); ++color_idx) { + for (const Triangle &tr : m_triangles) { + if (!tr.valid || tr.is_split() || /*tr.get_state() == EnforcerBlockerType::NONE ||*/ tr.is_selected_by_seed_fill() || + tr.get_state() != EnforcerBlockerType(color_idx)) + continue; + + for (int i = 0; i < 3; ++i) + m_iva_colors[color_idx].push_geometry(double(m_vertices[tr.verts_idxs[i]].v[0]), + double(m_vertices[tr.verts_idxs[i]].v[1]), + double(m_vertices[tr.verts_idxs[i]].v[2]), + double(tr.normal[0]), + double(tr.normal[1]), + double(tr.normal[2])); + m_iva_colors[color_idx].push_triangle(color_cnt[color_idx], color_cnt[color_idx] + 1, color_cnt[color_idx] + 2); + color_cnt[color_idx] += 3; + } + } + + for (const Triangle &tr : m_triangles) { + if (!tr.valid || tr.is_split() || !tr.is_selected_by_seed_fill()) continue; + + for (int i = 0; i < 3; ++i) + m_iva_seed_fill.push_geometry(double(m_vertices[tr.verts_idxs[i]].v[0]), + double(m_vertices[tr.verts_idxs[i]].v[1]), + double(m_vertices[tr.verts_idxs[i]].v[2]), + double(tr.normal[0]), + double(tr.normal[1]), + double(tr.normal[2])); + m_iva_seed_fill.push_triangle(seed_fill_cnt, seed_fill_cnt + 1, seed_fill_cnt + 2); + seed_fill_cnt += 3; + } + + for (auto &iva_color : m_iva_colors) + iva_color.finalize_geometry(true); + m_iva_seed_fill.finalize_geometry(true); + + std::vector render_colors(m_iva_colors.size()); + for (size_t color_idx = 0; color_idx < m_iva_colors.size(); ++color_idx) + render_colors[color_idx] = m_iva_colors[color_idx].has_VBOs(); + bool render_seed_fill = m_iva_seed_fill.has_VBOs(); + + auto *shader = wxGetApp().get_shader("gouraud"); + if (!shader) return; + + shader->start_using(); + ScopeGuard guard([shader]() { + if (shader) + shader->stop_using(); + }); + shader->set_uniform("slope.actived", false); + + for (size_t color_idx = 0; color_idx < m_iva_colors.size(); ++color_idx) { + if (render_colors[color_idx]) { + std::array color = {m_colors[color_idx][0] / 255.0f, m_colors[color_idx][1] / 255.0f, m_colors[color_idx][2] / 255.0f, 1.f}; + shader->set_uniform("uniform_color", color); + m_iva_colors[color_idx].render(); + } + } + + if (render_seed_fill) { + std::array color = {0.f, 1.00f, 0.44f, 1.f}; + shader->set_uniform("uniform_color", color); + m_iva_seed_fill.render(); + } +} } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index b0bfdf65a2..0653fd007b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -3,9 +3,23 @@ #include "GLGizmoPainterBase.hpp" -namespace Slic3r { +namespace Slic3r::GUI { -namespace GUI { +class TriangleSelectorMmuGui : public TriangleSelectorGUI { +public: + explicit TriangleSelectorMmuGui(const TriangleMesh& mesh, size_t extruder_count, const std::vector> &colors) + : TriangleSelectorGUI(mesh), m_colors(colors) { + m_iva_colors = std::vector(extruder_count); + } + + // Render current selection. Transformation matrices are supposed + // to be already set. + virtual void render(ImGuiWrapper* imgui = nullptr); + +private: + const std::vector> &m_colors; + std::vector m_iva_colors; +}; class GLGizmoMmuSegmentation : public GLGizmoPainterBase { @@ -15,12 +29,19 @@ public: void render_painter_gizmo() const override; + virtual bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) override; + protected: void on_render_input_window(float x, float y, float bottom_limit) override; std::string on_get_name() const override; bool on_is_selectable() const override; + size_t m_first_selected_extruder_idx = 0; + size_t m_second_selected_extruder_idx = 1; + std::vector m_extruders_names; + std::vector> m_extruders_colors; + private: bool on_init() override; @@ -36,9 +57,6 @@ private: std::map m_desc; }; - - -} // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 214ebed592..a7600084bc 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -10,7 +10,7 @@ #include - +class GLGizmoMmuSegmentation; namespace Slic3r { @@ -37,7 +37,7 @@ public: // Render current selection. Transformation matrices are supposed // to be already set. - void render(ImGuiWrapper* imgui = nullptr); + virtual void render(ImGuiWrapper* imgui = nullptr); #ifdef PRUSASLICER_TRIANGLE_SELECTOR_DEBUG void render_debug(ImGuiWrapper* imgui); @@ -48,8 +48,9 @@ public: private: GLIndexedVertexArray m_iva_enforcers; GLIndexedVertexArray m_iva_blockers; - GLIndexedVertexArray m_iva_seed_fill; std::array m_varrays; +protected: + GLIndexedVertexArray m_iva_seed_fill; }; @@ -68,7 +69,7 @@ public: GLGizmoPainterBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); ~GLGizmoPainterBase() override {} void set_painter_gizmo_data(const Selection& selection); - bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); + virtual bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); // Following function renders the triangles and cursor. Having this separated // from usual on_render method allows to render them before transparent objects, @@ -146,6 +147,8 @@ protected: void on_load(cereal::BinaryInputArchive& ar) override; void on_save(cereal::BinaryOutputArchive& ar) const override {} CommonGizmosDataID on_get_requirements() const override; + + friend class GLGizmoMmuSegmentation; };