From 9541e2d05e4bd234e954b08b879773cd7f7bd639 Mon Sep 17 00:00:00 2001 From: "zhou.xu" Date: Wed, 24 Jul 2024 14:48:05 +0800 Subject: [PATCH] NEW:add interactive function after importing SVG jira:STUDIO-7406 most of code is from PrusaSlicer and OrcaSlicer,thanks for Filip Sykala - NTB T15p(PrusaSlicer) and Noisyfox(OrcaSlicer) Port Emboss & SVG gizmo from PrusaSlicer (#2819) * Rework UI jobs to make them more understandable and flexible. Change-Id: I765c7658b0881869754bdb161d720e4cbb180c92 (cherry picked from commit 3cef4611793899fa0ac39cb4d3a3abed7270a8e9) --- resources/images/add_text_modifier.svg | 1 + resources/images/add_text_negative.svg | 1 + resources/images/add_text_part.svg | 1 + resources/images/menu_obj_svg.svg | 1 + resources/images/menu_obj_text.svg | 1 + resources/images/svg_modifier.svg | 1 + resources/images/svg_negative.svg | 1 + resources/images/svg_part.svg | 1 + resources/images/text_bake.svg | 1 + resources/images/text_lock_closed.svg | 1 + resources/images/text_lock_open.svg | 1 + resources/images/text_obj_warning.svg | 1 + resources/images/text_open.svg | 1 + resources/images/text_reflection_x.svg | 1 + resources/images/text_reflection_y.svg | 1 + resources/images/text_refresh.svg | 1 + resources/images/text_save.svg | 1 + resources/images/text_undo.svg | 1 + src/imgui/CMakeLists.txt | 2 + src/imgui/imgui_draw.cpp | 2 +- src/imgui/imgui_stdlib.cpp | 76 + src/imgui/imgui_stdlib.h | 22 + src/imgui/imgui_widgets.cpp | 7 +- src/imgui/imstb_truetype.h | 116 +- src/libslic3r/AppConfig.hpp | 1 + src/libslic3r/BoundingBox.hpp | 20 +- src/libslic3r/CMakeLists.txt | 20 +- src/libslic3r/ClipperUtils.cpp | 78 +- src/libslic3r/ClipperUtils.hpp | 73 +- src/libslic3r/CutSurface.cpp | 4082 +++++++++++++++++ src/libslic3r/CutSurface.hpp | 74 + src/libslic3r/Emboss.cpp | 2181 +++++++++ src/libslic3r/Emboss.hpp | 473 ++ src/libslic3r/EmbossShape.hpp | 143 + src/libslic3r/ExPolygon.cpp | 1053 ++--- src/libslic3r/ExPolygon.hpp | 57 +- src/libslic3r/ExPolygonSerialize.hpp | 28 + src/libslic3r/ExPolygonsIndex.cpp | 82 + src/libslic3r/ExPolygonsIndex.hpp | 74 + src/libslic3r/Format/bbs_3mf.cpp | 288 +- src/libslic3r/Geometry.hpp | 10 + src/libslic3r/IntersectionPoints.cpp | 45 + src/libslic3r/IntersectionPoints.hpp | 23 + src/libslic3r/Model.cpp | 18 +- src/libslic3r/Model.hpp | 21 +- src/libslic3r/NSVGUtils.cpp | 543 +++ src/libslic3r/NSVGUtils.hpp | 82 + src/libslic3r/Point.cpp | 39 +- src/libslic3r/Point.hpp | 72 +- src/libslic3r/Polygon.cpp | 48 +- src/libslic3r/Polygon.hpp | 46 +- src/libslic3r/Polyline.cpp | 36 +- src/libslic3r/Polyline.hpp | 20 +- src/libslic3r/TextConfiguration.hpp | 187 + src/libslic3r/Timer.cpp | 21 + src/libslic3r/Timer.hpp | 92 + src/libslic3r/Triangulation.cpp | 329 ++ src/libslic3r/Triangulation.hpp | 72 + src/libslic3r/Utils.hpp | 1 + src/libslic3r/utils.cpp | 28 + src/nanosvg/nanosvg.h | 315 +- src/nanosvg/nanosvgrast.h | 111 +- src/slic3r/CMakeLists.txt | 17 + src/slic3r/GUI/GLCanvas3D.cpp | 86 +- src/slic3r/GUI/GLCanvas3D.hpp | 10 + src/slic3r/GUI/GUI_Factories.cpp | 121 +- src/slic3r/GUI/GUI_Factories.hpp | 12 +- src/slic3r/GUI/GUI_ObjectList.cpp | 46 +- src/slic3r/GUI/Gizmos/GLGizmoBase.cpp | 77 + src/slic3r/GUI/Gizmos/GLGizmoBase.hpp | 20 +- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp | 2168 +++++++++ src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp | 166 + src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 21 +- src/slic3r/GUI/Gizmos/GLGizmosManager.hpp | 2 + .../GUI/Gizmos/GizmoObjectManipulation.cpp | 3 +- src/slic3r/GUI/IconManager.cpp | 413 ++ src/slic3r/GUI/IconManager.hpp | 134 + src/slic3r/GUI/ImGuiWrapper.cpp | 176 +- src/slic3r/GUI/ImGuiWrapper.hpp | 76 +- src/slic3r/GUI/Jobs/BoostThreadWorker.cpp | 182 + src/slic3r/GUI/Jobs/BoostThreadWorker.hpp | 155 + src/slic3r/GUI/Jobs/BusyCursorJob.hpp | 54 + src/slic3r/GUI/Jobs/EmbossJob.cpp | 1153 +++++ src/slic3r/GUI/Jobs/EmbossJob.hpp | 351 ++ src/slic3r/GUI/Jobs/JobNew.hpp | 68 + .../Jobs/NotificationProgressIndicator.cpp | 5 +- .../Jobs/NotificationProgressIndicator.hpp | 1 + src/slic3r/GUI/Jobs/PlaterWorker.hpp | 155 + src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp | 124 + src/slic3r/GUI/Jobs/Worker.hpp | 140 + src/slic3r/GUI/MainFrame.cpp | 1 + src/slic3r/GUI/NotificationManager.cpp | 55 +- src/slic3r/GUI/NotificationManager.hpp | 8 +- src/slic3r/GUI/ObjectDataViewModel.cpp | 66 +- src/slic3r/GUI/ObjectDataViewModel.hpp | 13 +- src/slic3r/GUI/Plater.cpp | 183 +- src/slic3r/GUI/Plater.hpp | 6 + src/slic3r/GUI/Selection.cpp | 55 + src/slic3r/GUI/Selection.hpp | 8 +- src/slic3r/GUI/SurfaceDrag.cpp | 718 +++ src/slic3r/GUI/SurfaceDrag.hpp | 170 + src/slic3r/Utils/FontUtils.cpp | 10 +- src/slic3r/Utils/RaycastManager.cpp | 391 ++ src/slic3r/Utils/RaycastManager.hpp | 188 + 105 files changed, 17947 insertions(+), 992 deletions(-) create mode 100644 resources/images/add_text_modifier.svg create mode 100644 resources/images/add_text_negative.svg create mode 100644 resources/images/add_text_part.svg create mode 100644 resources/images/menu_obj_svg.svg create mode 100644 resources/images/menu_obj_text.svg create mode 100644 resources/images/svg_modifier.svg create mode 100644 resources/images/svg_negative.svg create mode 100644 resources/images/svg_part.svg create mode 100644 resources/images/text_bake.svg create mode 100644 resources/images/text_lock_closed.svg create mode 100644 resources/images/text_lock_open.svg create mode 100644 resources/images/text_obj_warning.svg create mode 100644 resources/images/text_open.svg create mode 100644 resources/images/text_reflection_x.svg create mode 100644 resources/images/text_reflection_y.svg create mode 100644 resources/images/text_refresh.svg create mode 100644 resources/images/text_save.svg create mode 100644 resources/images/text_undo.svg create mode 100644 src/imgui/imgui_stdlib.cpp create mode 100644 src/imgui/imgui_stdlib.h create mode 100644 src/libslic3r/CutSurface.cpp create mode 100644 src/libslic3r/CutSurface.hpp create mode 100644 src/libslic3r/Emboss.cpp create mode 100644 src/libslic3r/Emboss.hpp create mode 100644 src/libslic3r/EmbossShape.hpp create mode 100644 src/libslic3r/ExPolygonSerialize.hpp create mode 100644 src/libslic3r/ExPolygonsIndex.cpp create mode 100644 src/libslic3r/ExPolygonsIndex.hpp create mode 100644 src/libslic3r/IntersectionPoints.cpp create mode 100644 src/libslic3r/IntersectionPoints.hpp create mode 100644 src/libslic3r/NSVGUtils.cpp create mode 100644 src/libslic3r/NSVGUtils.hpp create mode 100644 src/libslic3r/TextConfiguration.hpp create mode 100644 src/libslic3r/Timer.cpp create mode 100644 src/libslic3r/Timer.hpp create mode 100644 src/libslic3r/Triangulation.cpp create mode 100644 src/libslic3r/Triangulation.hpp create mode 100644 src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp create mode 100644 src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp create mode 100644 src/slic3r/GUI/IconManager.cpp create mode 100644 src/slic3r/GUI/IconManager.hpp create mode 100644 src/slic3r/GUI/Jobs/BoostThreadWorker.cpp create mode 100644 src/slic3r/GUI/Jobs/BoostThreadWorker.hpp create mode 100644 src/slic3r/GUI/Jobs/BusyCursorJob.hpp create mode 100644 src/slic3r/GUI/Jobs/EmbossJob.cpp create mode 100644 src/slic3r/GUI/Jobs/EmbossJob.hpp create mode 100644 src/slic3r/GUI/Jobs/JobNew.hpp create mode 100644 src/slic3r/GUI/Jobs/PlaterWorker.hpp create mode 100644 src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp create mode 100644 src/slic3r/GUI/Jobs/Worker.hpp create mode 100644 src/slic3r/GUI/SurfaceDrag.cpp create mode 100644 src/slic3r/GUI/SurfaceDrag.hpp create mode 100644 src/slic3r/Utils/RaycastManager.cpp create mode 100644 src/slic3r/Utils/RaycastManager.hpp diff --git a/resources/images/add_text_modifier.svg b/resources/images/add_text_modifier.svg new file mode 100644 index 000000000..efb21d925 --- /dev/null +++ b/resources/images/add_text_modifier.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/add_text_negative.svg b/resources/images/add_text_negative.svg new file mode 100644 index 000000000..a59209746 --- /dev/null +++ b/resources/images/add_text_negative.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/add_text_part.svg b/resources/images/add_text_part.svg new file mode 100644 index 000000000..d36ff970c --- /dev/null +++ b/resources/images/add_text_part.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/menu_obj_svg.svg b/resources/images/menu_obj_svg.svg new file mode 100644 index 000000000..039ac65d9 --- /dev/null +++ b/resources/images/menu_obj_svg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/menu_obj_text.svg b/resources/images/menu_obj_text.svg new file mode 100644 index 000000000..48661b30f --- /dev/null +++ b/resources/images/menu_obj_text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/svg_modifier.svg b/resources/images/svg_modifier.svg new file mode 100644 index 000000000..74548e964 --- /dev/null +++ b/resources/images/svg_modifier.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/svg_negative.svg b/resources/images/svg_negative.svg new file mode 100644 index 000000000..7845267e2 --- /dev/null +++ b/resources/images/svg_negative.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/svg_part.svg b/resources/images/svg_part.svg new file mode 100644 index 000000000..b75b85bd8 --- /dev/null +++ b/resources/images/svg_part.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_bake.svg b/resources/images/text_bake.svg new file mode 100644 index 000000000..4d89b3657 --- /dev/null +++ b/resources/images/text_bake.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_lock_closed.svg b/resources/images/text_lock_closed.svg new file mode 100644 index 000000000..ba2bf7b4a --- /dev/null +++ b/resources/images/text_lock_closed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_lock_open.svg b/resources/images/text_lock_open.svg new file mode 100644 index 000000000..12f420274 --- /dev/null +++ b/resources/images/text_lock_open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_obj_warning.svg b/resources/images/text_obj_warning.svg new file mode 100644 index 000000000..239e6b725 --- /dev/null +++ b/resources/images/text_obj_warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_open.svg b/resources/images/text_open.svg new file mode 100644 index 000000000..466eace4d --- /dev/null +++ b/resources/images/text_open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_reflection_x.svg b/resources/images/text_reflection_x.svg new file mode 100644 index 000000000..4a9401389 --- /dev/null +++ b/resources/images/text_reflection_x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_reflection_y.svg b/resources/images/text_reflection_y.svg new file mode 100644 index 000000000..55d357df6 --- /dev/null +++ b/resources/images/text_reflection_y.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_refresh.svg b/resources/images/text_refresh.svg new file mode 100644 index 000000000..b25d53312 --- /dev/null +++ b/resources/images/text_refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_save.svg b/resources/images/text_save.svg new file mode 100644 index 000000000..ba542737f --- /dev/null +++ b/resources/images/text_save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/text_undo.svg b/resources/images/text_undo.svg new file mode 100644 index 000000000..763a86ff7 --- /dev/null +++ b/resources/images/text_undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/imgui/CMakeLists.txt b/src/imgui/CMakeLists.txt index 213b3b830..592e78c8e 100644 --- a/src/imgui/CMakeLists.txt +++ b/src/imgui/CMakeLists.txt @@ -13,6 +13,8 @@ add_library(imgui STATIC imgui_demo.cpp imgui_draw.cpp imgui_widgets.cpp + imgui_stdlib.cpp + imgui_stdlib.h ) if(Boost_FOUND) diff --git a/src/imgui/imgui_draw.cpp b/src/imgui/imgui_draw.cpp index 913a551fa..619e41c69 100644 --- a/src/imgui/imgui_draw.cpp +++ b/src/imgui/imgui_draw.cpp @@ -2905,7 +2905,7 @@ static void UnpackAccumulativeOffsetsIntoRanges(int base_codepoint, const short* //------------------------------------------------------------------------- // [SECTION] ImFontAtlas glyph ranges helpers //------------------------------------------------------------------------- -const ImWchar* ImFontAtlas::GetGlyphRangesChineseSimplifiedCommon() // used in bold_font only +const ImWchar* ImFontAtlas::GetGlyphRangesChineseSimplifiedCommon() // used in bold_font only { // Store 2500 regularly used characters for Simplified Chinese. // Sourced from https://zh.wiktionary.org/wiki/%E9%99%84%E5%BD%95:%E7%8E%B0%E4%BB%A3%E6%B1%89%E8%AF%AD%E5%B8%B8%E7%94%A8%E5%AD%97%E8%A1%A8 diff --git a/src/imgui/imgui_stdlib.cpp b/src/imgui/imgui_stdlib.cpp new file mode 100644 index 000000000..cb1fe1743 --- /dev/null +++ b/src/imgui/imgui_stdlib.cpp @@ -0,0 +1,76 @@ +// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.) +// This is also an example of how you may wrap your own similar types. + +// Compatibility: +// - std::string support is only guaranteed to work from C++11. +// If you try to use it pre-C++11, please share your findings (w/ info about compiler/architecture) + +// Changelog: +// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string + +#include "imgui.h" +#include "imgui_stdlib.h" + +struct InputTextCallback_UserData +{ + std::string* Str; + ImGuiInputTextCallback ChainCallback; + void* ChainCallbackUserData; +}; + +static int InputTextCallback(ImGuiInputTextCallbackData* data) +{ + InputTextCallback_UserData* user_data = (InputTextCallback_UserData*)data->UserData; + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + // Resize string callback + // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. + std::string* str = user_data->Str; + IM_ASSERT(data->Buf == str->c_str()); + str->resize(data->BufTextLen); + data->Buf = (char*)str->c_str(); + } + else if (user_data->ChainCallback) + { + // Forward to user callback, if any + data->UserData = user_data->ChainCallbackUserData; + return user_data->ChainCallback(data); + } + return 0; +} + +bool ImGui::InputText(const char* label, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputText(label, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); +} + +bool ImGui::InputTextMultiline(const char* label, std::string* str, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextMultiline(label, (char*)str->c_str(), str->capacity() + 1, size, flags, InputTextCallback, &cb_user_data); +} + +bool ImGui::InputTextWithHint(const char* label, const char* hint, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextWithHint(label, hint, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); +} diff --git a/src/imgui/imgui_stdlib.h b/src/imgui/imgui_stdlib.h new file mode 100644 index 000000000..f860b0c78 --- /dev/null +++ b/src/imgui/imgui_stdlib.h @@ -0,0 +1,22 @@ +// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.) +// This is also an example of how you may wrap your own similar types. + +// Compatibility: +// - std::string support is only guaranteed to work from C++11. +// If you try to use it pre-C++11, please share your findings (w/ info about compiler/architecture) + +// Changelog: +// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string + +#pragma once + +#include + +namespace ImGui +{ + // ImGui::InputText() with std::string + // Because text input needs dynamic resizing, we need to setup a callback to grow the capacity + IMGUI_API bool InputText(const char* label, std::string* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextWithHint(const char* label, const char* hint, std::string* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); +} diff --git a/src/imgui/imgui_widgets.cpp b/src/imgui/imgui_widgets.cpp index be6b07664..4c418437c 100644 --- a/src/imgui/imgui_widgets.cpp +++ b/src/imgui/imgui_widgets.cpp @@ -3070,7 +3070,7 @@ bool ImGui::BBLDragFloat(const char *label, float *v, float v_speed, float v_min ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.00f, 0.68f, 0.26f, 0.00f)); ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.00f, 0.68f, 0.26f, 0.00f)); bool bbl_drag_scalar = BBLDragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags); - if (v_max > v_min + 0.001) { + if (v_max > v_min + 0.001) { *v = std::clamp(*v, v_min, v_max); } ImGui::PopStyleColor(3); @@ -4815,8 +4815,9 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), true, ImGuiWindowFlags_NoMove); - PopStyleVar(2); + PopStyleVar(3); PopStyleColor(); if (!child_visible) { @@ -5454,7 +5455,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Test if cursor is vertically visible if (cursor_offset.y - g.FontSize < scroll_y) scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); - else if (cursor_offset.y - inner_size.y >= scroll_y) + else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y) scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f; const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f); scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y); diff --git a/src/imgui/imstb_truetype.h b/src/imgui/imstb_truetype.h index c7a2d5a5e..c35d7456d 100644 --- a/src/imgui/imstb_truetype.h +++ b/src/imgui/imstb_truetype.h @@ -51,7 +51,7 @@ // Rob Loach Cort Stratton // Kenney Phillis Jr. github:oyvindjam // Brian Costabile github:vassvik -// +// // VERSION HISTORY // // 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() @@ -212,7 +212,7 @@ // // Advancing for the next character: // Call GlyphHMetrics, and compute 'current_point += SF * advance'. -// +// // // ADVANCED USAGE // @@ -257,7 +257,7 @@ // Curve tessellation 120 LOC \__ 550 LOC Bitmap creation // Bitmap management 100 LOC / // Baked bitmap interface 70 LOC / -// Font name matching & access 150 LOC ---- 150 +// Font name matching & access 150 LOC ---- 150 // C runtime library abstraction 60 LOC ---- 60 // // @@ -350,7 +350,7 @@ int main(int argc, char **argv) } return 0; } -#endif +#endif // // Output: // @@ -364,9 +364,9 @@ int main(int argc, char **argv) // :@@. M@M // @@@o@@@@ // :M@@V:@@. -// +// ////////////////////////////////////////////////////////////////////////////// -// +// // Complete program: print "Hello World!" banner, with bugs // #if 0 @@ -500,11 +500,11 @@ int main(int arg, char **argv) #ifndef __STB_INCLUDE_STB_TRUETYPE_H__ #define __STB_INCLUDE_STB_TRUETYPE_H__ -//#ifdef STBTT_STATIC -//#define STBTT_DEF static -//#else +#ifdef STBTT_STATIC +#define STBTT_DEF static +#else #define STBTT_DEF extern -//#endif +#endif #ifdef __cplusplus extern "C" { @@ -667,7 +667,7 @@ STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, cons // Calling these functions in sequence is roughly equivalent to calling // stbtt_PackFontRanges(). If you more control over the packing of multiple // fonts, or if you want to pack custom data into a font texture, take a look -// at the source to of stbtt_PackFontRanges() and create a custom version +// at the source to of stbtt_PackFontRanges() and create a custom version // using these functions, e.g. call GatherRects multiple times, // building up a single array of rects, then call PackRects once, // then call RenderIntoRects repeatedly. This may result in a @@ -975,7 +975,7 @@ STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, floa // and computing from that can allow drop-out prevention). // // The algorithm has not been optimized at all, so expect it to be slow -// if computing lots of characters or very large sizes. +// if computing lots of characters or very large sizes. @@ -1741,7 +1741,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s if (i != 0) num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); - // now start the new one + // now start the new one start_off = !(flags & 1); if (start_off) { // if we start off with an off-curve point, then when we need to find a point on the curve @@ -1794,7 +1794,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s int comp_num_verts = 0, i; stbtt_vertex *comp_verts = 0, *tmp = 0; float mtx[6] = {1,0,0,1,0,0}, m, n; - + flags = ttSHORT(comp); comp+=2; gidx = ttSHORT(comp); comp+=2; @@ -1824,7 +1824,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; } - + // Find transformation scales. m = (float) STBTT_sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); n = (float) STBTT_sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); @@ -2755,7 +2755,7 @@ static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, i float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); STBTT_assert(z != NULL); if (!z) return z; - + // round dx down to avoid overshooting if (dxdy < 0) z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); @@ -2833,7 +2833,7 @@ static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__ac } } } - + e = e->next; } } @@ -3563,7 +3563,7 @@ STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info { int ix0,iy0,ix1,iy1; stbtt__bitmap gbm; - stbtt_vertex *vertices; + stbtt_vertex *vertices; int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); if (scale_x == 0) scale_x = scale_y; @@ -3586,7 +3586,7 @@ STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info if (height) *height = gbm.h; if (xoff ) *xoff = ix0; if (yoff ) *yoff = iy0; - + if (gbm.w && gbm.h) { gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); if (gbm.pixels) { @@ -3597,7 +3597,7 @@ STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info } STBTT_free(vertices, info->userdata); return gbm.pixels; -} +} STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) { @@ -3609,7 +3609,7 @@ STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigne int ix0,iy0; stbtt_vertex *vertices; int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); - stbtt__bitmap gbm; + stbtt__bitmap gbm; stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); gbm.pixels = output; @@ -3631,7 +3631,7 @@ STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char * STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) { return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); -} +} STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint) { @@ -3646,7 +3646,7 @@ STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, uns STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) { return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); -} +} STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) { @@ -3771,7 +3771,7 @@ static void stbrp_init_target(stbrp_context *con, int pw, int ph, stbrp_node *no con->y = 0; con->bottom_y = 0; STBTT__NOTUSED(nodes); - STBTT__NOTUSED(num_nodes); + STBTT__NOTUSED(num_nodes); } static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rects) @@ -4156,7 +4156,7 @@ STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char n = 0; for (i=0; i < num_ranges; ++i) n += ranges[i].num_chars; - + rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); if (rects == NULL) return 0; @@ -4167,7 +4167,7 @@ STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); stbtt_PackFontRangesPackRects(spc, rects, n); - + return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); STBTT_free(rects, spc->user_allocator_context); @@ -4328,7 +4328,7 @@ static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex int x1 = (int) verts[i ].x, y1 = (int) verts[i ].y; if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; - if (x_inter < x) + if (x_inter < x) winding += (y0 < y1) ? 1 : -1; } } @@ -4354,7 +4354,7 @@ static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex y1 = (int)verts[i ].y; if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; - if (x_inter < x) + if (x_inter < x) winding += (y0 < y1) ? 1 : -1; } } else { @@ -4366,7 +4366,7 @@ static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex if (hits[1][0] < 0) winding += (hits[1][1] < 0 ? -1 : 1); } - } + } } } return winding; @@ -4447,7 +4447,7 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc // invert for y-downwards bitmaps scale_y = -scale_y; - + { int x,y,i,j; float *precompute; @@ -4596,7 +4596,7 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc STBTT_free(verts, info->userdata); } return data; -} +} STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) { @@ -4614,7 +4614,7 @@ STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata) // // check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string -static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, stbtt_int32 len1, stbtt_uint8 *s2, stbtt_int32 len2) +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, stbtt_int32 len1, stbtt_uint8 *s2, stbtt_int32 len2) { stbtt_int32 i=0; @@ -4653,7 +4653,7 @@ static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, s return i; } -static int stbtt_CompareUTF8toUTF16_bigendian_internal(char *s1, int len1, char *s2, int len2) +static int stbtt_CompareUTF8toUTF16_bigendian_internal(char *s1, int len1, char *s2, int len2) { return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((stbtt_uint8*) s1, len1, (stbtt_uint8*) s2, len2); } @@ -4782,7 +4782,7 @@ STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index) { - return stbtt_GetFontOffsetForIndex_internal((unsigned char *) data, index); + return stbtt_GetFontOffsetForIndex_internal((unsigned char *) data, index); } STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data) @@ -4875,38 +4875,38 @@ This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------ ALTERNATIVE A - MIT License Copyright (c) 2017 Sean Barrett -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ ALTERNATIVE B - Public Domain (www.unlicense.org) This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -software, either in source code form or as a compiled binary, for any purpose, +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors of this -software dedicate any and all copyright interest in the software to the public -domain. We make this dedication for the benefit of the public at large and to -the detriment of our heirs and successors. We intend this dedication to be an -overt act of relinquishment in perpetuity of all present and future rights to +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ */ diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index ae8eae2ca..d1dd4acba 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -80,6 +80,7 @@ public: { std::string value; this->get(section, key, value); return value; } std::string get(const std::string &key) const { std::string value; this->get("app", key, value); return value; } + bool get_bool(const std::string &key) const { return this->get(key) == "true" || this->get(key) == "1"; } void set(const std::string §ion, const std::string &key, const std::string &value) { #ifndef NDEBUG diff --git a/src/libslic3r/BoundingBox.hpp b/src/libslic3r/BoundingBox.hpp index 58cbdd688..22767622f 100644 --- a/src/libslic3r/BoundingBox.hpp +++ b/src/libslic3r/BoundingBox.hpp @@ -16,9 +16,9 @@ public: PointClass min; PointClass max; bool defined; - + BoundingBoxBase() : min(PointClass::Zero()), max(PointClass::Zero()), defined(false) {} - BoundingBoxBase(const PointClass &pmin, const PointClass &pmax) : + BoundingBoxBase(const PointClass &pmin, const PointClass &pmax) : min(pmin), max(pmax), defined(pmin(0) < pmax(0) && pmin(1) < pmax(1)) {} BoundingBoxBase(const PointClass &p1, const PointClass &p2, const PointClass &p3) : min(p1), max(p1), defined(false) { merge(p2); merge(p3); } @@ -27,7 +27,7 @@ public: template> BoundingBoxBase(It from, It to) { - construct(*this, from, to); + construct(*this, from, to); } BoundingBoxBase(const std::vector &points) @@ -107,8 +107,8 @@ class BoundingBox3Base : public BoundingBoxBase { public: BoundingBox3Base() : BoundingBoxBase() {} - BoundingBox3Base(const PointClass &pmin, const PointClass &pmax) : - BoundingBoxBase(pmin, pmax) + BoundingBox3Base(const PointClass &pmin, const PointClass &pmax) : + BoundingBoxBase(pmin, pmax) { if (pmin(2) >= pmax(2)) BoundingBoxBase::defined = false; } BoundingBox3Base(const PointClass &p1, const PointClass &p2, const PointClass &p3) : BoundingBoxBase(p1, p1) { merge(p2); merge(p3); } @@ -206,7 +206,7 @@ public: // Align the min corner to a grid of cell_size x cell_size cells, // to encompass the original bounding box. void align_to_grid(const coord_t cell_size); - + BoundingBox() : BoundingBoxBase() {} BoundingBox(const Point &pmin, const Point &pmax) : BoundingBoxBase(pmin, pmax) {} BoundingBox(const Points &points) : BoundingBoxBase(points) {} @@ -216,7 +216,9 @@ public: friend BoundingBox get_extents_rotated(const Points &points, double angle); }; -class BoundingBox3 : public BoundingBox3Base +using BoundingBoxes = std::vector; + +class BoundingBox3 : public BoundingBox3Base { public: BoundingBox3() : BoundingBox3Base() {} @@ -224,7 +226,7 @@ public: BoundingBox3(const Points3& points) : BoundingBox3Base(points) {} }; -class BoundingBoxf : public BoundingBoxBase +class BoundingBoxf : public BoundingBoxBase { public: BoundingBoxf() : BoundingBoxBase() {} @@ -232,7 +234,7 @@ public: BoundingBoxf(const std::vector &points) : BoundingBoxBase(points) {} }; -class BoundingBoxf3 : public BoundingBox3Base +class BoundingBoxf3 : public BoundingBox3Base { public: using BoundingBox3Base::BoundingBox3Base; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 82d011119..1751e25bd 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -60,9 +60,15 @@ set(lisbslic3r_sources EdgeGrid.hpp ElephantFootCompensation.cpp ElephantFootCompensation.hpp + Emboss.cpp + Emboss.hpp + EmbossShape.hpp enum_bitmask.hpp ExPolygon.cpp ExPolygon.hpp + ExPolygonSerialize.hpp + ExPolygonsIndex.cpp + ExPolygonsIndex.hpp Extruder.cpp Extruder.hpp ExtrusionEntity.cpp @@ -224,6 +230,8 @@ set(lisbslic3r_sources MutablePriorityQueue.hpp ObjectID.cpp ObjectID.hpp + NSVGUtils.cpp + NSVGUtils.hpp ParameterUtils.cpp ParameterUtils.hpp PerimeterGenerator.cpp @@ -316,6 +324,8 @@ set(lisbslic3r_sources Utils.hpp Time.cpp Time.hpp + Timer.cpp + Timer.hpp Thread.cpp Thread.hpp TriangleSelector.cpp @@ -426,7 +436,7 @@ if (APPLE) ) endif () -add_library(libslic3r STATIC ${lisbslic3r_sources} +add_library(libslic3r STATIC ${lisbslic3r_sources} "${CMAKE_CURRENT_BINARY_DIR}/libslic3r_version.h" ${OpenVDBUtils_SOURCES}) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${lisbslic3r_sources}) @@ -442,8 +452,12 @@ find_package(CGAL REQUIRED) find_package(OpenCV REQUIRED core) cmake_policy(POP) -add_library(libslic3r_cgal STATIC MeshBoolean.cpp MeshBoolean.hpp TryCatchSignal.hpp - TryCatchSignal.cpp) +add_library(libslic3r_cgal STATIC + CutSurface.hpp CutSurface.cpp + IntersectionPoints.hpp IntersectionPoints.cpp + MeshBoolean.hpp MeshBoolean.cpp + TryCatchSignal.hpp TryCatchSignal.cpp + Triangulation.hpp Triangulation.cpp) target_include_directories(libslic3r_cgal PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) # Reset compile options of libslic3r_cgal. Despite it being linked privately, CGAL options diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 2845f934e..6644ede98 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -130,7 +130,7 @@ template [[nodiscard]] std::vector clip_clipper_p [[nodiscard]] Points clip_clipper_polygon_with_subject_bbox(const Points &src, const BoundingBox &bbox) { return clip_clipper_polygon_with_subject_bbox_templ(src, bbox); } [[nodiscard]] ZPoints clip_clipper_polygon_with_subject_bbox(const ZPoints &src, const BoundingBox &bbox) { return clip_clipper_polygon_with_subject_bbox_templ(src, bbox); } -void clip_clipper_polygon_with_subject_bbox(const Polygon &src, const BoundingBox &bbox, Polygon &out) { +void clip_clipper_polygon_with_subject_bbox(const Polygon &src, const BoundingBox &bbox, Polygon &out) { clip_clipper_polygon_with_subject_bbox(src.points, bbox, out.points); } @@ -176,7 +176,7 @@ static ExPolygons PolyTreeToExPolygons(ClipperLib::PolyTree &&polytree) { struct Inner { static void PolyTreeToExPolygonsRecursive(ClipperLib::PolyNode &&polynode, ExPolygons *expolygons) - { + { size_t cnt = expolygons->size(); expolygons->resize(cnt + 1); (*expolygons)[cnt].contour.points = std::move(polynode.Contour); @@ -331,7 +331,7 @@ TResult clipper_do( { // Safety offset only allowed on intersection and difference. assert(do_safety_offset == ApplySafetyOffset::No || clipType != ClipperLib::ctUnion); - return do_safety_offset == ApplySafetyOffset::Yes ? + return do_safety_offset == ApplySafetyOffset::Yes ? clipper_do(clipType, std::forward(subject), safety_offset(std::forward(clip)), fillType) : clipper_do(clipType, std::forward(subject), std::forward(clip), fillType); } @@ -420,7 +420,20 @@ Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, Cli Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::PolylinesProvider(polylines), delta, joinType, miterLimit, end_type))); } -// returns number of expolygons collected (0 or 1). +Polygons contour_to_polygons(const Polygon &polygon, const float line_width, ClipperLib::JoinType join_type, double miter_limit) + { + assert(line_width > 1.f); + return to_polygons( + clipper_union(raw_offset(ClipperUtils::SinglePathProvider(polygon.points), line_width / 2, join_type, miter_limit, ClipperLib::etClosedLine))); + } +Polygons contour_to_polygons(const Polygons &polygons, const float line_width, ClipperLib::JoinType join_type, double miter_limit) + { + assert(line_width > 1.f); + return to_polygons( + clipper_union(raw_offset(ClipperUtils::PolygonsProvider(polygons), line_width / 2, join_type, miter_limit, ClipperLib::etClosedLine))); + } + + // returns number of expolygons collected (0 or 1). static int offset_expolygon_inner(const Slic3r::ExPolygon &expoly, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::Paths &out) { // 1) Offset the outer contour. @@ -468,8 +481,8 @@ static int offset_expolygon_inner(const Slic3r::ExPolygon &expoly, const float d // No hole remaining after an offset. Just copy the outer contour. append(out, std::move(contours)); } else if (delta < 0) { - // Negative offset. There is a chance, that the offsetted hole intersects the outer contour. - // Subtract the offsetted holes from the offsetted contours. + // Negative offset. There is a chance, that the offsetted hole intersects the outer contour. + // Subtract the offsetted holes from the offsetted contours. if (auto output = clipper_do(ClipperLib::ctDifference, contours, holes, ClipperLib::pftNonZero); ! output.empty()) { append(out, std::move(output)); } else { @@ -539,7 +552,7 @@ template static ClipperLib::PolyTree expolygons_offset_pt(const ExPolygonVector &expolygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) { auto [output, expolygons_collected] = expolygons_offset_raw(expolygons, delta, joinType, miterLimit); - // Unite the offsetted expolygons for both the + // Unite the offsetted expolygons for both the return clipper_union(output); } @@ -648,7 +661,7 @@ inline ClipperLib::PolyTree clipper_do_polytree( const ApplySafetyOffset do_safety_offset) { assert(do_safety_offset == ApplySafetyOffset::No || clipType != ClipperLib::ctUnion); - return do_safety_offset == ApplySafetyOffset::Yes ? + return do_safety_offset == ApplySafetyOffset::Yes ? clipper_do_polytree(clipType, std::forward(subject), safety_offset(std::forward(clip)), fillType) : clipper_do_polytree(clipType, std::forward(subject), std::forward(clip), fillType); } @@ -663,9 +676,9 @@ Slic3r::Polygons diff(const Slic3r::Polygon &subject, const Slic3r::Polygon &cli { return _clipper(ClipperLib::ctDifference, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } Slic3r::Polygons diff(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return _clipper(ClipperLib::ctDifference, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } -Slic3r::Polygons diff_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) +Slic3r::Polygons diff_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return diff(subject, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip, get_extents(subject).inflated(SCALED_EPSILON)), do_safety_offset); } -Slic3r::ExPolygons diff_clipped(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) +Slic3r::ExPolygons diff_clipped(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return diff_ex(subject, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip, get_extents(subject).inflated(SCALED_EPSILON)), do_safety_offset); } Slic3r::ExPolygons diff_clipped(const Slic3r::ExPolygons & subject, const Slic3r::ExPolygons & clip, ApplySafetyOffset do_safety_offset) { @@ -681,7 +694,7 @@ Slic3r::Polygons diff(const Slic3r::Surfaces &subject, const Slic3r::Polygons &c { return _clipper(ClipperLib::ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset) { return _clipper(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } -Slic3r::Polygons intersection_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) +Slic3r::Polygons intersection_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return intersection(subject, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip, get_extents(subject).inflated(SCALED_EPSILON)), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset) { return _clipper(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } @@ -801,7 +814,12 @@ Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, ClipperLib::PolyFil { return _clipper_ex(ClipperLib::ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); } Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject) { return PolyTreeToExPolygons(clipper_do_polytree(ClipperLib::ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ClipperLib::pftNonZero)); } -Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject) +Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &subject2) +{ + return PolyTreeToExPolygons( + clipper_do_polytree(ClipperLib::ctUnion, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(subject2), ClipperLib::pftNonZero)); +} + Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject) { return PolyTreeToExPolygons(clipper_do_polytree(ClipperLib::ctUnion, ClipperUtils::SurfacesProvider(subject), ClipperUtils::EmptyPathsProvider(), ClipperLib::pftNonZero)); } // BBS Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons& poly1, const Slic3r::ExPolygons& poly2, bool safety_offset_) @@ -843,14 +861,14 @@ static void _clipper_pl_recombine(Polylines &polylines) polylines.erase(polylines.begin() + j); --j; } else if (polylines[i].points.front() == polylines[j].points.front()) { - /* Since Clipper does not preserve orientation of polylines, + /* Since Clipper does not preserve orientation of polylines, also check the case when first point of i coincides with first point of j. */ polylines[j].reverse(); polylines[i].points.insert(polylines[i].points.begin(), polylines[j].points.begin(), polylines[j].points.end()-1); polylines.erase(polylines.begin() + j); --j; } else if (polylines[i].points.back() == polylines[j].points.back()) { - /* Since Clipper does not preserve orientation of polylines, + /* Since Clipper does not preserve orientation of polylines, also check the case when last point of i coincides with last point of j. */ polylines[j].reverse(); polylines[i].points.insert(polylines[i].points.end(), polylines[j].points.begin()+1, polylines[j].points.end()); @@ -919,10 +937,10 @@ Lines _clipper_ln(ClipperLib::ClipType clipType, const Lines &subject, const Pol polylines.reserve(subject.size()); for (const Line &line : subject) polylines.emplace_back(Polyline(line.a, line.b)); - + // perform operation polylines = _clipper_pl_open(clipType, ClipperUtils::PolylinesProvider(polylines), ClipperUtils::PolygonsProvider(clip)); - + // convert Polylines to Lines Lines retval; for (Polylines::const_iterator polyline = polylines.begin(); polyline != polylines.end(); ++polyline) @@ -949,7 +967,7 @@ ClipperLib::PolyNodes order_nodes(const ClipperLib::PolyNodes &nodes) // collect ordering points Points ordering_points; ordering_points.reserve(nodes.size()); - + for (const ClipperLib::PolyNode *node : nodes) ordering_points.emplace_back( Point(node->Contour.front().x(), node->Contour.front().y())); @@ -963,7 +981,7 @@ ClipperLib::PolyNodes order_nodes(const ClipperLib::PolyNodes &nodes) static void traverse_pt_noholes(const ClipperLib::PolyNodes &nodes, Polygons *out) { - foreach_node(nodes, [&out](const ClipperLib::PolyNode *node) + foreach_node(nodes, [&out](const ClipperLib::PolyNode *node) { traverse_pt_noholes(node->Childs, out); out->emplace_back(node->Contour); @@ -983,7 +1001,7 @@ static void traverse_pt_outside_in(ClipperLib::PolyNodes &&nodes, Polygons *retv //FIXME pass the last point to chain_clipper_polynodes? for (ClipperLib::PolyNode *node : chain_clipper_polynodes(ordering_points, nodes)) { retval->emplace_back(std::move(node->Contour)); - if (node->IsHole()) + if (node->IsHole()) // Orient a hole, which is clockwise oriented, to CCW. retval->back().reverse(); // traverse the next depth @@ -1010,7 +1028,7 @@ Polygons simplify_polygons(const Polygons &subject, bool preserve_collinear) } else { output = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(subject), ClipperLib::pftNonZero); } - + // convert into Slic3r polygons return to_polygons(std::move(output)); } @@ -1020,13 +1038,13 @@ ExPolygons simplify_polygons_ex(const Polygons &subject, bool preserve_collinear if (! preserve_collinear) return union_ex(simplify_polygons(subject, false)); - ClipperLib::PolyTree polytree; + ClipperLib::PolyTree polytree; ClipperLib::Clipper c; c.PreserveCollinear(true); c.StrictlySimple(true); c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); c.Execute(ClipperLib::ctUnion, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - + // convert into ExPolygons return PolyTreeToExPolygons(std::move(polytree)); } @@ -1039,7 +1057,7 @@ Polygons top_level_islands(const Slic3r::Polygons &polygons) // perform union clipper.AddPaths(ClipperUtils::PolygonsProvider(polygons), ClipperLib::ptSubject, true); ClipperLib::PolyTree polytree; - clipper.Execute(ClipperLib::ctUnion, polytree, ClipperLib::pftEvenOdd, ClipperLib::pftEvenOdd); + clipper.Execute(ClipperLib::ctUnion, polytree, ClipperLib::pftEvenOdd, ClipperLib::pftEvenOdd); // Convert only the top level islands to the output. Polygons out; out.reserve(polytree.ChildCount()); @@ -1050,7 +1068,7 @@ Polygons top_level_islands(const Slic3r::Polygons &polygons) // Outer offset shall not split the input contour into multiples. It is expected, that the solution will be non empty and it will contain just a single polygon. ClipperLib::Paths fix_after_outer_offset( - const ClipperLib::Path &input, + const ClipperLib::Path &input, // combination of default prameters to correspond to void ClipperOffset::Execute(Paths& solution, double delta) // to produce a CCW output contour from CCW input contour for a positive offset. ClipperLib::PolyFillType filltype, // = ClipperLib::pftPositive @@ -1068,7 +1086,7 @@ ClipperLib::Paths fix_after_outer_offset( // Inner offset may split the source contour into multiple contours, but one resulting contour shall not lie inside the other. ClipperLib::Paths fix_after_inner_offset( - const ClipperLib::Path &input, + const ClipperLib::Path &input, // combination of default prameters to correspond to void ClipperOffset::Execute(Paths& solution, double delta) // to produce a CCW output contour from CCW input contour for a negative offset. ClipperLib::PolyFillType filltype, // = ClipperLib::pftNegative @@ -1116,7 +1134,7 @@ ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::v // Clamp miter limit to 2. miter_limit = (miter_limit > 2.) ? 2. / (miter_limit * miter_limit) : 0.5; - + // perpenduclar vector auto perp = [](const Vec2d &v) -> Vec2d { return Vec2d(v.y(), - v.x()); }; @@ -1244,7 +1262,7 @@ Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector 0.); #endif /* NDEBUG */ @@ -1254,7 +1272,7 @@ Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector 0.); #endif /* NDEBUG */ @@ -1347,7 +1365,7 @@ for (const std::vector& ds : deltas) ExPolygons output; if (holes.empty()) { output.reserve(contours.size()); - for (ClipperLib::Path &path : contours) + for (ClipperLib::Path &path : contours) output.emplace_back(std::move(path)); } else { ClipperLib::Clipper clipper; @@ -1393,7 +1411,7 @@ ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vectorcontour.points : m_it_expolygon->holes[m_idx_contour - 1].points; } bool operator==(const iterator &rhs) const { return m_it_expolygon == rhs.m_it_expolygon && m_idx_contour == rhs.m_idx_contour; } bool operator!=(const iterator &rhs) const { return !(*this == rhs); } - iterator& operator++() { + iterator& operator++() { if (++ m_idx_contour == m_it_expolygon->holes.size() + 1) { ++ m_it_expolygon; m_idx_contour = 0; } return *this; } - const Points& operator++(int) { + const Points& operator++(int) { const Points &out = **this; ++ (*this); return out; @@ -244,14 +244,14 @@ namespace ClipperUtils { const Points& operator*() const { return (m_idx_contour == 0) ? m_it_surface->expolygon.contour.points : m_it_surface->expolygon.holes[m_idx_contour - 1].points; } bool operator==(const iterator &rhs) const { return m_it_surface == rhs.m_it_surface && m_idx_contour == rhs.m_idx_contour; } bool operator!=(const iterator &rhs) const { return !(*this == rhs); } - iterator& operator++() { + iterator& operator++() { if (++ m_idx_contour == m_it_surface->expolygon.holes.size() + 1) { ++ m_it_surface; m_idx_contour = 0; } return *this; } - const Points& operator++(int) { + const Points& operator++(int) { const Points &out = **this; ++ (*this); return out; @@ -285,14 +285,14 @@ namespace ClipperUtils { const Points& operator*() const { return (m_idx_contour == 0) ? (*m_it_surface)->expolygon.contour.points : (*m_it_surface)->expolygon.holes[m_idx_contour - 1].points; } bool operator==(const iterator &rhs) const { return m_it_surface == rhs.m_it_surface && m_idx_contour == rhs.m_idx_contour; } bool operator!=(const iterator &rhs) const { return !(*this == rhs); } - iterator& operator++() { + iterator& operator++() { if (++ m_idx_contour == (*m_it_surface)->expolygon.holes.size() + 1) { ++ m_it_surface; m_idx_contour = 0; } return *this; } - const Points& operator++(int) { + const Points& operator++(int) { const Points &out = **this; ++ (*this); return out; @@ -313,7 +313,7 @@ namespace ClipperUtils { size_t m_size; }; - + // For ClipperLib with Z coordinates. using ZPoint = Vec3i32; using ZPoints = std::vector; @@ -363,6 +363,10 @@ inline Slic3r::ExPolygons offset_ex(const Slic3r::Polygon &polygon, const float return offset_ex(temp, delta, joinType, miterLimit); } +// convert stroke to path by offsetting of contour +Polygons contour_to_polygons(const Polygon &polygon, const float line_width, ClipperLib::JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); +Polygons contour_to_polygons(const Polygons &polygon, const float line_width, ClipperLib::JoinType join_type = DefaultJoinType, double miter_limit = DefaultMiterLimit); + inline Slic3r::Polygons union_safety_offset (const Slic3r::Polygons &polygons) { return offset (polygons, ClipperSafetyOffset); } inline Slic3r::Polygons union_safety_offset (const Slic3r::ExPolygons &expolygons) { return offset (expolygons, ClipperSafetyOffset); } inline Slic3r::ExPolygons union_safety_offset_ex(const Slic3r::Polygons &polygons) { return offset_ex(polygons, ClipperSafetyOffset); } @@ -374,18 +378,18 @@ Slic3r::ExPolygons union_safety_offset_ex(const Slic3r::Polygons &polygons); Slic3r::ExPolygons union_safety_offset_ex(const Slic3r::ExPolygons &expolygons); // Aliases for the various offset(...) functions, conveying the purpose of the offset. -inline Slic3r::Polygons expand(const Slic3r::Polygon &polygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons expand(const Slic3r::Polygon &polygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset(polygon, delta, joinType, miterLimit); } -inline Slic3r::Polygons expand(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons expand(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset(polygons, delta, joinType, miterLimit); } -inline Slic3r::ExPolygons expand_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons expand_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset_ex(polygons, delta, joinType, miterLimit); } // Input polygons for shrinking shall be "normalized": There must be no overlap / intersections between the input polygons. -inline Slic3r::Polygons shrink(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons shrink(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset(polygons, -delta, joinType, miterLimit); } -inline Slic3r::ExPolygons shrink_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons shrink_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset_ex(polygons, -delta, joinType, miterLimit); } -inline Slic3r::ExPolygons shrink_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons shrink_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset_ex(polygons, -delta, joinType, miterLimit); } // Wherever applicable, please use the opening() / closing() variants instead, they convey their purpose better. // Input polygons for negative offset shall be "normalized": There must be no overlap / intersections between the input polygons. @@ -400,14 +404,14 @@ Slic3r::ExPolygons _clipper_ex(ClipperLib::ClipType clipType, // Offset outside, then inside produces morphological closing. All deltas should be positive. Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -inline Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons closing(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return closing(polygons, delta, delta, joinType, miterLimit); } Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -inline Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons closing_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return closing_ex(polygons, delta, delta, joinType, miterLimit); } -inline Slic3r::ExPolygons closing_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons closing_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset2_ex(polygons, delta, - delta, joinType, miterLimit); } -inline Slic3r::ExPolygons closing_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons closing_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset2_ex(surfaces, delta, - delta, joinType, miterLimit); } // Offset inside, then outside produces morphological opening. All deltas should be positive. @@ -415,15 +419,15 @@ inline Slic3r::ExPolygons closing_ex(const Slic3r::Surfaces &surfaces, const flo Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta1, const float delta2, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); -inline Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons opening(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return opening(polygons, delta, delta, joinType, miterLimit); } -inline Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons opening(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return opening(expolygons, delta, delta, joinType, miterLimit); } -inline Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::Polygons opening(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { return opening(surfaces, delta, delta, joinType, miterLimit); } -inline Slic3r::ExPolygons opening_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons opening_ex(const Slic3r::ExPolygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset2_ex(polygons, - delta, delta, joinType, miterLimit); } -inline Slic3r::ExPolygons opening_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) +inline Slic3r::ExPolygons opening_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit) { assert(delta > 0); return offset2_ex(surfaces, - delta, delta, joinType, miterLimit); } Slic3r::Lines _clipper_ln(ClipperLib::ClipType clipType, const Slic3r::Lines &subject, const Slic3r::Polygons &clip); @@ -549,6 +553,7 @@ Slic3r::Polygons union_(const Slic3r::Polygons &subject, const Slic3r::ExPolygon // May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative). Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, ClipperLib::PolyFillType fill_type = ClipperLib::pftNonZero); Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject); +Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &subject2); Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject); Slic3r::ExPolygons union_ex(const Slic3r::ExPolygons& poly1, const Slic3r::ExPolygons& poly2, bool safety_offset_ = false); @@ -578,7 +583,7 @@ template struct _foreach_node { template struct _foreach_node { void operator()(const ClipperLib::PolyNodes &nodes, Fn &&fn) { - for (auto &n : nodes) fn(n); + for (auto &n : nodes) fn(n); } }; @@ -587,7 +592,7 @@ template struct _foreach_node { void operator()(const ClipperLib::PolyNodes &nodes, Fn &&fn) { auto ordered_nodes = order_nodes(nodes); - for (auto &n : nodes) fn(n); + for (auto &n : nodes) fn(n); } }; @@ -604,10 +609,10 @@ template void traverse_pt(const ClipperLib::PolyNode *tree, Polygons *out) { if (!tree) return; // terminates recursion - + // Push the contour of the current level out->emplace_back(tree->Contour); - + // Do the recursion for all the children. traverse_pt(tree->Childs, out); } @@ -623,21 +628,21 @@ void traverse_pt(const ClipperLib::PolyNode *tree, ExPolygons *out) traverse_pt(tree->Childs, out); return; } - + ExPolygon level; level.contour.points = tree->Contour; - - foreach_node(tree->Childs, + + foreach_node(tree->Childs, [out, &level] (const ClipperLib::PolyNode *node) { - - // Holes are collected here. + + // Holes are collected here. level.holes.emplace_back(node->Contour); - + // By doing a recursion, a new level expoly is created with the contour // and holes of the lower level. Doing this for all the childs. traverse_pt(node->Childs, out); - }); - + }); + out->emplace_back(level); } diff --git a/src/libslic3r/CutSurface.cpp b/src/libslic3r/CutSurface.cpp new file mode 100644 index 000000000..1d74fcfc7 --- /dev/null +++ b/src/libslic3r/CutSurface.cpp @@ -0,0 +1,4082 @@ +#include "CutSurface.hpp" + +/// models_input.obj - Check transormation of model to each others +/// projection_center.obj - circle representing center of projection with correct distance +/// {M} .. model index +/// model/model{M}.off - CGAL model created from index_triangle_set +/// model_neg/model{M}.off - CGAL model created for differenciate (multi volume object) +/// shape.off - CGAL model created from shapes +/// constrained/model{M}.off - Visualization of inside and outside triangles +/// Green - not along constrained edge +/// Red - sure that are inside +/// Purple - sure that are outside +/// (only along constrained edge) +/// filled/model{M}.off - flood fill green triangles inside of red area +/// - Same meaning of color as constrained +/// {N} .. Order of cutted Area of Interestmodel from model surface +/// model_AOIs/{M}/cutAOI{N}.obj - Extracted Area of interest from corefined model +/// model_AOIs/{M}/outline{N}.obj - Outline of Cutted Area +/// {O} .. Order number of patch +/// patches/patch{O}.off +/// result.obj - Merged result its +/// result_contours/{O}.obj - visualization of contours for result patches +//#define DEBUG_OUTPUT_DIR std::string("C:/data/temp/cutSurface/") + +using namespace Slic3r; +#include "ExPolygonsIndex.hpp" +#include +#include +#include +#include +#include +#include + +// libslic3r +#include "TriangleMesh.hpp" // its_merge +#include "Utils.hpp" // next_highest_power_of_2 +#include "ClipperUtils.hpp" // union_ex + offset_ex + +namespace priv { + +using Project = Emboss::IProjection; +using Project3d = Emboss::IProject3d; + +/// +/// Set true for indices out of area of interest +/// +/// Flag to convert triangle to cgal +/// model +/// Convert 2d point to pair of 3d points +/// 2d bounding box define AOI +void set_skip_for_out_of_aoi(std::vector &skip_indicies, + const indexed_triangle_set &its, + const Project &projection, + const BoundingBox &shapes_bb); + +/// +/// Set true for indicies outward and almost parallel together. +/// Note: internally calculate normals +/// +/// Flag to convert triangle to cgal +/// model +/// Direction to measure angle +/// Maximal allowed angle between opposit normal and +/// projection direction [in DEG] +void set_skip_by_angle(std::vector &skip_indicies, + const indexed_triangle_set &its, + const Project3d &projection, + double max_angle = 89.); + + +using EpicKernel = CGAL::Exact_predicates_inexact_constructions_kernel; +using CutMesh = CGAL::Surface_mesh; +using CutMeshes = std::vector; + +using VI = CGAL::SM_Vertex_index; +using HI = CGAL::SM_Halfedge_index; +using EI = CGAL::SM_Edge_index; +using FI = CGAL::SM_Face_index; +using P3 = CGAL::Epick::Point_3; + +inline Vec3d to_vec3d(const P3 &p) { return Vec3d(p.x(),p.y(),p.z()); } + +/// +/// Convert triangle mesh model to CGAL Surface_mesh +/// Filtrate out opposite triangles +/// Add property map for source face index +/// +/// Model +/// Flags that triangle should be skiped +/// When true triangle will flip normal +/// CGAL mesh - half edge mesh +CutMesh to_cgal(const indexed_triangle_set &its, + const std::vector &skip_indicies, + bool flip = false); + +/// +/// Covert 2d shape (e.g. Glyph) to CGAL model +/// NOTE: internaly create +/// edge_shape_map .. Property map to store conversion from edge to contour +/// face_shape_map .. Property map to store conversion from face to contour +/// +/// 2d shapes to project +/// Define transformation 2d point into 3d +/// CGAL model of extruded shape +CutMesh to_cgal(const ExPolygons &shapes, const Project &projection); +// function to check result of projection. 2d int32_t -> 3d double +bool exist_duplicit_vertex(const CutMesh& mesh); + + +/// +/// IntersectingElement +/// +/// Adress polygon inside of ExPolygon +/// Keep information about source of vertex: +/// - from face (one of 2 possible) +/// - from edge (one of 2 possible) +/// +/// V1~~~~~V2 +/// | f1 /: +/// | / : +/// e1| /e2: +/// | / : +/// |/ f2 : +/// V1'~~~~V2' +/// +/// | .. edge +/// / .. edge +/// : .. foreign edge - neighbor +/// ~ .. no care edge - idealy should not cross model +/// V1,V1' .. projected 2d point to 3d +/// V2,V2' .. projected 2d point to 3d +/// +/// Vertex indexing +/// V1 .. i (vertex_base + 2x index of point in polygon) +/// V1' .. i + 1 +/// V2 .. j = i + 2 || 0 (for last i in polygon) +/// V2' .. j + 1 +/// +/// f1 .. text_face_1 (triangle face made by side of shape contour) +/// f2 .. text_face_2 +/// e1 .. text_edge_1 (edge on side of face made by side of shape contour) +/// e2 .. text_edge_2 +/// +/// +struct IntersectingElement +{ + // identify source point in shapes + uint32_t shape_point_index{std::numeric_limits::max()}; + + // store together type, is_first, is_last + unsigned char attr{std::numeric_limits::max()}; + + // vertex or edge ID, where edge ID is the index of the source point. + // There are 4 consecutive indices generated for a single contour edge: + // 0th - 1st text edge (straight) + // 1th - 1st text face + // 2nd - 2nd text edge (diagonal) + // 3th - 2nd text face + // Type of intersecting element from extruded shape( 3d ) + // NOTE: type must be storable to 3bit -> max value is 7 + enum class Type: unsigned char { + edge_1 = 0, + face_1 = 1, + edge_2 = 2, + face_2 = 3, + undefined = 4 + }; + + IntersectingElement &set_type(Type t) + { + attr = static_cast( + attr + (int) t - (int) get_type()); + return *this; + } + void set_is_first(){ attr += 8; } + void set_is_last(){ attr += 16; } + Type get_type() const { return static_cast(attr % 8);} + bool is_first() const { return 8 <= attr && attr < 16; } + bool is_last() const { return attr >= 16; } +}; + +// stored in model made by shape +using EdgeShapeMap = CutMesh::Property_map; +using FaceShapeMap = CutMesh::Property_map; + +// stored in surface source - pointer to EdgeShapeMap | FaceShapeMap +using VertexShapeMap = CutMesh::Property_map; + +// stored in model made by shape +const std::string edge_shape_map_name = "e:IntersectingElement"; +const std::string face_shape_map_name = "f:IntersectingElement"; + +// stored in surface source +const std::string vert_shape_map_name = "v:IntersectingElement"; + +/// +/// Flag for faces in CGAL mesh +/// +enum class FaceType { + // face inside of the cutted shape + inside, + // face outside of the cutted shape + outside, + // face without constrained edge (In or Out) + not_constrained, + + // Helper flag that inside was processed + inside_processed +}; +using FaceTypeMap = CutMesh::Property_map; +const std::string face_type_map_name = "f:side"; + +// Conversion one vertex index to another +using CvtVI2VI = CutMesh::Property_map; +// Each Patch track outline vertex conversion to tource model +const std::string patch_source_name = "v:patch_source"; + +// For VI that should be reduced, contain VI to use instead of reduced +// Other VI are invalid +using ReductionMap = CvtVI2VI; +const std::string vertex_reduction_map_name = "v:reduction"; + +// A property map containing the constrained-or-not status of each edge +using EdgeBoolMap = CutMesh::Property_map; +const std::string is_constrained_edge_name = "e:is_constrained"; + +/// +/// Create map to reduce unnecesary triangles, +/// Triangles are made by divided quad to two triangles +/// on side of cutting shape mesh +/// Note: also use from mesh (have to be created) +/// face_type_map .. Type of shape inside / outside +/// vert_shape_map .. Source of outline vertex +/// +/// Reduction map from vertex to vertex, +/// when key == value than no reduction +/// Faces of one +/// Input object +void create_reduce_map(ReductionMap &reduction_map, const CutMesh &meshes); + +// Patch made by Cut area of interest from model +// connected faces(triangles) and outlines(halfEdges) for one surface cut +using CutAOI = std::pair, std::vector>; +// vector of Cutted Area of interest cutted from one CGAL model +using CutAOIs = std::vector; +// vector of CutAOIs for each model +using VCutAOIs = std::vector; + +/// +/// Create AOIs(area of interest) on model surface +/// +/// Input model converted to CGAL +/// NOTE: will be extended by corefine edge +/// 2d contours +/// [const]Model made by shapes +/// NOTE: Can't be definde as const because of corefine function input definition, +/// but it is. +/// Wanted projection distance +/// Convert index to shape point from ExPolygons +/// Patches from model surface +CutAOIs cut_from_model(CutMesh &cgal_model, + const ExPolygons &shapes, + /*const*/ CutMesh &cgal_shape, + float projection_ratio, + const ExPolygonsIndices &s2i); + +using Loop = std::vector; +using Loops = std::vector; + +/// +/// Create closed loops of contour vertices created from open half edges +/// +/// Unsorted half edges +/// Source mesh for half edges +/// Closed loops +Loops create_loops(const std::vector &outlines, const CutMesh &mesh); + +// To track during diff_models, +// what was cutted off, from CutAOI +struct SurfacePatch +{ + // converted cut to CGAL mesh + // Mesh is reduced. + // (do not contain divided triangles on contour - created by side Quad) + CutMesh mesh; + // CvtVI2VI cvt = mesh.property_map(patch_source_name); + // Conversion VI from this patch to source VI(model) is stored in mesh property + + // Outlines - converted CutAOI.second (half edges) + // to loops (vertex indicies) by function create_loops + Loops loops; + + // bounding box of mesh + BoundingBoxf3 bb; + + //// Data needed to find best projection distances + // index of source model in models + size_t model_id; + // index of source CutAOI + size_t aoi_id; + // index of shape from ExPolygons + size_t shape_id = 0; + + // flag that this patch contain whole CutAOI + bool is_whole_aoi = true; +}; +using SurfacePatches = std::vector; + +struct ModelCutId +{ + // index of model + uint32_t model_index; + // index of cut inside model + uint32_t cut_index; +}; + +/// +/// Keep conversion from VCutAOIs to Index and vice versa +/// Model_index .. contour(or hole) poin from ExPolygons +/// Index .. continous number +/// +class ModelCut2index +{ + std::vector m_offsets; + // for check range of index + uint32_t m_count; + +public: + ModelCut2index(const VCutAOIs &cuts); + uint32_t calc_index(const ModelCutId &id) const; + ModelCutId calc_id(uint32_t index) const; + uint32_t get_count() const { return m_count; }; + const std::vector &get_offsets() const { return m_offsets; } +}; + +/// +/// Differenciate other models +/// +/// Patches from meshes +/// Source points for Cutted AOIs +/// NOTE: Create Reduction map as mesh property - clean on end +/// Original models without cut modifications +/// used for differenciation +/// NOTE: Clip function modify Mesh +/// Define projection direction +/// Cuts differenciate by models - Patch +SurfacePatches diff_models(VCutAOIs &cuts, + /*const*/ CutMeshes &cut_models, + /*const*/ CutMeshes &models, + const Project3d &projection); + +/// +/// Checking whether patch is uninterrupted cover of whole expolygon it belongs. +/// +/// Part of surface to check +/// Source shape +/// Source of cut +/// True when cover whole expolygon otherwise false +bool is_over_whole_expoly(const CutAOI &cutAOI, + const ExPolygon &shape, + const CutMesh &mesh); + +/// +/// Checking whether patch is uninterrupted cover of whole expolygon it belongs. +/// +/// Part of surface to check +/// Source shape +/// True when cover whole expolygon otherwise false +bool is_over_whole_expoly(const SurfacePatch &patch, + const ExPolygons &shapes, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes); +/// +/// Unptoject points from outline loops of patch +/// +/// Contain loops and vertices +/// Know how to project from 3d to 2d +/// Range of unprojected points x .. min, y .. max value +/// Unprojected points in loops +Polygons unproject_loops(const SurfacePatch &patch, const Project &projection, Vec2d &depth_range); + +/// +/// Unproject points from loops and create expolygons +/// +/// Patch to convert on expolygon +/// Convert 3d point to 2d +/// Range of unprojected points x .. min, y .. max value +/// Expolygon represent patch in 2d +ExPolygon to_expoly(const SurfacePatch &patch, const Project &projection, Vec2d &depth_range); + +/// +/// To select surface near projection distance +/// +struct ProjectionDistance +{ + // index of source model + uint32_t model_index = std::numeric_limits::max(); + + // index of CutAOI + uint32_t aoi_index = std::numeric_limits::max(); + + // index of Patch + uint32_t patch_index = std::numeric_limits::max(); + + // signed distance to projection + float distance = std::numeric_limits::max(); +}; +// addresed by ExPolygonsIndices +using ProjectionDistances = std::vector; + +// each point in shapes has its ProjectionDistances +using VDistances = std::vector; + +/// +/// Calculate distances for SurfacePatches outline points +/// NOTE: +/// each model has to have "vert_shape_map" .. Know source of new vertices +/// +/// Part of surface +/// Vertices position +/// Mesh created by shapes +/// Count of contour points in shapes +/// Define best distnace +/// Projection distances of cutted shape points +VDistances calc_distances(const SurfacePatches &patches, + const CutMeshes &models, + const CutMesh &shapes_mesh, + size_t count_shapes_points, + float projection_ratio); + +/// +/// Select distances in similar depth between expolygons +/// +/// All distances - Vector distances for each shape point +/// Vector of letters +/// Pivot for start projection in 2d +/// Convert index to addresss inside of shape +/// Cutted parts from surface +/// Closest distance projection indexed by points in shapes(see s2i) +ProjectionDistances choose_best_distance( + const VDistances &distances, + const ExPolygons &shapes, + const Point &start, + const ExPolygonsIndices &s2i, + const SurfacePatches &patches); + +/// +/// Create mask for patches +/// +/// For each point selected closest distance +/// All patches +/// Shape to cut +/// Bound of shapes +/// +/// +/// +/// +/// Mask of used patch +std::vector select_patches(const ProjectionDistances &best_distances, + const SurfacePatches &patches, + const ExPolygons &shapes, + const BoundingBox &shapes_bb, + const ExPolygonsIndices &s2i, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes, + const Project &projection); + +/// +/// Merge two surface cuts together +/// Added surface cut will be consumed +/// +/// Surface cut to extend +/// Surface cut to consume +void append(SurfaceCut &sc, SurfaceCut &&sc_add); + +/// +/// Convert patch to indexed_triangle_set +/// +/// Part of surface +/// Converted patch +SurfaceCut patch2cut(SurfacePatch &patch); + +/// +/// Merge masked patches to one surface cut +/// +/// All patches +/// NOTE: Not const because One needs to add property for Convert indices +/// Mash for using patch +/// Result surface cut +SurfaceCut merge_patches(/*const*/ SurfacePatches &patches, + const std::vector &mask); + +#ifdef DEBUG_OUTPUT_DIR +void prepare_dir(const std::string &dir); +void initialize_store(const std::string &dir_to_clear); +/// +/// Debug purpose store of mesh with colored face by face type +/// +/// Input mesh, could add property color +/// NOTE: Not const because need to [optionaly] append color property map +/// Color source +/// File to store +void store(const CutMesh &mesh, const FaceTypeMap &face_type_map, const std::string &dir, bool is_filled = false); +void store(const ExPolygons &shapes, const std::string &svg_file); +void store(const CutMesh &mesh, const ReductionMap &reduction_map, const std::string &dir); +void store(const CutAOIs &aois, const CutMesh &mesh, const std::string &dir); +void store(const SurfacePatches &patches, const std::string &dir); +void store(const Vec3f &vertex, const Vec3f &normal, const std::string &file, float size = 2.f); +//void store(const ProjectionDistances &pds, const VCutAOIs &aois, const CutMeshes &meshes, const std::string &file, float width = 0.2f/* [in mm] */); +using Connection = std::pair; using Connections = std::vector; +void store(const ExPolygons &shapes, const std::vector &mask_distances, const Connections &connections, const std::string &file_svg); +void store(const SurfaceCut &cut, const std::string &file, const std::string &contour_dir); +void store(const std::vector &models, const std::string &obj_filename); +void store(const std::vector&models, const std::string &dir); +void store(const Emboss::IProjection &projection, const Point &point_to_project, float projection_ratio, const std::string &obj_filename); +#endif // DEBUG_OUTPUT_DIR +} // namespace privat + +#ifdef DEBUG_OUTPUT_DIR +#include "libslic3r/SVG.hpp" +#include +#include +#endif // DEBUG_OUTPUT_DIR + +SurfaceCut Slic3r::cut_surface(const ExPolygons &shapes, + const std::vector &models, + const Emboss::IProjection &projection, + float projection_ratio) +{ + assert(!models.empty()); + assert(!shapes.empty()); + if (models.empty() || shapes.empty() ) return {}; + +#ifdef DEBUG_OUTPUT_DIR + priv::initialize_store(DEBUG_OUTPUT_DIR); + priv::store(models, DEBUG_OUTPUT_DIR + "models_input.obj"); + priv::store(shapes, DEBUG_OUTPUT_DIR + "shapes.svg"); +#endif // DEBUG_OUTPUT_DIR + + // for filter out triangles out of bounding box + BoundingBox shapes_bb = get_extents(shapes); +#ifdef DEBUG_OUTPUT_DIR + priv::store(projection, shapes_bb.center(), projection_ratio, DEBUG_OUTPUT_DIR + "projection_center.obj"); +#endif // DEBUG_OUTPUT_DIR + + // for filttrate opposite triangles and a little more + const float max_angle = 89.9f; + priv::CutMeshes cgal_models; // source for patch + priv::CutMeshes cgal_neg_models; // model used for differenciate patches + cgal_models.reserve(models.size()); + for (const indexed_triangle_set &its : models) { + std::vector skip_indicies(its.indices.size(), {false}); + priv::set_skip_for_out_of_aoi(skip_indicies, its, projection, shapes_bb); + + // create model for differenciate cutted patches + bool flip = true; + cgal_neg_models.push_back(priv::to_cgal(its, skip_indicies, flip)); + + // cut out more than only opposit triangles + priv::set_skip_by_angle(skip_indicies, its, projection, max_angle); + cgal_models.push_back(priv::to_cgal(its, skip_indicies)); + } +#ifdef DEBUG_OUTPUT_DIR + priv::store(cgal_models, DEBUG_OUTPUT_DIR + "model/");// model[0-N].off + priv::store(cgal_neg_models, DEBUG_OUTPUT_DIR + "model_neg/"); // model[0-N].off +#endif // DEBUG_OUTPUT_DIR + + priv::CutMesh cgal_shape = priv::to_cgal(shapes, projection); +#ifdef DEBUG_OUTPUT_DIR + CGAL::IO::write_OFF(DEBUG_OUTPUT_DIR + "shape.off", cgal_shape); // only debug +#endif // DEBUG_OUTPUT_DIR + + // create tool for convert index to shape Point adress and vice versa + ExPolygonsIndices s2i(shapes); + priv::VCutAOIs model_cuts; + // cut shape from each cgal model + for (priv::CutMesh &cgal_model : cgal_models) { + priv::CutAOIs cutAOIs = priv::cut_from_model( + cgal_model, shapes, cgal_shape, projection_ratio, s2i); +#ifdef DEBUG_OUTPUT_DIR + size_t index = &cgal_model - &cgal_models.front(); + priv::store(cutAOIs, cgal_model, DEBUG_OUTPUT_DIR + "model_AOIs/" + std::to_string(index) + "/"); // only debug +#endif // DEBUG_OUTPUT_DIR + model_cuts.push_back(std::move(cutAOIs)); + } + + priv::SurfacePatches patches = priv::diff_models(model_cuts, cgal_models, cgal_neg_models, projection); +#ifdef DEBUG_OUTPUT_DIR + priv::store(patches, DEBUG_OUTPUT_DIR + "patches/"); +#endif // DEBUG_OUTPUT_DIR + if (patches.empty()) return {}; + + // fix - convert shape_point_id to expolygon index + // save 1 param(s2i) from diff_models call + for (priv::SurfacePatch &patch : patches) + patch.shape_id = s2i.cvt(patch.shape_id).expolygons_index; + + // calc distance to projection for all outline points of cutAOI(shape) + // it is used for distiguish the top one + uint32_t shapes_points = s2i.get_count(); + // for each point collect all projection distances + priv::VDistances distances = priv::calc_distances(patches, cgal_models, cgal_shape, shapes_points, projection_ratio); + + Point start = shapes_bb.center(); // only align center + + // Use only outline points + // for each point select best projection + priv::ProjectionDistances best_projection = priv::choose_best_distance(distances, shapes, start, s2i, patches); + std::vector use_patch = priv::select_patches(best_projection, patches, shapes, shapes_bb, s2i, model_cuts, cgal_models, projection); + SurfaceCut result = merge_patches(patches, use_patch); + //*/ + +#ifdef DEBUG_OUTPUT_DIR + priv::store(result, DEBUG_OUTPUT_DIR + "result.obj", DEBUG_OUTPUT_DIR + "result_contours/"); +#endif // DEBUG_OUTPUT_DIR + return result; +} + +indexed_triangle_set Slic3r::cut2model(const SurfaceCut &cut, + const Emboss::IProject3d &projection) +{ + assert(!cut.empty()); + size_t count_vertices = cut.vertices.size() * 2; + size_t count_indices = cut.indices.size() * 2; + + // indices from from zig zag + for (const auto &c : cut.contours) { + assert(!c.empty()); + count_indices += c.size() * 2; + } + + indexed_triangle_set result; + result.vertices.reserve(count_vertices); + result.indices.reserve(count_indices); + + // front + result.vertices.insert(result.vertices.end(), + cut.vertices.begin(), cut.vertices.end()); + result.indices.insert(result.indices.end(), + cut.indices.begin(), cut.indices.end()); + + // back + for (const Vec3f &v : cut.vertices) { + Vec3d vd = v.cast(); + Vec3d vd2 = projection.project(vd); + result.vertices.push_back(vd2.cast()); + } + + size_t back_offset = cut.vertices.size(); + for (const auto &i : cut.indices) { + // check range of indices in cut + assert(i.x() + back_offset < result.vertices.size()); + assert(i.y() + back_offset < result.vertices.size()); + assert(i.z() + back_offset < result.vertices.size()); + assert(i.x() >= 0 && i.x() < cut.vertices.size()); + assert(i.y() >= 0 && i.y() < cut.vertices.size()); + assert(i.z() >= 0 && i.z() < cut.vertices.size()); + // Y and Z is swapped CCW triangles for back side + result.indices.emplace_back(i.x() + back_offset, + i.z() + back_offset, + i.y() + back_offset); + } + + // zig zag indices + for (const auto &contour : cut.contours) { + size_t prev_front_index = contour.back(); + size_t prev_back_index = back_offset + prev_front_index; + for (size_t front_index : contour) { + assert(front_index < cut.vertices.size()); + size_t back_index = back_offset + front_index; + result.indices.emplace_back(front_index, prev_front_index, back_index); + result.indices.emplace_back(prev_front_index, prev_back_index, back_index); + prev_front_index = front_index; + prev_back_index = back_index; + } + } + + assert(count_vertices == result.vertices.size()); + assert(count_indices == result.indices.size()); + return result; +} + +// set_skip_for_out_of_aoi helping functions +namespace priv { +// define plane +using PointNormal = std::pair; +using PointNormals = std::array; + +/// +/// Check +/// +/// +/// +/// +/// +bool is_out_of(const Vec3d &v, const PointNormal &point_normal); + +using IsOnSides = std::vector>; +/// +/// Check if triangle t has all vertices out of any plane +/// +/// Triangle +/// Flag is vertex index out of plane +/// True when triangle is out of one of plane +bool is_all_on_one_side(const Vec3i32 &t, const IsOnSides& is_on_sides); + +} // namespace priv + +bool priv::is_out_of(const Vec3d &v, const PointNormal &point_normal) +{ + const Vec3d& p = point_normal.first; + const Vec3d& n = point_normal.second; + double signed_distance = (v - p).dot(n); + return signed_distance > 1e-5; +}; + +bool priv::is_all_on_one_side(const Vec3i32 &t, const IsOnSides& is_on_sides) { + for (size_t side = 0; side < 4; side++) { + bool result = true; + for (auto vi : t) { + if (!is_on_sides[vi][side]) { + result = false; + break; + } + } + if (result) return true; + } + return false; +} + +void priv::set_skip_for_out_of_aoi(std::vector &skip_indicies, + const indexed_triangle_set &its, + const Project &projection, + const BoundingBox &shapes_bb) +{ + assert(skip_indicies.size() == its.indices.size()); + // 1`*----* 2` + // / 2 /| + // 1 *----* | + // | | * 3` + // | |/ + // 0 *----* 3 + ////////////////// + std::array, 4> bb; + int index = 0; + for (Point v : + {shapes_bb.min, Point{shapes_bb.min.x(), shapes_bb.max.y()}, + shapes_bb.max, Point{shapes_bb.max.x(), shapes_bb.min.y()}}) + bb[index++] = projection.create_front_back(v); + + // define planes to test + // 0 .. under + // 1 .. left + // 2 .. above + // 3 .. right + size_t prev_i = 3; + // plane is defined by point and normal + PointNormals point_normals; + for (size_t i = 0; i < 4; i++) { + const Vec3d &p1 = bb[i].first; + const Vec3d &p2 = bb[i].second; + const Vec3d &p3 = bb[prev_i].first; + prev_i = i; + + Vec3d v1 = p2 - p1; + v1.normalize(); + Vec3d v2 = p3 - p1; + v2.normalize(); + + Vec3d normal = v2.cross(v1); + normal.normalize(); + + point_normals[i] = {p1, normal}; + } + + // check that projection is not left handed + // Fix for reflected projection + if (is_out_of(point_normals[2].first, point_normals[0])) { + // projection is reflected so normals are reflected + for (auto &pn : point_normals) + pn.second *= -1; + } + + // same meaning as point normal + IsOnSides is_on_sides(its.vertices.size(), {false,false,false,false}); + + // inspect all vertices when it is out of bounding box + tbb::parallel_for(tbb::blocked_range(0, its.vertices.size()), + [&its, &point_normals, &is_on_sides](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + Vec3d v = its.vertices[i].cast(); + // under + above + for (int side : {0, 2}) { + if (is_out_of(v, point_normals[side])) { + is_on_sides[i][side] = true; + // when it is under it can't be above + break; + } + } + // left + right + for (int side : {1, 3}) { + if (is_out_of(v, point_normals[side])) { + is_on_sides[i][side] = true; + // when it is on left side it can't be on right + break; + } + } + } + }); // END parallel for + + // inspect all triangles, when it is out of bounding box + tbb::parallel_for(tbb::blocked_range(0, its.indices.size()), + [&its, &is_on_sides, &skip_indicies](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + if (is_all_on_one_side(its.indices[i], is_on_sides)) + skip_indicies[i] = true; + } + }); // END parallel for +} + +indexed_triangle_set Slic3r::its_mask(const indexed_triangle_set &its, + const std::vector &mask) +{ + if (its.indices.size() != mask.size()) { + assert(false); + return {}; + } + + std::vector cvt_vetices(its.vertices.size(), {std::numeric_limits::max()}); + size_t vertices_count = 0; + size_t faces_count = 0; + for (const auto &t : its.indices) { + size_t index = &t - &its.indices.front(); + if (!mask[index]) continue; + ++faces_count; + for (const auto vi : t) { + uint32_t &cvt = cvt_vetices[vi]; + if (cvt == std::numeric_limits::max()) + cvt = vertices_count++; + } + } + if (faces_count == 0) return {}; + + indexed_triangle_set result; + result.indices.reserve(faces_count); + result.vertices = std::vector(vertices_count); + for (size_t i = 0; i < its.vertices.size(); ++i) { + uint32_t index = cvt_vetices[i]; + if (index == std::numeric_limits::max()) continue; + result.vertices[index] = its.vertices[i]; + } + + for (const stl_triangle_vertex_indices &f : its.indices) + if (mask[&f - &its.indices.front()]) + result.indices.push_back(stl_triangle_vertex_indices( + cvt_vetices[f[0]], cvt_vetices[f[1]], cvt_vetices[f[2]])); + + return result; +} + +indexed_triangle_set Slic3r::its_cut_AoI(const indexed_triangle_set &its, + const BoundingBox &bb, + const Emboss::IProjection &projection) +{ + std::vector skip_indicies(its.indices.size(), false); + priv::set_skip_for_out_of_aoi(skip_indicies, its, projection, bb); + // invert values in vector of bool + skip_indicies.flip(); + return its_mask(its, skip_indicies); +} + +void priv::set_skip_by_angle(std::vector &skip_indicies, + const indexed_triangle_set &its, + const Project3d &projection, + double max_angle) +{ + assert(max_angle < 90. && max_angle > 89.); + assert(skip_indicies.size() == its.indices.size()); + float threshold = static_cast(cos(max_angle / 180. * M_PI)); + for (const stl_triangle_vertex_indices& face : its.indices) { + size_t index = &face - &its.indices.front(); + if (skip_indicies[index]) continue; + Vec3f n = its_face_normal(its, face); + const Vec3f& v = its.vertices[face[0]]; + const Vec3d vd = v.cast(); + // Improve: For Orthogonal Projection it is same for each vertex + Vec3d projectedd = projection.project(vd); + Vec3f projected = projectedd.cast(); + Vec3f project_dir = projected - v; + project_dir.normalize(); + float cos_alpha = project_dir.dot(n); + if (cos_alpha > threshold) continue; + skip_indicies[index] = true; + } +} + +priv::CutMesh priv::to_cgal(const indexed_triangle_set &its, + const std::vector &skip_indicies, + bool flip) +{ + const std::vector &vertices = its.vertices; + const std::vector &indices = its.indices; + + std::vector use_vetices(vertices.size(), {false}); + + size_t vertices_count = 0; + size_t faces_count = 0; + size_t edges_count = 0; + + for (const auto &t : indices) { + size_t index = &t - &indices.front(); + if (skip_indicies[index]) continue; + ++faces_count; + size_t count_used_vertices = 0; + for (const auto vi : t) { + if (!use_vetices[vi]) { + ++vertices_count; + use_vetices[vi] = true; + } else { + ++count_used_vertices; + } + } + switch (count_used_vertices) { + case 3: break; // all edges are already counted + case 2: edges_count += 2; break; + case 1: + case 0: edges_count += 3; break; + default: assert(false); + } + } + assert(vertices_count <= vertices.size()); + assert(edges_count <= (indices.size() * 3)); + assert(faces_count <= indices.size()); + + CutMesh result; + result.reserve(vertices_count, edges_count, faces_count); + + std::vector to_filtrated_vertices_index(vertices.size()); + size_t filtrated_vertices_index = 0; + for (size_t i = 0; i < vertices.size(); ++i) + if (use_vetices[i]) { + to_filtrated_vertices_index[i] = VI(filtrated_vertices_index); + ++filtrated_vertices_index; + } + + for (const stl_vertex& v : vertices) { + if (!use_vetices[&v - &vertices.front()]) continue; + result.add_vertex(CutMesh::Point{v.x(), v.y(), v.z()}); + } + + if (!flip) { + for (const stl_triangle_vertex_indices &f : indices) { + if (skip_indicies[&f - &indices.front()]) continue; + result.add_face(to_filtrated_vertices_index[f[0]], + to_filtrated_vertices_index[f[1]], + to_filtrated_vertices_index[f[2]]); + } + } else { + for (const stl_triangle_vertex_indices &f : indices) { + if (skip_indicies[&f - &indices.front()]) continue; + result.add_face(to_filtrated_vertices_index[f[2]], + to_filtrated_vertices_index[f[1]], + to_filtrated_vertices_index[f[0]]); + } + } + + return result; +} + +bool priv::exist_duplicit_vertex(const CutMesh &mesh) { + std::vector points; + points.reserve(mesh.vertices().size()); + // copy points + for (VI vi : mesh.vertices()) { + const P3 &p = mesh.point(vi); + points.emplace_back(p.x(), p.y(), p.z()); + } + std::sort(points.begin(), points.end(), [](const Vec3d &v1, const Vec3d &v2) { + return v1.x() < v2.x() || + (v1.x() == v2.x() && + (v1.y() < v2.y() || + (v1.y() == v2.y() && + v1.z() < v2.z()))); + }); + // find first duplicit + auto it = std::adjacent_find(points.begin(), points.end()); + return it != points.end(); +} + +priv::CutMesh priv::to_cgal(const ExPolygons &shapes, + const Project &projection) +{ + if (shapes.empty()) return {}; + + CutMesh result; + EdgeShapeMap edge_shape_map = result.add_property_map(edge_shape_map_name).first; + FaceShapeMap face_shape_map = result.add_property_map(face_shape_map_name).first; + + std::vector indices; + auto insert_contour = [&projection, &indices, &result, + &edge_shape_map, &face_shape_map] + (const Polygon &polygon) { + indices.clear(); + indices.reserve(polygon.points.size() * 2); + size_t num_vertices_old = result.number_of_vertices(); + for (const Point &polygon_point : polygon.points) { + auto [front, back] = projection.create_front_back(polygon_point); + P3 v_front{front.x(), front.y(), front.z()}; + VI vi1 = result.add_vertex(v_front); + assert(vi1.idx() == (indices.size() + num_vertices_old)); + indices.push_back(vi1); + + P3 v_back{back.x(), back.y(), back.z()}; + VI vi2 = result.add_vertex(v_back); + assert(vi2.idx() == (indices.size() + num_vertices_old)); + indices.push_back(vi2); + } + + auto find_edge = [&result](FI fi, VI from, VI to) { + HI hi = result.halfedge(fi); + for (; result.target(hi) != to; hi = result.next(hi)); + assert(result.source(hi) == from); + assert(result.target(hi) == to); + return result.edge(hi); + }; + + uint32_t contour_index = static_cast(num_vertices_old / 2); + for (int32_t i = 0; i < int32_t(indices.size()); i += 2) { + bool is_first = i == 0; + bool is_last = size_t(i + 2) >= indices.size(); + int32_t j = is_last ? 0 : (i + 2); + + FI fi1 = result.add_face(indices[i], indices[j], indices[i + 1]); + EI ei1 = find_edge(fi1, indices[i + 1], indices[i]); + EI ei2 = find_edge(fi1, indices[j], indices[i + 1]); + FI fi2 = result.add_face(indices[j], indices[j + 1], indices[i + 1]); + IntersectingElement element {contour_index, (unsigned char)IntersectingElement::Type::undefined}; + if (is_first) element.set_is_first(); + if (is_last) element.set_is_last(); + edge_shape_map[ei1] = element.set_type(IntersectingElement::Type::edge_1); + face_shape_map[fi1] = element.set_type(IntersectingElement::Type::face_1); + edge_shape_map[ei2] = element.set_type(IntersectingElement::Type::edge_2); + face_shape_map[fi2] = element.set_type(IntersectingElement::Type::face_2); + ++contour_index; + } + }; + + size_t count_point = count_points(shapes); + result.reserve(result.number_of_vertices() + 2 * count_point, + result.number_of_edges() + 4 * count_point, + result.number_of_faces() + 2 * count_point); + + // Identify polygon + for (const ExPolygon &shape : shapes) { + insert_contour(shape.contour); + for (const Polygon &hole : shape.holes) + insert_contour(hole); + } + assert(!exist_duplicit_vertex(result)); + return result; +} + +priv::ModelCut2index::ModelCut2index(const VCutAOIs &cuts) +{ + // prepare offsets + m_offsets.reserve(cuts.size()); + uint32_t offset = 0; + for (const CutAOIs &model_cuts: cuts) { + m_offsets.push_back(offset); + offset += model_cuts.size(); + } + m_count = offset; +} + +uint32_t priv::ModelCut2index::calc_index(const ModelCutId &id) const +{ + assert(id.model_index < m_offsets.size()); + uint32_t offset = m_offsets[id.model_index]; + uint32_t res = offset + id.cut_index; + assert(((id.model_index+1) < m_offsets.size() && res < m_offsets[id.model_index+1]) || + ((id.model_index+1) == m_offsets.size() && res < m_count)); + return res; +} + +priv::ModelCutId priv::ModelCut2index::calc_id(uint32_t index) const +{ + assert(index < m_count); + ModelCutId result{0,0}; + // find shape index + for (size_t model_index = 1; model_index < m_offsets.size(); ++model_index) { + if (m_offsets[model_index] > index) break; + result.model_index = model_index; + } + result.cut_index = index - m_offsets[result.model_index]; + return result; +} + +// cut_from_model help functions +namespace priv { + +/// +/// Track source of intersection +/// Help for anotate inner and outer faces +/// +struct Visitor : public CGAL::Polygon_mesh_processing::Corefinement::Default_visitor { + Visitor(const CutMesh &object, const CutMesh &shape, EdgeShapeMap edge_shape_map, + FaceShapeMap face_shape_map, VertexShapeMap vert_shape_map, bool* is_valid) : + object(object), shape(shape), edge_shape_map(edge_shape_map), face_shape_map(face_shape_map), + vert_shape_map(vert_shape_map), is_valid(is_valid) + {} + + const CutMesh &object; + const CutMesh &shape; + + // Properties of the shape mesh: + EdgeShapeMap edge_shape_map; + FaceShapeMap face_shape_map; + + // Properties of the object mesh. + VertexShapeMap vert_shape_map; + + // check for anomalities + bool* is_valid; + + // keep source of intersection for each intersection + // used to copy data into vert_shape_map + std::vector intersections; + + /// + /// Called when a new intersection point is detected. + /// The intersection is detected using a face of tm_f and an edge of tm_e. + /// Intersecting an edge hh_edge from tm_f with a face h_e of tm_e. + /// https://doc.cgal.org/latest/Polygon_mesh_processing/classPMPCorefinementVisitor.html#a00ee0ca85db535a48726a92414acda7f + /// + /// The id of the intersection point, starting at 0. Ids are consecutive. + /// Dimension of a simplex part of face(h_e) that is intersected by edge(h_f): + /// 0 for vertex: target(h_e) + /// 1 for edge: h_e + /// 2 for the interior of face: face(h_e) + /// + /// A halfedge from tm_f indicating the simplex intersected: + /// if sdim==0 the target of h_f is the intersection point, + /// if sdim==1 the edge of h_f contains the intersection point in its interior, + /// if sdim==2 the face of h_f contains the intersection point in its interior. + /// @Vojta: Edge of tm_f, see is_target_coplanar & is_source_coplanar whether any vertex of h_f is coplanar with face(h_e). + /// + /// A halfedge from tm_e + /// @Vojta: Vertex, halfedge or face of tm_e intersected by h_f, see comment at sdim. + /// + /// Mesh containing h_f + /// Mesh containing h_e + /// True if the target of h_e is the intersection point + /// @Vojta: source(h_f) is coplanar with face(made by h_e). + /// True if the source of h_e is the intersection point + /// @Vojta: target(h_f) is coplanar with face(h_e). + void intersection_point_detected(std::size_t i_id, + int sdim, + HI h_f, + HI h_e, + const CutMesh &tm_f, + const CutMesh &tm_e, + bool is_target_coplanar, + bool is_source_coplanar); + + /// + /// Called when a new vertex is added in tm (either an edge split or a vertex inserted in the interior of a face). + /// Fill vertex_shape_map by intersections + /// + /// Order number of intersection point + /// New added vertex + /// Affected mesh + void new_vertex_added(std::size_t i_id, VI v, const CutMesh &tm); +}; + +/// +/// Distiquish face type for half edge +/// +/// Define face +/// Mesh to process +/// Vertices of mesh made by shapes +/// Keep information about source of created vertex +/// +/// Convert index to shape point from ExPolygons +/// Face type defined by hi +bool is_face_inside(HI hi, + const CutMesh &mesh, + const CutMesh &shape_mesh, + const VertexShapeMap &vertex_shape_map, + const ExPolygonsIndices &shape2index); + +/// +/// Face with constrained edge are inside/outside by type of intersection +/// Other set to not_constrained(still it could be inside/outside) +/// +/// [Output] property map with type of faces +/// Mesh to process +/// Keep information about source of created vertex +/// Dynamic Edge Constrained Map of bool +/// Vertices of mesh made by shapes +/// Convert index to shape point from ExPolygons +void set_face_type(FaceTypeMap &face_type_map, + const CutMesh &mesh, + const VertexShapeMap &vertex_shape_map, + const EdgeBoolMap &ecm, + const CutMesh &shape_mesh, + const ExPolygonsIndices &shape2index); + +/// +/// Change FaceType from not_constrained to inside +/// For neighbor(or neighbor of neighbor of ...) of inside triangles. +/// Process only not_constrained triangles +/// +/// Corefined mesh +/// In/Out map with faces type +void flood_fill_inner(const CutMesh &mesh, FaceTypeMap &face_type_map); + +/// +/// Collect connected inside faces +/// Collect outline half edges +/// +/// Queue of face to process - find connected +/// [Output] collected Face indices from mesh +/// [Output] collected Halfedge indices from mesh +/// Use flag inside / outside +/// NOTE: Modify in function: inside -> inside_processed +/// mesh to process +void collect_surface_data(std::queue &process, + std::vector &faces, + std::vector &outlines, + FaceTypeMap &face_type_map, + const CutMesh &mesh); + +/// +/// Create areas from mesh surface +/// +/// Model +/// Cutted shapes +/// Define Triangles of interest. +/// Edge between inside / outside. +/// NOTE: Not const because it need to flag proccessed faces +/// Areas of interest from mesh +CutAOIs create_cut_area_of_interests(const CutMesh &mesh, + const ExPolygons &shapes, + FaceTypeMap &face_type_map); + +} // namespace priv + +void priv::Visitor::intersection_point_detected(std::size_t i_id, + int sdim, + HI h_f, + HI h_e, + const CutMesh &tm_f, + const CutMesh &tm_e, + bool is_target_coplanar, + bool is_source_coplanar) +{ + if (i_id >= intersections.size()) { + size_t capacity = Slic3r::next_highest_power_of_2(i_id + 1); + intersections.reserve(capacity); + intersections.resize(capacity); + } + + const IntersectingElement *intersection_ptr = nullptr; + if (&tm_e == &shape) { + assert(&tm_f == &object); + switch (sdim) { + case 1: + // edge x edge intersection + intersection_ptr = &edge_shape_map[shape.edge(h_e)]; + break; + case 2: + // edge x face intersection + intersection_ptr = &face_shape_map[shape.face(h_e)]; + break; + default: assert(false); + } + if (is_target_coplanar) + vert_shape_map[object.source(h_f)] = intersection_ptr; + if (is_source_coplanar) + vert_shape_map[object.target(h_f)] = intersection_ptr; + } else { + assert(&tm_f == &shape && &tm_e == &object); + assert(!is_target_coplanar); + assert(!is_source_coplanar); + if (is_target_coplanar || is_source_coplanar) + *is_valid = false; + intersection_ptr = &edge_shape_map[shape.edge(h_f)]; + if (sdim == 0) vert_shape_map[object.target(h_e)] = intersection_ptr; + } + + if (intersection_ptr->shape_point_index == std::numeric_limits::max()) { + // there is unexpected intersection + // Top (or Bottom) shape contour edge (or vertex) intersection + // Suggest to change projection min/max limits + *is_valid = false; + } + intersections[i_id] = intersection_ptr; +} + +void priv::Visitor::new_vertex_added(std::size_t i_id, VI v, const CutMesh &tm) +{ + assert(&tm == &object); + assert(i_id < intersections.size()); + const IntersectingElement *intersection_ptr = intersections[i_id]; + assert(intersection_ptr != nullptr); + // intersection was not filled in function intersection_point_detected + //assert(intersection_ptr->point_index != std::numeric_limits::max()); + vert_shape_map[v] = intersection_ptr; +} + +bool priv::is_face_inside(HI hi, + const CutMesh &mesh, + const CutMesh &shape_mesh, + const VertexShapeMap &vertex_shape_map, + const ExPolygonsIndices &shape2index) +{ + VI vi_from = mesh.source(hi); + VI vi_to = mesh.target(hi); + // This face has a constrained edge. + const IntersectingElement &shape_from = *vertex_shape_map[vi_from]; + const IntersectingElement &shape_to = *vertex_shape_map[vi_to]; + assert(shape_from.shape_point_index != std::numeric_limits::max()); + assert(shape_from.attr != (unsigned char) IntersectingElement::Type::undefined); + assert(shape_to.shape_point_index != std::numeric_limits::max()); + assert(shape_to.attr != (unsigned char) IntersectingElement::Type::undefined); + + // index into contour + uint32_t i_from = shape_from.shape_point_index; + uint32_t i_to = shape_to.shape_point_index; + IntersectingElement::Type type_from = shape_from.get_type(); + IntersectingElement::Type type_to = shape_to.get_type(); + if (i_from == i_to && type_from == type_to) { + // intersecting element must be face + assert(type_from == IntersectingElement::Type::face_1 || + type_from == IntersectingElement::Type::face_2); + + // count of vertices is twice as count of point in the contour + uint32_t i = i_from * 2; + // j is next contour point in vertices + uint32_t j = i + 2; + if (shape_from.is_last()) { + ExPolygonsIndex point_id = shape2index.cvt(i_from); + point_id.point_index = 0; + j = shape2index.cvt(point_id)*2; + } + + // opposit point(in triangle face) to edge + const P3 &p = mesh.point(mesh.target(mesh.next(hi))); + + // abc is source triangle face + CGAL::Sign abcp = type_from == IntersectingElement::Type::face_1 ? + CGAL::orientation(shape_mesh.point(VI(i)), + shape_mesh.point(VI(i + 1)), + shape_mesh.point(VI(j)), p) : + // type_from == IntersectingElement::Type::face_2 + CGAL::orientation(shape_mesh.point(VI(j)), + shape_mesh.point(VI(i + 1)), + shape_mesh.point(VI(j + 1)), p); + return abcp == CGAL::POSITIVE; + } else if (i_from < i_to || (i_from == i_to && type_from < type_to)) { + bool is_last = shape_to.is_last() && shape_from.is_first(); + // check continuity of indicies + assert(i_from == i_to || is_last || (i_from + 1) == i_to); + return !is_last; + } else { + assert(i_from > i_to || (i_from == i_to && type_from > type_to)); + bool is_last = shape_to.is_first() && shape_from.is_last(); + // check continuity of indicies + assert(i_from == i_to || is_last || (i_to + 1) == i_from); + return is_last; + } + + assert(false); + return false; +} + +void priv::set_face_type(FaceTypeMap &face_type_map, + const CutMesh &mesh, + const VertexShapeMap &vertex_shape_map, + const EdgeBoolMap &ecm, + const CutMesh &shape_mesh, + const ExPolygonsIndices &shape2index) +{ + for (EI ei : mesh.edges()) { + if (!ecm[ei]) continue; + HI hi = mesh.halfedge(ei); + FI fi = mesh.face(hi); + bool is_inside = is_face_inside(hi, mesh, shape_mesh, vertex_shape_map, shape2index); + face_type_map[fi] = is_inside ? FaceType::inside : FaceType::outside; + HI hi_op = mesh.opposite(hi); + assert(hi_op.is_valid()); + if (!hi_op.is_valid()) continue; + FI fi_op = mesh.face(hi_op); + assert(fi_op.is_valid()); + if (!fi_op.is_valid()) continue; + face_type_map[fi_op] = (!is_inside) ? FaceType::inside : FaceType::outside; + } +} + +priv::CutAOIs priv::cut_from_model(CutMesh &cgal_model, + const ExPolygons &shapes, + CutMesh &cgal_shape, + float projection_ratio, + const ExPolygonsIndices &s2i) +{ + // pointer to edge or face shape_map + VertexShapeMap vert_shape_map = cgal_model.add_property_map(vert_shape_map_name, nullptr).first; + + // detect anomalities in visitor. + bool is_valid = true; + // NOTE: map are created when convert shapes to cgal model + const EdgeShapeMap& edge_shape_map = cgal_shape.property_map(edge_shape_map_name).first; + const FaceShapeMap& face_shape_map = cgal_shape.property_map(face_shape_map_name).first; + Visitor visitor{cgal_model, cgal_shape, edge_shape_map, face_shape_map, vert_shape_map, &is_valid}; + + // a property map containing the constrained-or-not status of each edge + EdgeBoolMap ecm = cgal_model.add_property_map(is_constrained_edge_name, false).first; + const auto &p = CGAL::parameters::visitor(visitor) + .edge_is_constrained_map(ecm) + .throw_on_self_intersection(false); + const auto& q = CGAL::parameters::do_not_modify(true); + CGAL::Polygon_mesh_processing::corefine(cgal_model, cgal_shape, p, q); + + if (!is_valid) return {}; + + FaceTypeMap face_type_map = cgal_model.add_property_map(face_type_map_name, FaceType::not_constrained).first; + + // Select inside and outside face in model + set_face_type(face_type_map, cgal_model, vert_shape_map, ecm, cgal_shape, s2i); +#ifdef DEBUG_OUTPUT_DIR + store(cgal_model, face_type_map, DEBUG_OUTPUT_DIR + "constrained/"); // only debug +#endif // DEBUG_OUTPUT_DIR + + // flood fill the other faces inside the region. + flood_fill_inner(cgal_model, face_type_map); + +#ifdef DEBUG_OUTPUT_DIR + store(cgal_model, face_type_map, DEBUG_OUTPUT_DIR + "filled/", true); // only debug +#endif // DEBUG_OUTPUT_DIR + + // IMPROVE: AOIs area could be created during flood fill + return create_cut_area_of_interests(cgal_model, shapes, face_type_map); +} + +void priv::flood_fill_inner(const CutMesh &mesh, + FaceTypeMap &face_type_map) +{ + std::vector process; + // guess count of connected not constrained triangles + size_t guess_size = 128; + process.reserve(guess_size); + + // check if neighbor(one of three in triangle) has type inside + auto has_inside_neighbor = [&mesh, &face_type_map](FI fi) { + HI hi = mesh.halfedge(fi); + HI hi_end = hi; + auto exist_next = [&hi, &hi_end, &mesh]() -> bool { + hi = mesh.next(hi); + return hi != hi_end; + }; + // loop over 3 half edges of face + do { + HI hi_opposite = mesh.opposite(hi); + // open edge doesn't have opposit half edge + if (!hi_opposite.is_valid()) continue; + FI fi_opposite = mesh.face(hi_opposite); + if (!fi_opposite.is_valid()) continue; + if (face_type_map[fi_opposite] == FaceType::inside) return true; + } while (exist_next()); + return false; + }; + + for (FI fi : mesh.faces()) { + FaceType type = face_type_map[fi]; + if (type != FaceType::not_constrained) continue; + if (!has_inside_neighbor(fi)) continue; + assert(process.empty()); + process.push_back(fi); + //store(mesh, face_type_map, DEBUG_OUTPUT_DIR + "progress.off"); + + while (!process.empty()) { + FI process_fi = process.back(); + process.pop_back(); + // Do not fill twice + FaceType& process_type = face_type_map[process_fi]; + if (process_type == FaceType::inside) continue; + process_type = FaceType::inside; + + // check neighbor triangle + HI hi = mesh.halfedge(process_fi); + HI hi_end = hi; + auto exist_next = [&hi, &hi_end, &mesh]() -> bool { + hi = mesh.next(hi); + return hi != hi_end; + }; + do { + HI hi_opposite = mesh.opposite(hi); + // open edge doesn't have opposit half edge + if (!hi_opposite.is_valid()) continue; + FI fi_opposite = mesh.face(hi_opposite); + if (!fi_opposite.is_valid()) continue; + FaceType type_opposite = face_type_map[fi_opposite]; + if (type_opposite == FaceType::not_constrained) + process.push_back(fi_opposite); + } while (exist_next()); + } + } +} + +void priv::collect_surface_data(std::queue &process, + std::vector &faces, + std::vector &outlines, + FaceTypeMap &face_type_map, + const CutMesh &mesh) +{ + assert(!process.empty()); + assert(faces.empty()); + assert(outlines.empty()); + while (!process.empty()) { + FI fi = process.front(); + process.pop(); + + FaceType &fi_type = face_type_map[fi]; + // Do not process twice + if (fi_type == FaceType::inside_processed) continue; + assert(fi_type == FaceType::inside); + // flag face as processed + fi_type = FaceType::inside_processed; + faces.push_back(fi); + + // check neighbor triangle + HI hi = mesh.halfedge(fi); + HI hi_end = hi; + do { + HI hi_opposite = mesh.opposite(hi); + // open edge doesn't have opposit half edge + if (!hi_opposite.is_valid()) { + outlines.push_back(hi); + hi = mesh.next(hi); + continue; + } + FI fi_opposite = mesh.face(hi_opposite); + if (!fi_opposite.is_valid()) { + outlines.push_back(hi); + hi = mesh.next(hi); + continue; + } + FaceType side = face_type_map[fi_opposite]; + if (side == FaceType::inside) { + process.emplace(fi_opposite); + } else if (side == FaceType::outside) { + // store outlines + outlines.push_back(hi); + } + hi = mesh.next(hi); + } while (hi != hi_end); + } +} + +void priv::create_reduce_map(ReductionMap &reduction_map, const CutMesh &mesh) +{ + const VertexShapeMap &vert_shape_map = mesh.property_map(vert_shape_map_name).first; + const EdgeBoolMap &ecm = mesh.property_map(is_constrained_edge_name).first; + + // check if vertex was made by edge_2 which is diagonal of quad + auto is_reducible_vertex = [&vert_shape_map](VI reduction_from) -> bool { + const IntersectingElement *ie = vert_shape_map[reduction_from]; + if (ie == nullptr) return false; + IntersectingElement::Type type = ie->get_type(); + return type == IntersectingElement::Type::edge_2; + }; + + /// + /// Append reduction or change existing one. + /// + /// HalEdge between outside and inside face. + /// Target vertex will be reduced, source vertex left + /// [[maybe_unused]] &face_type_map, &is_reducible_vertex are need only in debug + auto add_reduction = [&] //&reduction_map, &mesh, &face_type_map, &is_reducible_vertex + (HI hi) { + VI erase = mesh.target(hi); + VI left = mesh.source(hi); + assert(is_reducible_vertex(erase)); + assert(!is_reducible_vertex(left)); + VI &vi = reduction_map[erase]; + // check if it is first add + if (vi.is_valid()) + return; + + // check that all triangles after reduction has 'erase' and 'left' vertex + // on same side of opposite line of vertex in triangle + Vec3d v_erase = to_vec3d(mesh.point(erase)); + Vec3d v_left = to_vec3d(mesh.point(left)); + for (FI fi : mesh.faces_around_target(hi)) { + if (!fi.is_valid()) + continue; + // get vertices of rest + VI vi_a, vi_b; + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) { + if (!vi.is_valid()) + continue; + if (vi == erase) + continue; + if (!vi_a.is_valid()) + vi_a = vi; + else { + assert(!vi_b.is_valid()); + vi_b = vi; + } + } + assert(vi_b.is_valid()); + // do not check triangle, which will be removed + if (vi_a == left || vi_b == left) + continue; + + Vec3d v_a = to_vec3d(mesh.point(vi_a)); + Vec3d v_b = to_vec3d(mesh.point(vi_b)); + // Vectors of triangle edges + Vec3d v_ab = v_b - v_a; + Vec3d v_ae = v_erase - v_a; + Vec3d v_al = v_left - v_a; + + Vec3d n1 = v_ab.cross(v_ae); + Vec3d n2 = v_ab.cross(v_al); + // check that normal has same direction + if (((n1.x() > 0) != (n2.x() > 0)) || + ((n1.y() > 0) != (n2.y() > 0)) || + ((n1.z() > 0) != (n2.z() > 0))) + return; // this reduction will create CCW triangle + } + + reduction_map[erase] = left; + // I have no better rule than take the first + // for decide which reduction will be better + // But it could be use only one of them + }; + + for (EI ei : mesh.edges()) { + if (!ecm[ei]) continue; + HI hi = mesh.halfedge(ei); + VI vi = mesh.target(hi); + if (is_reducible_vertex(vi)) add_reduction(hi); + + HI hi_op = mesh.opposite(hi); + VI vi_op = mesh.target(hi_op); + if (is_reducible_vertex(vi_op)) add_reduction(hi_op); + } +#ifdef DEBUG_OUTPUT_DIR + store(mesh, reduction_map, DEBUG_OUTPUT_DIR + "reduces/"); +#endif // DEBUG_OUTPUT_DIR +} + +priv::CutAOIs priv::create_cut_area_of_interests(const CutMesh &mesh, + const ExPolygons &shapes, + FaceTypeMap &face_type_map) +{ + // IMPROVE: Create better heuristic for count. + size_t faces_per_cut = mesh.faces().size() / shapes.size(); + size_t outlines_per_cut = faces_per_cut / 2; + size_t cuts_per_model = shapes.size() * 2; + + CutAOIs result; + result.reserve(cuts_per_model); + + // It is faster to use one queue for all cuts + std::queue process; + for (FI fi : mesh.faces()) { + if (face_type_map[fi] != FaceType::inside) continue; + + CutAOI cut; + std::vector &faces = cut.first; + std::vector &outlines = cut.second; + + // faces for one surface cut + faces.reserve(faces_per_cut); + // outline for one surface cut + outlines.reserve(outlines_per_cut); + + assert(process.empty()); + // Process queue of faces to separate to surface_cut + process.push(fi); + collect_surface_data(process, faces, outlines, face_type_map, mesh); + assert(!faces.empty()); + assert(!outlines.empty()); + result.emplace_back(std::move(cut)); + } + return result; +} + +namespace priv { + +/// +/// Calculate projection distance of point [in mm] +/// +/// Point to calc distance +/// Index of point on contour +/// Model of cutting shape +/// Ratio for best projection distance +/// Distance of point from best projection +float calc_distance(const P3 &p, + uint32_t pi, + const CutMesh &shapes_mesh, + float projection_ratio); + +} + +float priv::calc_distance(const P3 &p, + uint32_t pi, + const CutMesh &shapes_mesh, + float projection_ratio) +{ + // It is known because shapes_mesh is created inside of private space + VI vi_start(2 * pi); + VI vi_end(2 * pi + 1); + + // Get range for intersection + const P3 &start = shapes_mesh.point(vi_start); + const P3 &end = shapes_mesh.point(vi_end); + + // find index in vector with biggest difference + size_t max_i = 0; + float max_val = 0.f; + for (size_t i = 0; i < 3; i++) { + float val = start[i] - end[i]; + // abs value + if (val < 0.f) val *= -1.f; + if (max_val < val) { + max_val = val; + max_i = i; + } + } + + float from_start = p[max_i] - start[max_i]; + float best_distance = projection_ratio * (end[max_i] - start[max_i]); + return from_start - best_distance; +} + +priv::VDistances priv::calc_distances(const SurfacePatches &patches, + const CutMeshes &models, + const CutMesh &shapes_mesh, + size_t count_shapes_points, + float projection_ratio) +{ + priv::VDistances result(count_shapes_points); + for (const SurfacePatch &patch : patches) { + // map is created during intersection by corefine visitor + const VertexShapeMap &vert_shape_map = + models[patch.model_id].property_map(vert_shape_map_name).first; + uint32_t patch_index = &patch - &patches.front(); + // map is created during patch creation / dividing + const CvtVI2VI& cvt = patch.mesh.property_map(patch_source_name).first; + // for each point on outline + for (const Loop &loop : patch.loops) + for (const VI &vi_patch : loop) { + VI vi_model = cvt[vi_patch]; + if (!vi_model.is_valid()) continue; + const IntersectingElement *ie = vert_shape_map[vi_model]; + if (ie == nullptr) continue; + assert(ie->shape_point_index != std::numeric_limits::max()); + assert(ie->attr != (unsigned char) IntersectingElement::Type::undefined); + uint32_t pi = ie->shape_point_index; + assert(pi <= count_shapes_points); + std::vector &pds = result[pi]; + uint32_t model_index = patch.model_id; + uint32_t aoi_index = patch.aoi_id; + //uint32_t hi_index = &hi - &patch.outline.front(); + const P3 &p = patch.mesh.point(vi_patch); + float distance = calc_distance(p, pi, shapes_mesh, projection_ratio); + pds.push_back({model_index, aoi_index, patch_index, distance}); + } + } + return result; +} + + +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/Line.hpp" +// functions for choose_best_distance +namespace priv { + +// euler square size of vector stored in Point +float calc_size_sq(const Point &p); + +// structure to store index and distance together +struct ClosePoint +{ + // index of closest point from another shape + uint32_t index = std::numeric_limits::max(); + // squere distance to index + float dist_sq = std::numeric_limits::max(); +}; + +struct SearchData{ +// IMPROVE: float lines are enough +std::vector lines; +// convert line index into Shape point index +std::vector cvt; +// contain lines from prev point to Point index +AABBTreeIndirect::Tree<2, double> tree; +}; + +SearchData create_search_data(const ExPolygons &shapes, const std::vector& mask); +uint32_t get_closest_point_index(const SearchData &sd, size_t line_idx, const Vec2d &hit_point, const ExPolygons &shapes, const ExPolygonsIndices &s2i); + +// use AABB Tree Lines to find closest point +uint32_t find_closest_point_index(const Point &p, const ExPolygons &shapes, const ExPolygonsIndices &s2i, const std::vector &mask); + +std::pair find_closest_point_pair(const ExPolygons &shapes, const std::vector &done_shapes, const ExPolygonsIndices &s2i, const std::vector &mask); + +// Search for closest projection to wanted distance +const ProjectionDistance *get_closest_projection(const ProjectionDistances &distance, float wanted_distance); + +// fill result around known index inside one polygon +void fill_polygon_distances(const ProjectionDistance &pd, uint32_t index, const ExPolygonsIndex &id, ProjectionDistances & result, const ExPolygon &shape, const VDistances &distances); + +// search for closest projection for expolygon +// choose correct cut by source point +void fill_shape_distances(uint32_t start_index, const ProjectionDistance *start_pd, ProjectionDistances &result, const ExPolygonsIndices &s2i, const ExPolygon &shape, const VDistances &distances); + +// find close points between finished and unfinished ExPolygons +ClosePoint find_close_point(const Point &p, ProjectionDistances &result, std::vector& finished_shapes,const ExPolygonsIndices &s2i, const ExPolygons &shapes); + +} + +float priv::calc_size_sq(const Point &p){ + // NOTE: p.squaredNorm() can't be use due to overflow max int value + return (float) p.x() * p.x() + (float) p.y() * p.y(); +} + +priv::SearchData priv::create_search_data(const ExPolygons &shapes, + const std::vector &mask) +{ + // IMPROVE: Use float precission (it is enough) + SearchData sd; + sd.lines.reserve(mask.size()); + sd.cvt.reserve(mask.size()); + size_t index = 0; + auto add_lines = [&sd, &index, &mask] + (const Polygon &poly) { + Vec2d prev = poly.back().cast(); + bool use_point = mask[index + poly.points.size() - 1]; + for (const Point &p : poly.points) { + if (!use_point) { + use_point = mask[index]; + if (use_point) prev = p.cast(); + } else if (!mask[index]) { + use_point = false; + } else { + Vec2d p_d = p.cast(); + sd.lines.emplace_back(prev, p_d); + sd.cvt.push_back(index); + prev = p_d; + } + ++index; + } + }; + + for (const ExPolygon &shape : shapes) { + add_lines(shape.contour); + for (const Polygon &hole : shape.holes) add_lines(hole); + } + sd.tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(sd.lines); + return sd; +} + +uint32_t priv::get_closest_point_index(const SearchData &sd, + size_t line_idx, + const Vec2d &hit_point, + const ExPolygons &shapes, + const ExPolygonsIndices &s2i) +{ + const Linef &line = sd.lines[line_idx]; + Vec2d dir = line.a - line.b; + Vec2d dir_abs = dir.cwiseAbs(); + // use x coordinate + int i = (dir_abs.x() > dir_abs.y())? 0 :1; + + bool use_index = abs(line.a[i] - hit_point[i]) > + abs(line.b[i] - hit_point[i]); + size_t point_index = sd.cvt[line_idx]; + + // Lambda used only for check result + [[maybe_unused]] auto is_same = [&s2i, &shapes] + (const Vec2d &p, size_t i) -> bool { + auto id = s2i.cvt(i); + const ExPolygon &shape = shapes[id.expolygons_index]; + const Polygon &poly = (id.polygon_index == 0) ? + shape.contour : + shape.holes[id.polygon_index - 1]; + auto p_ = p.cast(); + return p_ == poly[id.point_index]; + }; + + if (use_index) { + assert(is_same(line.b, point_index)); + return point_index; + } + auto id = s2i.cvt(point_index); + if (id.point_index != 0) { + assert(is_same(line.a, point_index - 1)); + return point_index - 1; + } + const ExPolygon &shape = shapes[id.expolygons_index]; + size_t count_polygon_points = (id.polygon_index == 0) ? + shape.contour.size() : + shape.holes[id.polygon_index - 1].size(); + size_t prev_point_index = point_index + (count_polygon_points - 1); + assert(is_same(line.a, prev_point_index)); + // return previous point index + return prev_point_index; +} + +// use AABB Tree Lines +uint32_t priv::find_closest_point_index(const Point &p, + const ExPolygons &shapes, + const ExPolygonsIndices &s2i, + const std::vector &mask) +{ + SearchData sd = create_search_data(shapes, mask); + if (sd.tree.nodes().size() == 0){ + // no lines in expolygon, check whether exist point to start + double closest_square_distance = INFINITY; + uint32_t closest_id = -1; + for (uint32_t i = 0; i < mask.size(); i++) + if (mask[i]){ + ExPolygonsIndex ei = s2i.cvt(i); + const Point& s_p = ei.is_contour()? + shapes[ei.expolygons_index].contour[ei.point_index]: + shapes[ei.expolygons_index].holes[ei.hole_index()][ei.point_index]; + double square_distance = (p - s_p).cast().squaredNorm(); + if (closest_id >= mask.size() || + closest_square_distance > square_distance) { + closest_id = i; + closest_square_distance = square_distance; + } + } + assert(closest_id < mask.size()); + return closest_id; + } + size_t line_idx = std::numeric_limits::max(); + Vec2d hit_point; + Vec2d p_d = p.cast(); + [[maybe_unused]] double distance_sq = + AABBTreeLines::squared_distance_to_indexed_lines( + sd.lines, sd.tree, p_d, line_idx, hit_point); + assert(distance_sq > 0); + + // IMPROVE: one could use line ratio to find closest point + return get_closest_point_index(sd, line_idx, hit_point, shapes, s2i); +} + +std::pair priv::find_closest_point_pair( + const ExPolygons &shapes, + const std::vector &done_shapes, + const ExPolygonsIndices &s2i, + const std::vector &mask) +{ + assert(mask.size() == s2i.get_count()); + assert(done_shapes.size() == shapes.size()); + std::vector unfinished_mask = mask; // copy + + size_t index = 0; + for (size_t shape_index = 0; shape_index < shapes.size(); shape_index++) { + size_t count = count_points(shapes[shape_index]); + if (done_shapes[shape_index]) { + for (size_t i = 0; i < count; ++i, ++index) + unfinished_mask[index] = false; + } else { + index += count; + } + } + assert(index == s2i.get_count()); + SearchData sd = create_search_data(shapes, unfinished_mask); + + struct ClosestPair + { + size_t finish_idx = std::numeric_limits::max(); + size_t unfinished_line_idx = std::numeric_limits::max(); + Vec2d hit_point = Vec2d(); + double distance_sq = std::numeric_limits::max(); + } cp; + + index = 0; + for (size_t shape_index = 0; shape_index < shapes.size(); shape_index++) { + const ExPolygon shape = shapes[shape_index]; + if (!done_shapes[shape_index]) { + index += count_points(shape); + continue; + } + + auto search_in_polygon = [&index, &cp, &sd, &mask](const Polygon& polygon) { + for (size_t i = 0; i < polygon.size(); ++i, ++index) { + if (mask[index] == false) continue; + Vec2d p_d = polygon[i].cast(); + size_t line_idx = std::numeric_limits::max(); + Vec2d hit_point; + double distance_sq = AABBTreeLines::squared_distance_to_indexed_lines( + sd.lines, sd.tree, p_d, line_idx, hit_point, cp.distance_sq); + if (distance_sq < 0 || + distance_sq >= cp.distance_sq) continue; + assert(line_idx < sd.lines.size()); + cp.distance_sq = distance_sq; + cp.unfinished_line_idx = line_idx; + cp.hit_point = hit_point; + cp.finish_idx = index; + } + }; + search_in_polygon(shape.contour); + for (const Polygon& hole: shape.holes) + search_in_polygon(hole); + } + assert(index == s2i.get_count()); + // check that exists result + if (cp.finish_idx == std::numeric_limits::max()) { + return std::make_pair(std::numeric_limits::max(), + std::numeric_limits::max()); + } + + size_t unfinished_idx = get_closest_point_index(sd, cp.unfinished_line_idx, cp.hit_point, shapes, s2i); + return std::make_pair(cp.finish_idx, unfinished_idx); +} + +const priv::ProjectionDistance *priv::get_closest_projection( + const ProjectionDistances &distance, float wanted_distance) +{ + // minimal distance + float min_d = std::numeric_limits::max(); + const ProjectionDistance *min_pd = nullptr; + for (const ProjectionDistance &pd : distance) { + float d = std::fabs(pd.distance - wanted_distance); + // There should be limit for maximal distance + if (min_d > d) { + min_d = d; + min_pd = &pd; + } + } + return min_pd; +} + +void priv::fill_polygon_distances(const ProjectionDistance &pd, + uint32_t index, + const ExPolygonsIndex &id, + ProjectionDistances &result, + const ExPolygon &shape, + const VDistances &distances) +{ + const Points& points = (id.polygon_index == 0) ? + shape.contour.points : + shape.holes[id.polygon_index - 1].points; + // border of indexes for Polygon + uint32_t first_index = index - id.point_index; + uint32_t last_index = first_index + points.size(); + + uint32_t act_index = index; + const ProjectionDistance* act_pd = &pd; + + // Copy starting pd to result + result[act_index] = pd; + + auto exist_next = [&distances, &act_index, &act_pd, &result] + (uint32_t nxt_index) { + const ProjectionDistance *nxt_pd = get_closest_projection(distances[nxt_index] ,act_pd->distance); + // exist next projection distance ? + if (nxt_pd == nullptr) return false; + + // check no rewrite result + assert(result[nxt_index].aoi_index == std::numeric_limits::max()); + // copy founded projection to result + result[nxt_index] = *nxt_pd; // copy + + // next + act_index = nxt_index; + act_pd = &result[nxt_index]; + return true; + }; + + // last index in circle + uint32_t finish_index = (index == first_index) ? (last_index - 1) : + (index - 1); + // Positive iteration inside polygon + do { + uint32_t nxt_index = act_index + 1; + // close loop of indexes inside of contour + if (nxt_index == last_index) nxt_index = first_index; + // check that exist next + if (!exist_next(nxt_index)) break; + } while (act_index != finish_index); + + // when all results for polygon are set no neccessary to iterate negative + if (act_index == finish_index) return; + + act_index = index; + act_pd = &pd; + // Negative iteration inside polygon + do { + uint32_t nxt_index = (act_index == first_index) ? + (last_index-1) : (act_index - 1); + // When iterate negative it must be split to parts + // and can't iterate in circle + assert(nxt_index != index); + // check that exist next + if (!exist_next(nxt_index)) break; + } while (true); +} + +// IMPROVE: when select distance fill in all distances from Patch +void priv::fill_shape_distances(uint32_t start_index, + const ProjectionDistance *start_pd, + ProjectionDistances &result, + const ExPolygonsIndices &s2i, + const ExPolygon &shape, + const VDistances &distances) +{ + uint32_t expolygons_index = s2i.cvt(start_index).expolygons_index; + uint32_t first_shape_index = s2i.cvt({expolygons_index, 0, 0}); + do { + fill_polygon_distances(*start_pd, start_index, s2i.cvt(start_index),result, shape, distances); + // seaching only inside shape, return index of closed finished point + auto find_close_finished_point = [&first_shape_index, &shape, &result] + (const Point &p) -> ClosePoint { + uint32_t index = first_shape_index; + ClosePoint cp; + auto check_finished_points = [&cp, &result, &index, &p] + (const Points& pts) { + for (const Point &p_ : pts) { + // finished point with some distances + if (result[index].aoi_index == std::numeric_limits::max()) { + ++index; + continue; + } + float distance = calc_size_sq(p_ - p); + if (cp.dist_sq > distance) { + cp.dist_sq = distance; + cp.index = index; + } + ++index; + } + }; + check_finished_points(shape.contour.points); + for (const Polygon &h : shape.holes) + check_finished_points(h.points); + return cp; + }; + + // find next closest pair of points + // (finished + unfinished) in ExPolygon + start_index = std::numeric_limits::max(); // unfinished_index + uint32_t finished_index = std::numeric_limits::max(); + float dist_sq = std::numeric_limits::max(); + + // first index in shape + uint32_t index = first_shape_index; + auto check_unfinished_points = [&index, &result, &distances, &find_close_finished_point, &dist_sq, &start_index, &finished_index] + (const Points& pts) { + for (const Point &p : pts) { + // try find unfinished + if (result[index].aoi_index != + std::numeric_limits::max() || + distances[index].empty()) { + ++index; + continue; + } + ClosePoint cp = find_close_finished_point(p); + if (dist_sq > cp.dist_sq) { + dist_sq = cp.dist_sq; + start_index = index; + finished_index = cp.index; + } + ++index; + } + }; + // for each unfinished points + check_unfinished_points(shape.contour.points); + for (const Polygon &h : shape.holes) + check_unfinished_points(h.points); + } while (start_index != std::numeric_limits::max()); +} + +priv::ClosePoint priv::find_close_point(const Point &p, + ProjectionDistances &result, + std::vector &finished_shapes, + const ExPolygonsIndices &s2i, + const ExPolygons &shapes) +{ + // result + ClosePoint cp; + // for all finished points + for (uint32_t shape_index = 0; shape_index < shapes.size(); ++shape_index) { + if (!finished_shapes[shape_index]) continue; + const ExPolygon &shape = shapes[shape_index]; + uint32_t index = s2i.cvt({shape_index, 0, 0}); + auto find_close_point_in_points = [&p, &cp, &index, &result] + (const Points &pts){ + for (const Point &p_ : pts) { + // Exist result (is finished) ? + if (result[index].aoi_index == + std::numeric_limits::max()) { + ++index; + continue; + } + float distance_sq = calc_size_sq(p - p_); + if (cp.dist_sq > distance_sq) { + cp.dist_sq = distance_sq; + cp.index = index; + } + ++index; + } + }; + find_close_point_in_points(shape.contour.points); + // shape could be inside of another shape's hole + for (const Polygon& h:shape.holes) + find_close_point_in_points(h.points); + } + return cp; +} + +// IMPROVE: when select distance fill in all distances from Patch +priv::ProjectionDistances priv::choose_best_distance( + const VDistances &distances, const ExPolygons &shapes, const Point &start, const ExPolygonsIndices &s2i, const SurfacePatches &patches) +{ + assert(distances.size() == count_points(shapes)); + + // vector of patches for shape + std::vector> shapes_patches(shapes.size()); + for (const SurfacePatch &patch : patches) + shapes_patches[patch.shape_id].push_back(&patch-&patches.front()); + + // collect one closest projection for each outline point + ProjectionDistances result(distances.size()); + + // store info about finished shapes + std::vector finished_shapes(shapes.size(), {false}); + + // wanted distance from ideal projection + // Distances are relative to projection distance + // so first wanted distance is the closest one (ZERO) + float wanted_distance = 0.f; + + std::vector mask_distances(s2i.get_count(), {true}); + for (const auto &d : distances) + if (d.empty()) mask_distances[&d - &distances.front()] = false; + + // Select point from shapes(text contour) which is closest to center (all in 2d) + uint32_t unfinished_index = find_closest_point_index(start, shapes, s2i, mask_distances); + assert(unfinished_index < s2i.get_count()); + if (unfinished_index >= s2i.get_count()) + // no point to select + return result; + +#ifdef DEBUG_OUTPUT_DIR + Connections connections; + connections.reserve(shapes.size()); + connections.emplace_back(unfinished_index, unfinished_index); +#endif // DEBUG_OUTPUT_DIR + + do { + const ProjectionDistance* pd = get_closest_projection(distances[unfinished_index], wanted_distance); + // selection of closest_id should proove that pd has value + // (functions: get_closest_point_index and find_close_point_in_points) + assert(pd != nullptr); + uint32_t expolygons_index = s2i.cvt(unfinished_index).expolygons_index; + const ExPolygon &shape = shapes[expolygons_index]; + std::vector &shape_patches = shapes_patches[expolygons_index]; + if (shape_patches.size() == 1){ + // Speed up, only one patch so copy distance from patch + uint32_t first_shape_index = s2i.cvt({expolygons_index, 0, 0}); + uint32_t laset_shape_index = first_shape_index + count_points(shape); + for (uint32_t i = first_shape_index; i < laset_shape_index; ++i) { + const ProjectionDistances &pds = distances[i]; + if (pds.empty()) continue; + // check that index belongs to patch + assert(pds.front().patch_index == shape_patches.front()); + result[i] = pds.front(); + if (pds.size() == 1) continue; + + float relative_distance = fabs(result[i].distance - pd->distance); + // patch could contain multiple value for one outline point + // so choose closest to start point + for (uint32_t pds_index = 1; pds_index < pds.size(); ++pds_index) { + // check that index still belongs to same patch + assert(pds[pds_index].patch_index == shape_patches.front()); + float relative_distance2 = fabs(pds[pds_index].distance - pd->distance); + if (relative_distance > relative_distance2) { + relative_distance = relative_distance2; + result[i] = pds[pds_index]; + } + } + } + } else { + // multiple patches for expolygon + // check that exist patch to fill shape + assert(!shape_patches.empty()); + fill_shape_distances(unfinished_index, pd, result, s2i, shape, distances); + } + + finished_shapes[expolygons_index] = true; + // The most close points between finished and unfinished shapes + auto [finished, unfinished] = find_closest_point_pair( + shapes, finished_shapes, s2i, mask_distances); + + // detection of end (best doesn't have value) + if (finished == std::numeric_limits::max()) break; + + assert(unfinished != std::numeric_limits::max()); + const ProjectionDistance &closest_pd = result[finished]; + // check that best_cp is finished and has result + assert(closest_pd.aoi_index != std::numeric_limits::max()); + wanted_distance = closest_pd.distance; + unfinished_index = unfinished; + +#ifdef DEBUG_OUTPUT_DIR + connections.emplace_back(finished, unfinished); +#endif // DEBUG_OUTPUT_DIR + } while (true); //(unfinished_index != std::numeric_limits::max()); +#ifdef DEBUG_OUTPUT_DIR + store(shapes, mask_distances, connections, DEBUG_OUTPUT_DIR + "closest_points.svg"); +#endif // DEBUG_OUTPUT_DIR + return result; +} + +// functions to help 'diff_model' +namespace priv { +const VI default_vi(std::numeric_limits::max()); + +// Keep info about intersection source +struct Source{ HI hi; int sdim=0;}; +using Sources = std::vector; +const std::string vertex_source_map_name = "v:SourceIntersecting"; +using VertexSourceMap = CutMesh::Property_map; + +/// +/// Corefine visitor +/// Store intersection source for vertices of constrained edge of tm1 +/// Must be used with corefine flag no modification of tm2 +/// +struct IntersectionSources +{ + const CutMesh *patch; // patch + const CutMesh *model; // const model + + VertexSourceMap vmap; + + // keep sources from call intersection_point_detected + // until call new_vertex_added + Sources* sources; + + // count intersections + void intersection_point_detected(std::size_t i_id, + int sdim, + HI h_f, + HI h_e, + const CutMesh &tm_f, + const CutMesh &tm_e, + bool is_target_coplanar, + bool is_source_coplanar) + { + Source source; + if (&tm_e == model) { + source = {h_e, sdim}; + // check other CGAL model that is patch + assert(&tm_f == patch); + if (is_target_coplanar) { + assert(sdim == 0); + vmap[tm_f.source(h_f)] = source; + } + if (is_source_coplanar) { + assert(sdim == 0); + vmap[tm_f.target(h_f)] = source; + } + + // clear source to be able check that this intersection source is + // not used any more + if (is_source_coplanar || is_target_coplanar) source = {}; + } else { + source = {h_f, sdim}; + assert(&tm_f == model && &tm_e == patch); + assert(!is_target_coplanar); + assert(!is_source_coplanar); + // if (is_target_coplanar) vmap[tm_e.source(h_e)] = source; + // if (is_source_coplanar) vmap[tm_e.target(h_e)] = source; + // if (sdim == 0) + // vmap[tm_e.target(h_e)] = source; + } + + // By documentation i_id is consecutive. + // check id goes in a row, without skips + assert(sources->size() == i_id); + // add source of intersection + sources->push_back(source); + } + + /// + /// Store VI to intersections by i_id + /// + /// Order number of intersection point + /// New added vertex + /// Affected mesh + void new_vertex_added(std::size_t i_id, VI v, const CutMesh &tm) + { + // check that it is first insertation into item of vmap + assert(!vmap[v].hi.is_valid()); + // check valid addresing into sources + assert(i_id < sources->size()); + // check that source has value + assert(sources->at(i_id).hi.is_valid()); + vmap[v] = sources->at(i_id); + } + + // Not used visitor functions + void before_subface_creations(FI /* f_old */, CutMesh & /* mesh */) {} + void after_subface_created(FI /* f_new */, CutMesh & /* mesh */) {} + void after_subface_creations(CutMesh &) {} + void before_subface_created(CutMesh &) {} + void before_edge_split(HI /* h */, CutMesh & /* tm */) {} + void edge_split(HI /* hnew */, CutMesh & /* tm */) {} + void after_edge_split() {} + void add_retriangulation_edge(HI /* h */, CutMesh & /* tm */) {} +}; + +/// +/// Create map1 and map2 +/// +/// Convert tm1.face to type +/// Corefined mesh +/// Source of intersection +/// Identify constrainde edge +/// Convert tm1.face to type +void create_face_types(FaceTypeMap &map, + const CutMesh &tm1, + const CutMesh &tm2, + const EdgeBoolMap &ecm, + const VertexSourceMap &sources); + +/// +/// Implement 'cut' Minus 'clipper', where clipper is reverse input Volume +/// NOTE: clipper will be modified (corefined by cut) !!! +/// +/// differ from +/// differ what +/// True on succes, otherwise FALSE +bool clip_cut(SurfacePatch &cut, CutMesh clipper); + +BoundingBoxf3 bounding_box(const CutAOI &cut, const CutMesh &mesh); +BoundingBoxf3 bounding_box(const CutMesh &mesh); +BoundingBoxf3 bounding_box(const SurfacePatch &ecut); + +/// +/// Create patch +/// +/// Define patch faces +/// Source of fis +/// NOTE: Need temporary add property map for convert vertices +/// Options to reduce vertices from fis. +/// NOTE: Used for skip vertices made by diagonal edge in rectangle of shape side +/// Patch +SurfacePatch create_surface_patch(const std::vector &fis, + /*const*/ CutMesh &mesh, + const ReductionMap *rmap = nullptr); + +} // namespace priv + +void priv::create_face_types(FaceTypeMap &map, + const CutMesh &tm1, + const CutMesh &tm2, + const EdgeBoolMap &ecm, + const VertexSourceMap &sources) +{ + auto get_intersection_source = [&tm2](const Source& s1, const Source& s2)->FI{ + // when one of sources is face than return it + FI fi1 = tm2.face(s1.hi); + if (s1.sdim == 2) return fi1; + FI fi2 = tm2.face(s2.hi); + if (s2.sdim == 2) return fi2; + // both vertices are made by same source triangle + if (fi1 == fi2) return fi1; + + // when one from sources is edge second one decide side of triangle triangle + HI hi1_opposit = tm2.opposite(s1.hi); + FI fi1_opposit; + if (hi1_opposit.is_valid()) + fi1_opposit = tm2.face(hi1_opposit); + if (fi2 == fi1_opposit) return fi2; + + HI hi2_opposit = tm2.opposite(s2.hi); + FI fi2_opposit; + if (hi2_opposit.is_valid()) + fi2_opposit = tm2.face(hi2_opposit); + if (fi1 == fi2_opposit) return fi1; + if (fi1_opposit.is_valid() && fi1_opposit == fi2_opposit) + return fi1_opposit; + + // when intersection is vertex need loop over neighbor + for (FI fi_around_hi1 : tm2.faces_around_target(s1.hi)) { + for (FI fi_around_hi2 : tm2.faces_around_target(s2.hi)) { + if (fi_around_hi1 == fi_around_hi2) + return fi_around_hi1; + } + } + + // should never rich it + // Exist case when do not know source triangle for decide side of intersection + assert(false); + return FI(); + }; + + for (FI fi : tm1.faces()) map[fi] = FaceType::not_constrained; + for (EI ei1 : tm1.edges()) { + if (!get(ecm, ei1)) continue; + + // get faces from tm1 (f1a + f1b) + HI hi1 = tm1.halfedge(ei1); + assert(hi1.is_valid()); + FI f1a = tm1.face(hi1); + assert(f1a.is_valid()); + HI hi_op = tm1.opposite(hi1); + assert(hi_op.is_valid()); + FI f1b = tm1.face(hi_op); + assert(f1b.is_valid()); + + // get faces from tm2 (f2a + f2b) + VI vi1_source = tm1.source(hi1); + assert(vi1_source.is_valid()); + VI vi1_target = tm1.target(hi1); + assert(vi1_target.is_valid()); + + const Source &s_s = sources[vi1_source]; + const Source &s_t = sources[vi1_target]; + FI fi2 = get_intersection_source(s_s, s_t); + + // in release solve situation that face was NOT deduced + if (!fi2.is_valid()) continue; + + HI hi2 = tm2.halfedge(fi2); + std::array t; + size_t ti =0; + for (VI vi2 : tm2.vertices_around_face(hi2)) + t[ti++] = &tm2.point(vi2); + + // triangle tip from face f1a + VI vi1a_tip = tm1.target(tm1.next(hi1)); + assert(vi1a_tip.is_valid()); + const P3 &p = tm1.point(vi1a_tip); + + // check if f1a is behinde f2a + // inside mean it will be used + // outside will be discarded + if (CGAL::orientation(*t[0], *t[1], *t[2], p) == CGAL::POSITIVE) { + map[f1a] = FaceType::inside; + map[f1b] = FaceType::outside; + } else { + map[f1a] = FaceType::outside; + map[f1b] = FaceType::inside; + } + } +} + +#include +#include +bool priv::clip_cut(SurfacePatch &cut, CutMesh clipper) +{ + CutMesh& tm = cut.mesh; + // create backup for case that there is no intersection + CutMesh backup_copy = tm; + + class ExistIntersectionClipVisitor: public CGAL::Polygon_mesh_processing::Corefinement::Default_visitor + { + bool* exist_intersection; + public: + ExistIntersectionClipVisitor(bool *exist_intersection): exist_intersection(exist_intersection){} + void intersection_point_detected(std::size_t, int , HI, HI, const CutMesh&, const CutMesh&, bool, bool) + { *exist_intersection = true;} + }; + bool exist_intersection = false; + ExistIntersectionClipVisitor visitor{&exist_intersection}; + + // namep parameters for model tm and function clip + const auto &np_tm = CGAL::parameters::visitor(visitor) + .throw_on_self_intersection(false); + + // name parameters for model clipper and function clip + const auto &np_c = CGAL::parameters::throw_on_self_intersection(false); + // Can't use 'do_not_modify', when Ture than clipper has to be closed !! + // .do_not_modify(true); + // .throw_on_self_intersection(false); is set automaticaly by param 'do_not_modify' + // .clip_volume(false); is set automaticaly by param 'do_not_modify' + + bool suc = CGAL::Polygon_mesh_processing::clip(tm, clipper, np_tm, np_c); + + // true if the output surface mesh is manifold. + // If false is returned tm and clipper are only corefined. + assert(suc); + // decide what TODO when can't clip source object !?! + if (!exist_intersection || !suc) { + // TODO: test if cut is fully in or fully out!! + cut.mesh = backup_copy; + return false; + } + return true; +} + +BoundingBoxf3 priv::bounding_box(const CutAOI &cut, const CutMesh &mesh) { + const P3& p_from_cut = mesh.point(mesh.target(mesh.halfedge(cut.first.front()))); + Vec3d min = to_vec3d(p_from_cut); + Vec3d max = min; + for (FI fi : cut.first) { + for(VI vi: mesh.vertices_around_face(mesh.halfedge(fi))){ + const P3& p = mesh.point(vi); + for (size_t i = 0; i < 3; ++i) { + if (min[i] > p[i]) min[i] = p[i]; + if (max[i] < p[i]) max[i] = p[i]; + } + } + } + return BoundingBoxf3(min, max); +} + +BoundingBoxf3 priv::bounding_box(const CutMesh &mesh) +{ + Vec3d min = to_vec3d(*mesh.points().begin()); + Vec3d max = min; + for (VI vi : mesh.vertices()) { + const P3 &p = mesh.point(vi); + for (size_t i = 0; i < 3; ++i) { + if (min[i] > p[i]) min[i] = p[i]; + if (max[i] < p[i]) max[i] = p[i]; + } + } + return BoundingBoxf3(min, max); +} + +BoundingBoxf3 priv::bounding_box(const SurfacePatch &ecut) { + return bounding_box(ecut.mesh); +} + +priv::SurfacePatch priv::create_surface_patch(const std::vector &fis, + /* const */ CutMesh &mesh, + const ReductionMap *rmap) +{ + auto is_counted = mesh.add_property_map("v:is_counted").first; + uint32_t count_vertices = 0; + if (rmap == nullptr) { + for (FI fi : fis) + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) + if (!is_counted[vi]) { + is_counted[vi] = true; + ++count_vertices; + } + } else { + for (FI fi : fis) + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) { + // Will vertex be reduced? + if ((*rmap)[vi].is_valid()) continue; + if (!is_counted[vi]) { + is_counted[vi] = true; + ++count_vertices; + } + } + } + mesh.remove_property_map(is_counted); + + uint32_t count_faces = fis.size(); + // IMPROVE: Value is greater than neccessary, count edges used twice + uint32_t count_edges = count_faces*3; + + CutMesh cm; + cm.reserve(count_vertices, count_edges, count_faces); + + // vertex conversion function from mesh VI to result VI + CvtVI2VI mesh2result = mesh.add_property_map("v:mesh2result").first; + + if (rmap == nullptr) { + for (FI fi : fis) { + std::array t; + int index = 0; + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) { + VI &vi_cvt = mesh2result[vi]; + if (!vi_cvt.is_valid()) { + vi_cvt = VI(cm.vertices().size()); + cm.add_vertex(mesh.point(vi)); + } + t[index++] = vi_cvt; + } + cm.add_face(t[0], t[1], t[2]); + } + } else { + for (FI fi :fis) { + std::array t; + int index = 0; + bool exist_reduction = false; + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) { + VI vi_r = (*rmap)[vi]; + if (vi_r.is_valid()) { + exist_reduction = true; + vi = vi_r; + } + VI &vi_cvt = mesh2result[vi]; + if (!vi_cvt.is_valid()) { + vi_cvt = VI(cm.vertices().size()); + cm.add_vertex(mesh.point(vi)); + } + t[index++] = vi_cvt; + } + + // prevent add reduced triangle + if (exist_reduction && + (t[0] == t[1] || + t[1] == t[2] || + t[2] == t[0])) + continue; + + cm.add_face(t[0], t[1], t[2]); + } + } + + assert(count_vertices == cm.vertices().size()); + assert((rmap == nullptr && count_faces == cm.faces().size()) || + (rmap != nullptr && count_faces >= cm.faces().size())); + assert(count_edges >= cm.edges().size()); + + // convert VI from this patch to source VI, when exist + CvtVI2VI cvt = cm.add_property_map(patch_source_name).first; + // vi_s .. VertexIndex into mesh (source) + // vi_d .. new VertexIndex in cm (destination) + for (VI vi_s : mesh.vertices()) { + VI vi_d = mesh2result[vi_s]; + if (!vi_d.is_valid()) continue; + cvt[vi_d] = vi_s; + } + mesh.remove_property_map(mesh2result); + return {std::move(cm)}; +} + +// diff_models help functions +namespace priv { + +struct SurfacePatchEx +{ + SurfacePatch patch; + + // flag that part will be deleted + bool full_inside = false; + // flag that Patch could contain more than one part + bool just_cliped = false; +}; +using SurfacePatchesEx = std::vector; + + +using BBS = std::vector; +/// +/// Create bounding boxes for AOI +/// +/// Cutted AOI from models +/// Source points of cuts +/// Bounding boxes +BBS create_bbs(const VCutAOIs &cuts, const CutMeshes &cut_models); + +using Primitive = CGAL::AABB_face_graph_triangle_primitive; +using Traits = CGAL::AABB_traits; +using Ray = EpicKernel::Ray_3; +using Tree = CGAL::AABB_tree; +using Trees = std::vector; +/// +/// Create AABB trees for check when patch is whole inside of model +/// +/// Source for trees +/// trees +Trees create_trees(const CutMeshes &models); + +/// +/// Check whether bounding box has intersection with model +/// +/// Bounding box to check +/// Model to check with +/// All bounding boxes from VCutAOIs +/// Help index into VCutAOIs +/// True when exist bounding boxes intersection +bool has_bb_intersection(const BoundingBoxf3 &bb, + size_t model_index, + const BBS &bbs, + const ModelCut2index &m2i); + +/// +/// Only for model without intersection +/// Use ray (in projection direction) from a point from patch +/// and count intersections: pair .. outside | odd .. inside +/// +/// Patch to check +/// Model converted to AABB tree +/// Define direction of projection +/// True when patch point lay inside of model defined by tree, +/// otherwise FALSE +bool is_patch_inside_of_model(const SurfacePatch &patch, + const Tree &tree, + const Project3d &projection); + +/// +/// Return some shape point index which identify shape +/// NOTE: Used to find expolygon index +/// +/// Used to search source shapes poin +/// +/// shape point index +uint32_t get_shape_point_index(const CutAOI &cut, const CutMesh &model); + +using PatchNumber = CutMesh::Property_map; +/// +/// Separate triangles singned with number n +/// +/// Face indices owned by separate patch +/// Original patch +/// NOTE: Can't be const. For indexing vetices need temporary add property map +/// conversion map +/// Just separated patch +SurfacePatch separate_patch(const std::vector &fis, + /* const*/ SurfacePatch &patch, + const CvtVI2VI &cvt_from); + +/// +/// Separate connected triangles into it's own patches +/// new patches are added to back of input patches +/// +/// index into patches +/// In/Out Patches +void divide_patch(size_t i, SurfacePatchesEx &patches); + +/// +/// Fill outline in patches by open edges +/// +/// Input/Output meshes with open edges +void collect_open_edges(SurfacePatches &patches); + +} // namespace priv + +std::vector priv::create_bbs(const VCutAOIs &cuts, + const CutMeshes &cut_models) +{ + size_t count = 0; + for (const CutAOIs &cut : cuts) count += cut.size(); + + std::vector bbs; + bbs.reserve(count); + for (size_t model_index = 0; model_index < cut_models.size(); ++model_index) { + const CutMesh &cut_model = cut_models[model_index]; + const CutAOIs &cutAOIs = cuts[model_index]; + for (size_t cut_index = 0; cut_index < cutAOIs.size(); ++cut_index) { + const CutAOI &cut = cutAOIs[cut_index]; + bbs.push_back(bounding_box(cut, cut_model)); + } + } + return bbs; +} + + +priv::Trees priv::create_trees(const CutMeshes &models) { + Trees result; + result.reserve(models.size()); + for (const CutMesh &model : models) { + Tree tree; + tree.insert(faces(model).first, faces(model).second, model); + tree.build(); + result.emplace_back(std::move(tree)); + } + return result; +} + +bool priv::has_bb_intersection(const BoundingBoxf3 &bb, + size_t model_index, + const BBS &bbs, + const ModelCut2index &m2i) +{ + const auto&offsets = m2i.get_offsets(); + // for cut index with model_index2 + size_t start = offsets[model_index]; + size_t next = model_index + 1; + size_t end = (next < offsets.size()) ? offsets[next] : m2i.get_count(); + for (size_t bb_index = start; bb_index < end; bb_index++) + if (bb.intersects(bbs[bb_index])) return true; + return false; +} + +bool priv::is_patch_inside_of_model(const SurfacePatch &patch, + const Tree &tree, + const Project3d &projection) +{ + // TODO: Solve model with hole in projection direction !!! + const P3 &a = patch.mesh.point(VI(0)); + Vec3d a_ = to_vec3d(a); + Vec3d b_ = projection.project(a_); + P3 b(b_.x(), b_.y(), b_.z()); + + Ray ray_query(a, b); + size_t count = tree.number_of_intersected_primitives(ray_query); + bool is_in = (count % 2) == 1; + + // try opposit direction result should be same, otherwise open model is used + //Vec3f c_ = a_ - (b_ - a_); // opposit direction + //P3 c(c_.x(), c_.y(), c_.z()); + //Ray ray_query2(a, b); + //size_t count2 = tree.number_of_intersected_primitives(ray_query2); + //bool is_in2 = (count2 % 2) == 1; + assert(((tree.number_of_intersected_primitives( + Ray(a, P3(2 * a.x() - b.x(), + 2 * a.y() - b.y(), + 2 * a.z() - b.z()))) % + 2) == 1) == is_in); + return is_in; +} + +uint32_t priv::get_shape_point_index(const CutAOI &cut, const CutMesh &model) +{ + // map is created during intersection by corefine visitor + const VertexShapeMap &vert_shape_map = model.property_map(vert_shape_map_name).first; + // for each half edge of outline + for (HI hi : cut.second) { + VI vi = model.source(hi); + const IntersectingElement *ie = vert_shape_map[vi]; + if (ie == nullptr) continue; + assert(ie->shape_point_index != std::numeric_limits::max()); + return ie->shape_point_index; + } + // can't found any intersecting element in cut + assert(false); + return 0; +} + +priv::SurfacePatch priv::separate_patch(const std::vector& fis, + SurfacePatch &patch, + const CvtVI2VI &cvt_from) +{ + assert(patch.mesh.is_valid()); + SurfacePatch patch_new = create_surface_patch(fis, patch.mesh); + patch_new.bb = bounding_box(patch_new.mesh); + patch_new.aoi_id = patch.aoi_id; + patch_new.model_id = patch.model_id; + patch_new.shape_id = patch.shape_id; + // fix cvt + CvtVI2VI cvt = patch_new.mesh.property_map(patch_source_name).first; + for (VI &vi : cvt) { + if (!vi.is_valid()) continue; + vi = cvt_from[vi]; + } + return patch_new; +} + +void priv::divide_patch(size_t i, SurfacePatchesEx &patches) +{ + SurfacePatchEx &patch_ex = patches[i]; + assert(patch_ex.just_cliped); + patch_ex.just_cliped = false; + + SurfacePatch& patch = patch_ex.patch; + CutMesh& cm = patch.mesh; + assert(!cm.faces().empty()); + std::string patch_number_name = "f:patch_number"; + CutMesh::Property_map is_processed = cm.add_property_map(patch_number_name, false).first; + + const CvtVI2VI& cvt_from = patch.mesh.property_map(patch_source_name).first; + + std::vector fis; + fis.reserve(cm.faces().size()); + + SurfacePatchesEx new_patches; + std::vector queue; + // IMPROVE: create groups around triangles and than connect groups + for (FI fi_cm : cm.faces()) { + if (is_processed[fi_cm]) continue; + assert(queue.empty()); + queue.push_back(fi_cm); + if (!fis.empty()) { + // Be carefull after push to patches, + // all ref on patch contain non valid values + SurfacePatchEx patch_ex_n; + patch_ex_n.patch = separate_patch(fis, patch, cvt_from); + patch_ex_n.patch.is_whole_aoi = false; + new_patches.push_back(std::move(patch_ex_n)); + fis.clear(); + } + // flood fill from triangle fi_cm to surrounding + do { + FI fi_q = queue.back(); + queue.pop_back(); + if (is_processed[fi_q]) continue; + is_processed[fi_q] = true; + fis.push_back(fi_q); + HI hi = cm.halfedge(fi_q); + for (FI fi : cm.faces_around_face(hi)) { + // by documentation The face descriptor may be the null face, and it may be several times the same face descriptor. + if (!fi.is_valid()) continue; + if (!is_processed[fi]) queue.push_back(fi); + } + } while (!queue.empty()); + } + cm.remove_property_map(is_processed); + assert(!fis.empty()); + + // speed up for only one patch - no dividing (the most common) + if (new_patches.empty()) { + patch.bb = bounding_box(cm); + patch.is_whole_aoi = false; + } else { + patch = separate_patch(fis, patch, cvt_from); + patches.insert(patches.end(), new_patches.begin(), new_patches.end()); + } +} + +void priv::collect_open_edges(SurfacePatches &patches) { + std::vector open_half_edges; + for (SurfacePatch &patch : patches) { + open_half_edges.clear(); + const CutMesh &mesh = patch.mesh; + for (FI fi : mesh.faces()) { + HI hi1 = mesh.halfedge(fi); + assert(hi1.is_valid()); + HI hi2 = mesh.next(hi1); + assert(hi2.is_valid()); + HI hi3 = mesh.next(hi2); + assert(hi3.is_valid()); + // Is fi triangle? + assert(mesh.next(hi3) == hi1); + for (HI hi : {hi1, hi2, hi3}) { + HI hi_op = mesh.opposite(hi); + FI fi_op = mesh.face(hi_op); + if (!fi_op.is_valid()) + open_half_edges.push_back(hi); + } + } + patch.loops = create_loops(open_half_edges, mesh); + } +} + +priv::SurfacePatches priv::diff_models(VCutAOIs &cuts, + /*const*/ CutMeshes &cut_models, + /*const*/ CutMeshes &models, + const Project3d &projection) +{ + // IMPROVE: when models contain ONE mesh. It is only about convert cuts to patches + // and reduce unneccessary triangles on contour + + //Convert model_index and cut_index into one index + priv::ModelCut2index m2i(cuts); + + // create bounding boxes for cuts + std::vector bbs = create_bbs(cuts, cut_models); + Trees trees(models.size()); + + SurfacePatches patches; + + // queue of patches for one AOI (permanent with respect to for loop) + SurfacePatchesEx aoi_patches; + + //SurfacePatches aoi_patches; + patches.reserve(m2i.get_count()); // only approximation of count + size_t index = 0; + for (size_t model_index = 0; model_index < models.size(); ++model_index) { + CutAOIs &model_cuts = cuts[model_index]; + CutMesh &cut_model_ = cut_models[model_index]; + const CutMesh &cut_model = cut_model_; + ReductionMap vertex_reduction_map = cut_model_.add_property_map(vertex_reduction_map_name).first; + create_reduce_map(vertex_reduction_map, cut_model); + + for (size_t cut_index = 0; cut_index < model_cuts.size(); ++cut_index, ++index) { + const CutAOI &cut = model_cuts[cut_index]; + SurfacePatchEx patch_ex; + SurfacePatch &patch = patch_ex.patch; + patch = create_surface_patch(cut.first, cut_model_, &vertex_reduction_map); + patch.bb = bbs[index]; + patch.aoi_id = cut_index; + patch.model_id = model_index; + patch.shape_id = get_shape_point_index(cut, cut_model); + patch.is_whole_aoi = true; + + aoi_patches.clear(); + aoi_patches.push_back(patch_ex); + for (size_t model_index2 = 0; model_index2 < models.size(); ++model_index2) { + // do not clip source model itself + if (model_index == model_index2) continue; + for (SurfacePatchEx &patch_ex : aoi_patches) { + SurfacePatch &patch = patch_ex.patch; + if (has_bb_intersection(patch.bb, model_index2, bbs, m2i) && + clip_cut(patch, models[model_index2])){ + patch_ex.just_cliped = true; + } else { + // build tree on demand + // NOTE: it is possible not neccessary: e.g. one model + Tree &tree = trees[model_index2]; + if (tree.empty()) { + const CutMesh &model = models[model_index2]; + auto f_range = faces(model); + tree.insert(f_range.first, f_range.second, model); + tree.build(); + } + if (is_patch_inside_of_model(patch, tree, projection)) + patch_ex.full_inside = true; + } + } + // erase full inside + for (size_t i = aoi_patches.size(); i != 0; --i) { + auto it = aoi_patches.begin() + (i - 1); + if (it->full_inside) aoi_patches.erase(it); + } + + // detection of full AOI inside of model + if (aoi_patches.empty()) break; + + // divide cliped into parts + size_t end = aoi_patches.size(); + for (size_t i = 0; i < end; ++i) + if (aoi_patches[i].just_cliped) + divide_patch(i, aoi_patches); + } + + if (!aoi_patches.empty()) { + patches.reserve(patches.size() + aoi_patches.size()); + for (SurfacePatchEx &patch : aoi_patches) + patches.push_back(std::move(patch.patch)); + + } + } + cut_model_.remove_property_map(vertex_reduction_map); + } + + // Also use outline inside of patches(made by non manifold models) + // IMPROVE: trace outline from AOIs + collect_open_edges(patches); + return patches; +} + +bool priv::is_over_whole_expoly(const SurfacePatch &patch, + const ExPolygons &shapes, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes) +{ + if (!patch.is_whole_aoi) return false; + return is_over_whole_expoly(cutAOIs[patch.model_id][patch.aoi_id], + shapes[patch.shape_id], + meshes[patch.model_id]); +} + +bool priv::is_over_whole_expoly(const CutAOI &cutAOI, + const ExPolygon &shape, + const CutMesh &mesh) +{ + // NonInterupted contour is without other point and contain all from shape + const VertexShapeMap &vert_shape_map = mesh.property_map(vert_shape_map_name).first; + for (HI hi : cutAOI.second) { + const IntersectingElement *ie_s = vert_shape_map[mesh.source(hi)]; + const IntersectingElement *ie_t = vert_shape_map[mesh.target(hi)]; + if (ie_s == nullptr || ie_t == nullptr) + return false; + + assert(ie_s->attr != (unsigned char) IntersectingElement::Type::undefined); + assert(ie_t->attr != (unsigned char) IntersectingElement::Type::undefined); + + // check if it is neighbor indices + uint32_t i_s = ie_s->shape_point_index; + uint32_t i_t = ie_t->shape_point_index; + assert(i_s != std::numeric_limits::max()); + assert(i_t != std::numeric_limits::max()); + if (i_s == std::numeric_limits::max() || + i_t == std::numeric_limits::max()) + return false; + + // made by same index + if (i_s == i_t) continue; + + // order from source to target + if (i_s > i_t) { + std::swap(i_s, i_t); + std::swap(ie_s, ie_t); + } + // Must be after fix order !! + bool is_last_polygon_segment = ie_s->is_first() && ie_t->is_last(); + if (is_last_polygon_segment) { + std::swap(i_s, i_t); + std::swap(ie_s, ie_t); + } + + // Is continous indices + if (!is_last_polygon_segment && + (ie_s->is_last() || (i_s + 1) != i_t)) + return false; + + IntersectingElement::Type t_s = ie_s->get_type(); + IntersectingElement::Type t_t = ie_t->get_type(); + if (t_s == IntersectingElement::Type::undefined || + t_t == IntersectingElement::Type::undefined) + return false; + + // next segment must start with edge intersection + if (t_t != IntersectingElement::Type::edge_1) + return false; + + // After face1 must be edge2 or face2 + if (t_s == IntersectingElement::Type::face_1) + return false; + } + + // When all open edges are on contour than there is NO holes is shape + auto is_open = [&mesh](HI hi)->bool { + HI opposite = mesh.opposite(hi); + return !mesh.face(opposite).is_valid(); + }; + + std::vector opens; // copy + opens.reserve(cutAOI.second.size()); + for (HI hi : cutAOI.second) // from lower to bigger + if (is_open(hi)) opens.push_back(hi); + std::sort(opens.begin(), opens.end()); + + for (FI fi: cutAOI.first) { + HI face_hi = mesh.halfedge(fi); + for (HI hi : mesh.halfedges_around_face(face_hi)) { + if (!is_open(hi)) continue; + // open edge + auto lb = std::lower_bound(opens.begin(), opens.end(), hi); + if (lb == opens.end() || *lb != hi) + return false; // not in contour + } + } + return true; +} + +std::vector priv::select_patches(const ProjectionDistances &best_distances, + const SurfacePatches &patches, + const ExPolygons &shapes, + const BoundingBox &shapes_bb, + const ExPolygonsIndices &s2i, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes, + const Project &projection) +{ + // extension to cover numerical mistake made by back projection patch from 3d to 2d + // Calculated as one percent of average size(width and height) + Point s = shapes_bb.size(); + const float extend_delta = (s.x() + s.y())/ float(2 * 100); + + // vector of patches for shape + std::vector> used_shapes_patches(shapes.size()); + std::vector in_distances(patches.size(), {false}); + for (const ProjectionDistance &d : best_distances) { + // exist valid projection for shape point? + if (d.patch_index == std::numeric_limits::max()) continue; + if (in_distances[d.patch_index]) continue; + in_distances[d.patch_index] = true; + + ExPolygonsIndex id = s2i.cvt(&d - &best_distances.front()); + used_shapes_patches[id.expolygons_index].push_back(d.patch_index); + } + + // vector of patches for shape + std::vector> shapes_patches(shapes.size()); + for (const SurfacePatch &patch : patches) + shapes_patches[patch.shape_id].push_back(&patch - &patches.front()); + +#ifdef DEBUG_OUTPUT_DIR + std::string store_dir = DEBUG_OUTPUT_DIR + "select_patches/"; + prepare_dir(store_dir); +#endif // DEBUG_OUTPUT_DIR + + for (size_t shape_index = 0; shape_index < shapes.size(); shape_index++) { + const ExPolygon &shape = shapes[shape_index]; + std::vector &used_shape_patches = used_shapes_patches[shape_index]; + if (used_shape_patches.empty()) continue; + // is used all exist patches? + if (used_shapes_patches.size() == shapes_patches[shape_index].size()) continue; + if (used_shape_patches.size() == 1) { + uint32_t patch_index = used_shape_patches.front(); + const SurfacePatch &patch = patches[patch_index]; + if (is_over_whole_expoly(patch, shapes, cutAOIs, meshes)) continue; + } + + // only shapes containing multiple patches + // or not full filled are back projected (hard processed) + + // intersection of converted patches to 2d + ExPolygons fill; + fill.reserve(used_shape_patches.size()); + + // Heuristics to predict which patch to be used need average patch depth + Vec2d used_patches_depth(std::numeric_limits::max(), std::numeric_limits::min()); + for (uint32_t patch_index : used_shape_patches) { + ExPolygon patch_area = to_expoly(patches[patch_index], projection, used_patches_depth); + //*/ + ExPolygons patch_areas = offset_ex(patch_area, extend_delta); + fill.insert(fill.end(), patch_areas.begin(), patch_areas.end()); + /*/ + // without save extension + fill.push_back(patch_area); + //*/ + } + fill = union_ex(fill); + + // not cutted area of expolygon + ExPolygons rest = diff_ex(ExPolygons{shape}, fill, ApplySafetyOffset::Yes); +#ifdef DEBUG_OUTPUT_DIR + BoundingBox shape_bb = get_extents(shape); + SVG svg(store_dir + "shape_" + std::to_string(shape_index) + ".svg", shape_bb); + svg.draw(fill, "darkgreen"); + svg.draw(rest, "green"); +#endif // DEBUG_OUTPUT_DIR + + // already filled by multiple patches + if (rest.empty()) continue; + + // find patches overlaped rest area + struct PatchShape{ + uint32_t patch_index; + ExPolygon shape; + ExPolygons intersection; + double depth_range_center_distance; // always positive + }; + using PatchShapes = std::vector; + PatchShapes patch_shapes; + + double used_patches_depth_center = (used_patches_depth[0] + used_patches_depth[1]) / 2; + + // sort used_patches for faster search + std::sort(used_shape_patches.begin(), used_shape_patches.end()); + for (uint32_t patch_index : shapes_patches[shape_index]) { + // check is patch already used + auto it = std::lower_bound(used_shape_patches.begin(), used_shape_patches.end(), patch_index); + if (it != used_shape_patches.end() && *it == patch_index) continue; + + // Heuristics to predict which patch to be used need average patch depth + Vec2d patche_depth_range(std::numeric_limits::max(), std::numeric_limits::min()); + ExPolygon patch_shape = to_expoly(patches[patch_index], projection, patche_depth_range); + double depth_center = (patche_depth_range[0] + patche_depth_range[1]) / 2; + double depth_range_center_distance = std::fabs(used_patches_depth_center - depth_center); + + ExPolygons patch_intersection = intersection_ex(ExPolygons{patch_shape}, rest); + if (patch_intersection.empty()) continue; + + patch_shapes.push_back({patch_index, patch_shape, patch_intersection, depth_range_center_distance}); + } + + // nothing to add + if (patch_shapes.empty()) continue; + // only one solution to add + if (patch_shapes.size() == 1) { + used_shape_patches.push_back(patch_shapes.front().patch_index); + continue; + } + + // Idea: Get depth range of used patches and add patches in order by distance to used depth center + std::sort(patch_shapes.begin(), patch_shapes.end(), [](const PatchShape &a, const PatchShape &b) + { return a.depth_range_center_distance < b.depth_range_center_distance; }); + +#ifdef DEBUG_OUTPUT_DIR + for (size_t i = patch_shapes.size(); i > 0; --i) { + const PatchShape &p = patch_shapes[i - 1]; + int gray_level = (i * 200) / patch_shapes.size(); + std::stringstream color; + color << "#" << std::hex << std::setfill('0') << std::setw(2) << gray_level << gray_level << gray_level; + svg.draw(p.shape, color.str()); + Point text_pos = get_extents(p.shape).center().cast(); + svg.draw_text(text_pos, std::to_string(i-1).c_str(), "orange", std::ceil(shape_bb.size().x() / 20 * 0.000001)); + //svg.draw(p.intersection, color.str()); + } +#endif // DEBUG_OUTPUT_DIR + + for (const PatchShape &patch : patch_shapes) { + // Check when exist some place to fill + ExPolygons patch_intersection = intersection_ex(patch.intersection, rest); + if (patch_intersection.empty()) continue; + + // Extend for sure + ExPolygons intersection = offset_ex(patch.intersection, extend_delta); + rest = diff_ex(rest, intersection, ApplySafetyOffset::Yes); + + used_shape_patches.push_back(patch.patch_index); + if (rest.empty()) break; + } + + // QUESTION: How to select which patch to use? How to sort them? + // Now is used back projection distance from used patches + // + // Idealy by outline depth: (need ray cast into patches) + // how to calc wanted depth - idealy by depth of outline help to overlap + // how to calc patch depth - depth in place of outline position + // Which outline to use between + + } + + std::vector result(patches.size(), {false}); + for (const std::vector &patches: used_shapes_patches) + for (uint32_t patch_index : patches) { + assert(patch_index < result.size()); + // check only onece insertation of patch + assert(!result[patch_index]); + result[patch_index] = true; + } + return result; +} + +priv::Loops priv::create_loops(const std::vector &outlines, const CutMesh& mesh) +{ + Loops loops; + Loops unclosed; + for (HI hi : outlines) { + VI vi_s = mesh.source(hi); + VI vi_t = mesh.target(hi); + Loop *loop_move = nullptr; + Loop *loop_connect = nullptr; + for (std::vector &cut : unclosed) { + if (cut.back() != vi_s) continue; + if (cut.front() == vi_t) { + // cut closing + loop_move = &cut; + } else { + loop_connect = &cut; + } + break; + } + if (loop_move != nullptr) { + // index of closed cut + size_t index = loop_move - &unclosed.front(); + // move cut to result + loops.emplace_back(std::move(*loop_move)); + // remove it from unclosed cut + unclosed.erase(unclosed.begin() + index); + } else if (loop_connect != nullptr) { + // try find tail to connect cut + Loop *loop_tail = nullptr; + for (Loop &cut : unclosed) { + if (cut.front() != vi_t) continue; + loop_tail = &cut; + break; + } + if (loop_tail != nullptr) { + // index of tail + size_t index = loop_tail - &unclosed.front(); + // move to connect vector + loop_connect->insert(loop_connect->end(), + make_move_iterator(loop_tail->begin()), + make_move_iterator(loop_tail->end())); + // remove tail from unclosed cut + unclosed.erase(unclosed.begin() + index); + } else { + loop_connect->push_back(vi_t); + } + } else { // not found + bool create_cut = true; + // try to insert to front of cut + for (Loop &cut : unclosed) { + if (cut.front() != vi_t) continue; + cut.insert(cut.begin(), vi_s); + create_cut = false; + break; + } + if (create_cut) + unclosed.emplace_back(std::vector{vi_s, vi_t}); + } + } + assert(unclosed.empty()); + return loops; +} + +Polygons priv::unproject_loops(const SurfacePatch &patch, const Project &projection, Vec2d &depth_range) +{ + assert(!patch.loops.empty()); + if (patch.loops.empty()) return {}; + + // NOTE: this method is working only when patch did not contain outward faces + Polygons polys; + polys.reserve(patch.loops.size()); + // project conture into 2d space to fillconvert outlines to + + size_t count = 0; + for (const Loop &l : patch.loops) count += l.size(); + std::vector depths; + depths.reserve(count); + + Points pts; + for (const Loop &l : patch.loops) { + pts.clear(); + pts.reserve(l.size()); + for (VI vi : l) { + const P3 &p3 = patch.mesh.point(vi); + Vec3d p = to_vec3d(p3); + double depth; + std::optional p2_opt = projection.unproject(p, &depth); + if (depth_range[0] > depth) depth_range[0] = depth; // min + if (depth_range[1] < depth) depth_range[1] = depth; // max + // Check when appear that skip is enough for poit which can't be unprojected + // - it could break contour + assert(p2_opt.has_value()); + if (!p2_opt.has_value()) continue; + + pts.push_back(p2_opt->cast()); + depths.push_back(static_cast(depth)); + } + // minimal is triangle + assert(pts.size() >= 3); + if (pts.size() < 3) continue; + + polys.emplace_back(pts); + } + + assert(!polys.empty()); + return polys; +} + +ExPolygon priv::to_expoly(const SurfacePatch &patch, const Project &projection, Vec2d &depth_range) +{ + Polygons polys = unproject_loops(patch, projection, depth_range); + // should not be used when no opposit triangle are counted so should not create overlaps + ClipperLib::PolyFillType fill_type = ClipperLib::PolyFillType::pftEvenOdd; + ExPolygons expolys = Slic3r::union_ex(polys, fill_type); + if (expolys.size() == 1) + return expolys.front(); + + // It should be one expolygon + assert(false); + + if (expolys.empty()) return {}; + // find biggest + const ExPolygon *biggest = &expolys.front(); + for (size_t index = 1; index < expolys.size(); ++index) { + const ExPolygon *current = &expolys[index]; + if (biggest->contour.size() < current->contour.size()) + biggest = current; + } + return *biggest; +} + +SurfaceCut priv::patch2cut(SurfacePatch &patch) +{ + CutMesh &mesh = patch.mesh; + + std::string convert_map_name = "v:convert"; + CutMesh::Property_map convert_map = + mesh.add_property_map(convert_map_name).first; + + size_t indices_size = mesh.faces().size(); + size_t vertices_size = mesh.vertices().size(); + + SurfaceCut sc; + sc.indices.reserve(indices_size); + sc.vertices.reserve(vertices_size); + for (VI vi : mesh.vertices()) { + // vi order is is not sorted + // assert(vi.idx() == sc.vertices.size()); + // vi is not continous + // assert(vi.idx() < vertices_size); + convert_map[vi] = sc.vertices.size(); + const P3 &p = mesh.point(vi); + sc.vertices.emplace_back(p.x(), p.y(), p.z()); + } + + for (FI fi : mesh.faces()) { + HI hi = mesh.halfedge(fi); + assert(mesh.next(hi).is_valid()); + assert(mesh.next(mesh.next(hi)).is_valid()); + // Is fi triangle? + assert(mesh.next(mesh.next(mesh.next(hi))) == hi); + + // triangle indicies + Vec3i32 ti; + size_t i = 0; + for (VI vi : { mesh.source(hi), + mesh.target(hi), + mesh.target(mesh.next(hi))}) + ti[i++] = convert_map[vi]; + sc.indices.push_back(ti); + } + + sc.contours.reserve(patch.loops.size()); + for (const Loop &loop : patch.loops) { + sc.contours.push_back({}); + std::vector &contour = sc.contours.back(); + contour.reserve(loop.size()); + for (VI vi : loop) contour.push_back(convert_map[vi]); + } + + // Not neccessary, clean and free memory + mesh.remove_property_map(convert_map); + return sc; +} + +void priv::append(SurfaceCut &sc, SurfaceCut &&sc_add) +{ + if (sc.empty()) { + sc = std::move(sc_add); + return; + } + + if (!sc_add.contours.empty()) { + SurfaceCut::Index offset = static_cast( + sc.vertices.size()); + size_t require = sc.contours.size() + sc_add.contours.size(); + if (sc.contours.capacity() < require) sc.contours.reserve(require); + for (std::vector &cut : sc_add.contours) + for (SurfaceCut::Index &i : cut) i += offset; + Slic3r::append(sc.contours, std::move(sc_add.contours)); + } + its_merge(sc, std::move(sc_add)); +} + +SurfaceCut priv::merge_patches(SurfacePatches &patches, const std::vector& mask) +{ + SurfaceCut result; + for (SurfacePatch &patch : patches) { + size_t index = &patch - &patches.front(); + if (!mask[index]) continue; + append(result, patch2cut(patch)); + } + return result; +} + +#ifdef DEBUG_OUTPUT_DIR +void priv::prepare_dir(const std::string &dir){ + namespace fs = std::filesystem; + if (fs::exists(dir)) { + for (auto &path : fs::directory_iterator(dir)) fs::remove_all(path); + } else { + fs::create_directories(dir); + } +} + +namespace priv{ +int reduction_order = 0; +int filled_order = 0; +int constrained_order = 0; +int diff_patch_order = 0; + +} // namespace priv + +void priv::initialize_store(const std::string& dir) +{ + // clear previous output + prepare_dir(dir); + reduction_order = 0; + filled_order = 0; + constrained_order = 0; + diff_patch_order = 0; +} + +void priv::store(const Vec3f &vertex, + const Vec3f &normal, + const std::string &file, + float size) +{ + int flatten = 20; + size_t min_i = 0; + for (size_t i = 1; i < 3; i++) + if (normal[min_i] > normal[i]) min_i = i; + Vec3f up_ = Vec3f::Zero(); + up_[min_i] = 1.f; + Vec3f side = normal.cross(up_).normalized() * size; + Vec3f up = side.cross(normal).normalized() * size; + + indexed_triangle_set its; + its.vertices.reserve(flatten + 1); + its.indices.reserve(flatten); + + its.vertices.push_back(vertex); + its.vertices.push_back(vertex + up); + size_t max_i = static_cast(flatten); + for (size_t i = 1; i < max_i; i++) { + float angle = i * 2 * M_PI / flatten; + Vec3f v = vertex + sin(angle) * side + cos(angle) * up; + its.vertices.push_back(v); + its.indices.emplace_back(0, i, i + 1); + } + its.indices.emplace_back(0, flatten, 1); + its_write_obj(its, file.c_str()); +} + +void priv::store(const CutMesh &mesh, const FaceTypeMap &face_type_map, const std::string& dir, bool is_filled) +{ + std::string off_file; + if (is_filled) { + if (filled_order == 0) prepare_dir(dir); + off_file = dir + "model" + std::to_string(filled_order++) + ".off"; + }else{ + if (constrained_order == 0) prepare_dir(dir); + off_file = dir + "model" + std::to_string(constrained_order++) + ".off"; + } + + CutMesh &mesh_ = const_cast(mesh); + auto face_colors = mesh_.add_property_map("f:color").first; + for (FI fi : mesh.faces()) { + auto &color = face_colors[fi]; + switch (face_type_map[fi]) { + case FaceType::inside: color = CGAL::Color{100, 250, 100}; break; // light green + case FaceType::inside_processed: color = CGAL::Color{170, 0, 0}; break; // dark red + case FaceType::outside: color = CGAL::Color{100, 0, 100}; break; // purple + case FaceType::not_constrained: color = CGAL::Color{127, 127, 127}; break; // gray + default: color = CGAL::Color{0, 0, 255}; // blue + } + } + CGAL::IO::write_OFF(off_file, mesh, CGAL::parameters::face_color_map(face_colors)); + mesh_.remove_property_map(face_colors); +} + +void priv::store(const ExPolygons &shapes, const std::string &svg_file) { + SVG svg(svg_file); + svg.draw(shapes); +} + +void priv::store(const CutMesh &mesh, const ReductionMap &reduction_map, const std::string& dir) +{ + if (reduction_order == 0) prepare_dir(dir); + std::string off_file = dir + "model" + std::to_string(reduction_order++) + ".off"; + + CutMesh &mesh_ = const_cast(mesh); + auto vertex_colors = mesh_.add_property_map("v:color").first; + // initialize to gray color + for (VI vi: mesh.vertices()) + vertex_colors[vi] = CGAL::Color{127, 127, 127}; + + for (VI reduction_from : mesh.vertices()) { + VI reduction_to = reduction_map[reduction_from]; + if (!reduction_to.is_valid()) continue; + vertex_colors[reduction_from] = CGAL::Color{255, 0, 0}; + vertex_colors[reduction_to] = CGAL::Color{0, 0, 255}; + } + + CGAL::IO::write_OFF(off_file, mesh, CGAL::parameters::vertex_color_map(vertex_colors)); + mesh_.remove_property_map(vertex_colors); +} + +namespace priv { +indexed_triangle_set create_indexed_triangle_set(const std::vector &faces, + const CutMesh &mesh); +} // namespace priv + +indexed_triangle_set priv::create_indexed_triangle_set( + const std::vector &faces, const CutMesh &mesh) +{ + std::vector vertices; + vertices.reserve(faces.size() * 2); + + indexed_triangle_set its; + its.indices.reserve(faces.size()); + for (FI fi : faces) { + HI hi = mesh.halfedge(fi); + HI hi_end = hi; + + int ti = 0; + Vec3i32 t; + + do { + VI vi = mesh.source(hi); + auto res = std::find(vertices.begin(), vertices.end(), vi); + t[ti++] = res - vertices.begin(); + if (res == vertices.end()) vertices.push_back(vi); + hi = mesh.next(hi); + } while (hi != hi_end); + + its.indices.push_back(t); + } + + its.vertices.reserve(vertices.size()); + for (VI vi : vertices) { + const auto &p = mesh.point(vi); + its.vertices.emplace_back(p.x(), p.y(), p.z()); + } + return its; +} + +void priv::store(const CutAOIs &aois, const CutMesh &mesh, const std::string &dir) { + auto create_outline_its = + [&mesh](const std::vector &outlines) -> indexed_triangle_set { + static const float line_width = 0.1f; + indexed_triangle_set its; + its.indices.reserve(2*outlines.size()); + its.vertices.reserve(outlines.size()*4); + for (HI hi : outlines) { + //FI fi = mesh.face(hi); + VI vi_a = mesh.source(hi); + VI vi_b = mesh.target(hi); + VI vi_c = mesh.target(mesh.next(hi)); + P3 p3_a = mesh.point(vi_a); + P3 p3_b = mesh.point(vi_b); + P3 p3_c = mesh.point(vi_c); + + Vec3f a(p3_a.x(), p3_a.y(), p3_a.z()); + Vec3f b(p3_b.x(), p3_b.y(), p3_b.z()); + Vec3f c(p3_c.x(), p3_c.y(), p3_c.z()); + + Vec3f v1 = b - a; // from a to b + v1.normalize(); + Vec3f v2 = c - a; // from a to c + v2.normalize(); + Vec3f norm = v1.cross(v2); + norm.normalize(); + Vec3f perp_to_edge = norm.cross(v1); + perp_to_edge.normalize(); + Vec3f dir = -perp_to_edge * line_width; + + size_t ai = its.vertices.size(); + its.vertices.push_back(a); + size_t bi = its.vertices.size(); + its.vertices.push_back(b); + size_t ai2 = its.vertices.size(); + its.vertices.push_back(a + dir); + size_t bi2 = its.vertices.size(); + its.vertices.push_back(b + dir); + + its.indices.push_back(Vec3i32(ai, ai2, bi)); + its.indices.push_back(Vec3i32(ai2, bi2, bi)); + } + return its; + }; + + prepare_dir(dir); + for (const auto &aoi : aois) { + size_t index = &aoi - &aois.front(); + std::string file = dir + "aoi" + std::to_string(index) + ".obj"; + indexed_triangle_set its = create_indexed_triangle_set(aoi.first, mesh); + its_write_obj(its, file.c_str()); + + // exist some outline? + if (aoi.second.empty()) continue; + std::string file_outline = dir + "outline" + std::to_string(index) + ".obj"; + indexed_triangle_set outline = create_outline_its(aoi.second); + its_write_obj(outline, file_outline.c_str()); + } +} + +void priv::store(const SurfacePatches &patches, const std::string &dir) { + prepare_dir(dir); + for (const priv::SurfacePatch &patch : patches) { + size_t index = &patch - &patches.front(); + if (patch.mesh.faces().empty()) continue; + CGAL::IO::write_OFF(dir + "patch" + std::to_string(index) + ".off", patch.mesh); + } +} +// +//void priv::store(const ProjectionDistances &pds, +// const VCutAOIs &aois, +// const CutMeshes &meshes, +// const std::string &file, +// float width) +//{ +// // create rectangle for each half edge from projection distances +// indexed_triangle_set its; +// its.vertices.reserve(4 * pds.size()); +// its.indices.reserve(2 * pds.size()); +// for (const ProjectionDistance &pd : pds) { +// if (pd.aoi_index == std::numeric_limits::max()) continue; +// HI hi = aois[pd.model_index][pd.aoi_index].second[pd.hi_index]; +// const CutMesh &mesh = meshes[pd.model_index]; +// VI vi1 = mesh.source(hi); +// VI vi2 = mesh.target(hi); +// VI vi3 = mesh.target(mesh.next(hi)); +// const P3 &p1 = mesh.point(vi1); +// const P3 &p2 = mesh.point(vi2); +// const P3 &p3 = mesh.point(vi3); +// Vec3f v1(p1.x(), p1.y(), p1.z()); +// Vec3f v2(p2.x(), p2.y(), p2.z()); +// Vec3f v3(p3.x(), p3.y(), p3.z()); +// +// Vec3f v12 = v2 - v1; +// v12.normalize(); +// Vec3f v13 = v3 - v1; +// v13.normalize(); +// Vec3f n = v12.cross(v13); +// n.normalize(); +// Vec3f side = n.cross(v12); +// side.normalize(); +// side *= -width; +// +// uint32_t i = its.vertices.size(); +// its.vertices.push_back(v1); +// its.vertices.push_back(v1+side); +// its.vertices.push_back(v2); +// its.vertices.push_back(v2+side); +// +// its.indices.emplace_back(i, i + 1, i + 2); +// its.indices.emplace_back(i + 2, i + 1, i + 3); +// } +// its_write_obj(its, file.c_str()); +//} + +void priv::store(const ExPolygons &shapes, const std::vector &mask, const Connections &connections, const std::string &file_svg) +{ + auto bb = get_extents(shapes); + int width = get_extents(shapes.front()).size().x() / 70; + + SVG svg(file_svg, bb); + svg.draw(shapes); + + ExPolygonsIndices s2i(shapes); + auto get_point = [&shapes, &s2i](size_t i)->Point { + auto id = s2i.cvt(i); + const ExPolygon &s = shapes[id.expolygons_index]; + const Polygon &p = (id.polygon_index == 0) ? + s.contour : + s.holes[id.polygon_index - 1]; + return p[id.point_index]; + }; + + bool is_first = true; + for (const Connection &c : connections) { + if (is_first) { + is_first = false; + Point p = get_point(c.first); + svg.draw(p, "purple", 4 * width); + continue; + } + Point p1 = get_point(c.first); + Point p2 = get_point(c.second); + svg.draw(Line(p1, p2), "red", width); + } + + for (size_t i = 0; i < s2i.get_count(); i++) { + Point p = get_point(i); + svg.draw(p, "black", 2*width); + if (!mask[i]) + svg.draw(p, "white", width); + } + svg.Close(); +} + +namespace priv { +/// +/// Create model consist of rectangles for each contour edge +/// +/// +/// +/// +indexed_triangle_set create_contour_its(const indexed_triangle_set& its, const std::vector &contour); + +/// +/// Getter on triangle tip (third vertex of face) +/// +/// First vertex index +/// Second vertex index +/// Source model +/// Tip Vertex index +unsigned int get_triangle_tip(unsigned int vi1, + unsigned int vi2, + const indexed_triangle_set &its); +} + + +unsigned int priv::get_triangle_tip(unsigned int vi1, + unsigned int vi2, + const indexed_triangle_set &its) +{ + assert(vi1 < its.vertices.size()); + assert(vi2 < its.vertices.size()); + for (const auto &t : its.indices) { + unsigned int tvi = std::numeric_limits::max(); + for (const auto &vi : t) { + unsigned int vi_ = static_cast(vi); + if (vi_ == vi1) continue; + if (vi_ == vi2) continue; + if (tvi == std::numeric_limits::max()) { + tvi = vi_; + } else { + tvi = std::numeric_limits::max(); + break; + } + } + if (tvi != std::numeric_limits::max()) + return tvi; + } + // triangle with indices vi1 and vi2 doesnt exist + assert(false); + return std::numeric_limits::max(); +} + +indexed_triangle_set priv::create_contour_its( + const indexed_triangle_set &its, const std::vector &contour) +{ + static const float line_width = 0.1f; + indexed_triangle_set result; + result.vertices.reserve((contour.size() + 1) * 4); + result.indices.reserve((contour.size() + 1) * 2); + unsigned int prev_vi = contour.back(); + for (unsigned int vi : contour) { + const Vec3f &a = its.vertices[vi]; + const Vec3f &b = its.vertices[prev_vi]; + const Vec3f &c = its.vertices[get_triangle_tip(vi, prev_vi, its)]; + + Vec3f v1 = b - a; // from a to b + v1.normalize(); + Vec3f v2 = c - a; // from a to c + v2.normalize(); + // triangle normal + Vec3f norm = v1.cross(v2); + norm.normalize(); + // perpendiculat to edge lay on triangle + Vec3f perp_to_edge = norm.cross(v1); + perp_to_edge.normalize(); + + Vec3f dir = -perp_to_edge * line_width; + + size_t ai = result.vertices.size(); + result.vertices.push_back(a); + size_t bi = result.vertices.size(); + result.vertices.push_back(b); + size_t ai2 = result.vertices.size(); + result.vertices.push_back(a + dir); + size_t bi2 = result.vertices.size(); + result.vertices.push_back(b + dir); + + result.indices.push_back(Vec3i32(ai, bi, ai2)); + result.indices.push_back(Vec3i32(ai2, bi, bi2)); + prev_vi = vi; + } + return result; +} + +//void priv::store(const SurfaceCuts &cut, const std::string &dir) { +// prepare_dir(dir); +// for (const auto &c : cut) { +// size_t index = &c - &cut.front(); +// std::string file = dir + "cut" + std::to_string(index) + ".obj"; +// its_write_obj(c, file.c_str()); +// for (const auto& contour : c.contours) { +// size_t c_index = &contour - &c.contours.front(); +// std::string c_file = dir + "cut" + std::to_string(index) + +// "contour" + std::to_string(c_index) + ".obj"; +// indexed_triangle_set c_its = create_contour_its(c, contour); +// its_write_obj(c_its, c_file.c_str()); +// } +// } +//} + +void priv::store(const SurfaceCut &cut, const std::string &file, const std::string &contour_dir) { + prepare_dir(contour_dir); + its_write_obj(cut, file.c_str()); + for (const auto& contour : cut.contours) { + size_t c_index = &contour - &cut.contours.front(); + std::string c_file = contour_dir + std::to_string(c_index) + ".obj"; + indexed_triangle_set c_its = create_contour_its(cut, contour); + its_write_obj(c_its, c_file.c_str()); + } +} + +void priv::store(const std::vector &models, + const std::string &obj_filename) +{ + indexed_triangle_set merged_model; + for (const indexed_triangle_set &model : models) + its_merge(merged_model, model); + its_write_obj(merged_model, obj_filename.c_str()); +} + +void priv::store(const std::vector &models, + const std::string &dir) +{ + prepare_dir(dir); + if (models.empty()) return; + if (models.size() == 1) { + CGAL::IO::write_OFF(dir + "model.off", models.front()); + return; + } + size_t model_index = 0; + for (const priv::CutMesh& model : models) { + std::string filename = dir + "model" + std::to_string(model_index++) + ".off"; + CGAL::IO::write_OFF(filename, model); + } +} + +// store projection center +void priv::store(const Emboss::IProjection &projection, + const Point &point_to_project, + float projection_ratio, + const std::string &obj_filename) +{ + auto [front, back] = projection.create_front_back(point_to_project); + Vec3d diff = back - front; + Vec3d pos = front + diff * projection_ratio; + priv::store(pos.cast(), diff.normalized().cast(), + DEBUG_OUTPUT_DIR + "projection_center.obj"); // only debug +} + +#endif // DEBUG_OUTPUT_DIR + +bool Slic3r::corefine_test(const std::string &model_path, const std::string &shape_path) { + priv::CutMesh model, shape; + if (!CGAL::IO::read_OFF(model_path, model)) return false; + if (!CGAL::IO::read_OFF(shape_path, shape)) return false; + + CGAL::Polygon_mesh_processing::corefine(model, shape); + return true; +} diff --git a/src/libslic3r/CutSurface.hpp b/src/libslic3r/CutSurface.hpp new file mode 100644 index 000000000..9f4e3159c --- /dev/null +++ b/src/libslic3r/CutSurface.hpp @@ -0,0 +1,74 @@ +#ifndef slic3r_CutSurface_hpp_ +#define slic3r_CutSurface_hpp_ + +#include +#include // indexed_triangle_set +#include "ExPolygon.hpp" +#include "Emboss.hpp" // IProjection + +namespace Slic3r{ + +/// +/// Represents cutted surface from object +/// Extend index triangle set by outlines +/// +struct SurfaceCut : public indexed_triangle_set +{ + // vertex indices(index to mesh vertices) + using Index = unsigned int; + using Contour = std::vector; + using Contours = std::vector; + // list of circulated open surface + Contours contours; +}; + +/// +/// Cut surface shape from models. +/// +/// Multiple shape to cut from model +/// Multi mesh to cut, need to be in same coordinate system +/// Define transformation 2d shape into 3d +/// Define ideal ratio between front and back projection to cut +/// 0 .. means use closest to front projection +/// 1 .. means use closest to back projection +/// value from <0, 1> +/// +/// Cutted surface from model +SurfaceCut cut_surface(const ExPolygons &shapes, + const std::vector &models, + const Emboss::IProjection &projection, + float projection_ratio); + +/// +/// Create model from surface cuts by projection +/// +/// Surface from model with outlines +/// Way of emboss +/// Mesh +indexed_triangle_set cut2model(const SurfaceCut &cut, + const Emboss::IProject3d &projection); + +/// +/// Separate (A)rea (o)f (I)nterest .. AoI from model +/// NOTE: Only 2d filtration, do not filtrate by Z coordinate +/// +/// Input model +/// Bounding box to project into space +/// Define tranformation of BB into space +/// Triangles lay at least partialy inside of projected Bounding box +indexed_triangle_set its_cut_AoI(const indexed_triangle_set &its, + const BoundingBox &bb, + const Emboss::IProjection &projection); + +/// +/// Separate triangles by mask +/// +/// Input model +/// Mask - same size as its::indices +/// Copy of indices by mask(with their vertices) +indexed_triangle_set its_mask(const indexed_triangle_set &its, const std::vector &mask); + +bool corefine_test(const std::string &model_path, const std::string &shape_path); + +} // namespace Slic3r +#endif // slic3r_CutSurface_hpp_ diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp new file mode 100644 index 000000000..269dbf05a --- /dev/null +++ b/src/libslic3r/Emboss.cpp @@ -0,0 +1,2181 @@ +#include +#include "Emboss.hpp" +#include +#include +#include +#include +#include +#include // union_ex + for boldness(polygon extend(offset)) +#include "IntersectionPoints.hpp" + +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "imgui/imstb_truetype.h" // stbtt_fontinfo +#include "Utils.hpp" // ScopeGuard + +#include // CGAL project +#include "libslic3r.h" + +// to heal shape +#include "ExPolygonsIndex.hpp" +#include "libslic3r/AABBTreeLines.hpp" // search structure for found close points +#include "libslic3r/Line.hpp" +#include "libslic3r/BoundingBox.hpp" + +// Experimentaly suggested ration of font ascent by multiple fonts +// to get approx center of normal text line +const double ASCENT_CENTER = 1/3.; // 0.5 is above small letter + +// every glyph's shape point is divided by SHAPE_SCALE - increase precission of fixed point value +// stored in fonts (to be able represents curve by sequence of lines) +static constexpr double SHAPE_SCALE = 0.001; // SCALING_FACTOR promile is fine enough +static unsigned MAX_HEAL_ITERATION_OF_TEXT = 10; + +using namespace Slic3r; +using namespace Emboss; +using fontinfo_opt = std::optional; + +// NOTE: approach to heal shape by Clipper::Closing is not working + +// functionality to remove all spikes from shape +// Potentionaly useable for eliminate spike in layer +//#define REMOVE_SPIKES + +// function to remove useless islands and holes +// #define REMOVE_SMALL_ISLANDS +#ifdef REMOVE_SMALL_ISLANDS +namespace { void remove_small_islands(ExPolygons &shape, double minimal_area);} +#endif //REMOVE_SMALL_ISLANDS + +//#define VISUALIZE_HEAL +#ifdef VISUALIZE_HEAL +namespace { +// for debug purpose only +// NOTE: check scale when store svg !! +#include "libslic3r/SVG.hpp" // for visualize_heal +static std::string visualize_heal_svg_filepath = "C:/data/temp/heal.svg"; +void visualize_heal(const std::string &svg_filepath, const ExPolygons &expolygons) +{ + Points pts = to_points(expolygons); + BoundingBox bb(pts); + // double svg_scale = SHAPE_SCALE / unscale(1.); + // bb.scale(svg_scale); + SVG svg(svg_filepath, bb); + svg.draw(expolygons); + + Points duplicits = collect_duplicates(pts); + int black_size = std::max(bb.size().x(), bb.size().y()) / 20; + svg.draw(duplicits, "black", black_size); + + Slic3r::IntersectionsLines intersections_f = get_intersections(expolygons); + Points intersections = get_unique_intersections(intersections_f); + svg.draw(intersections, "red", black_size * 1.2); +} +} // namespace +#endif // VISUALIZE_HEAL + +// do not expose out of this file stbtt_ data types +namespace{ +using Polygon = Slic3r::Polygon; +bool is_valid(const FontFile &font, unsigned int index); +fontinfo_opt load_font_info(const unsigned char *data, unsigned int index = 0); +std::optional get_glyph(const stbtt_fontinfo &font_info, int unicode_letter, float flatness); + +// take glyph from cache +const Glyph* get_glyph(int unicode, const FontFile &font, const FontProp &font_prop, + Glyphs &cache, fontinfo_opt &font_info_opt); + +// scale and convert float to int coordinate +Point to_point(const stbtt__point &point); + +// bad is contour smaller than 3 points +void remove_bad(Polygons &polygons); +void remove_bad(ExPolygons &expolygons); + +// Try to remove self intersection by subtracting rect 2x2 px +ExPolygon create_bounding_rect(const ExPolygons &shape); + +// Heal duplicates points and self intersections +bool heal_dupl_inter(ExPolygons &shape, unsigned max_iteration); + +const Points pts_2x2({Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1)}); +const Points pts_3x3({Point(-1, -1), Point(1, -1), Point(1, 1), Point(-1, 1)}); + +struct SpikeDesc +{ + // cosinus of max spike angle + double cos_angle; // speed up to skip acos + + // Half of Wanted bevel size + double half_bevel; + + /// + /// Calculate spike description + /// + /// Size of spike width after cut of the tip, has to be grater than 2.5 + /// When spike has same or more pixels with width less than 1 pixel + SpikeDesc(double bevel_size, double pixel_spike_length = 6): + // create min angle given by spike_length + // Use it as minimal height of 1 pixel base spike + cos_angle(std::fabs(std::cos( + /*angle*/ 2. * std::atan2(pixel_spike_length, .5) + ))), + + // When remove spike this angle is set. + // Value must be grater than min_angle + half_bevel(bevel_size / 2) + {} +}; + +// return TRUE when remove point. It could create polygon with 2 points. +bool remove_when_spike(Polygon &polygon, size_t index, const SpikeDesc &spike_desc); +void remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &duplicates); + +#ifdef REMOVE_SPIKES +// Remove long sharp corners aka spikes +// by adding points to bevel tip of spikes - Not printable parts +// Try to not modify long sides of spike and add points on it's side +void remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc); +void remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc); +void remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc); +#endif + +// spike ... very sharp corner - when not removed cause iteration of heal process +// index ... index of duplicit point in polygon +bool remove_when_spike(Slic3r::Polygon &polygon, size_t index, const SpikeDesc &spike_desc) { + + std::optional add; + bool do_erase = false; + Points &pts = polygon.points; + { + size_t pts_size = pts.size(); + if (pts_size < 3) + return false; + + const Point &a = (index == 0) ? pts.back() : pts[index - 1]; + const Point &b = pts[index]; + const Point &c = (index == (pts_size - 1)) ? pts.front() : pts[index + 1]; + + // calc sides + Vec2d ba = (a - b).cast(); + Vec2d bc = (c - b).cast(); + + double dot_product = ba.dot(bc); + + // sqrt together after multiplication save one sqrt + double ba_size_sq = ba.squaredNorm(); + double bc_size_sq = bc.squaredNorm(); + double norm = sqrt(ba_size_sq * bc_size_sq); + double cos_angle = dot_product / norm; + + // small angle are around 1 --> cos(0) = 1 + if (cos_angle < spike_desc.cos_angle) + return false; // not a spike + + // has to be in range <-1, 1> + // Due to preccission of floating point number could be sligtly out of range + if (cos_angle > 1.) + cos_angle = 1.; + // if (cos_angle < -1.) + // cos_angle = -1.; + + // Current Spike angle + double angle = acos(cos_angle); + double wanted_size = spike_desc.half_bevel / cos(angle / 2.); + double wanted_size_sq = wanted_size * wanted_size; + + bool is_ba_short = ba_size_sq < wanted_size_sq; + bool is_bc_short = bc_size_sq < wanted_size_sq; + + auto a_side = [&b, &ba, &ba_size_sq, &wanted_size]() -> Point { + Vec2d ba_norm = ba / sqrt(ba_size_sq); + return b + (wanted_size * ba_norm).cast(); + }; + auto c_side = [&b, &bc, &bc_size_sq, &wanted_size]() -> Point { + Vec2d bc_norm = bc / sqrt(bc_size_sq); + return b + (wanted_size * bc_norm).cast(); + }; + + if (is_ba_short && is_bc_short) { + // remove short spike + do_erase = true; + } else if (is_ba_short) { + // move point B on C-side + pts[index] = c_side(); + } else if (is_bc_short) { + // move point B on A-side + pts[index] = a_side(); + } else { + // move point B on C-side and add point on A-side(left - before) + pts[index] = c_side(); + add = a_side(); + if (*add == pts[index]) { + // should be very rare, when SpikeDesc has small base + // will be fixed by remove B point + add.reset(); + do_erase = true; + } + } + } + if (do_erase) { + pts.erase(pts.begin() + index); + return true; + } + if (add.has_value()) + pts.insert(pts.begin() + index, *add); + return false; +} + +void remove_spikes_in_duplicates(ExPolygons &expolygons, const Points &duplicates) { + if (duplicates.empty()) + return; + auto check = [](Slic3r::Polygon &polygon, const Point &d) -> bool { + double spike_bevel = 1 / SHAPE_SCALE; + double spike_length = 5.; + const static SpikeDesc sd(spike_bevel, spike_length); + Points& pts = polygon.points; + bool exist_remove = false; + for (size_t i = 0; i < pts.size(); i++) { + if (pts[i] != d) + continue; + exist_remove |= remove_when_spike(polygon, i, sd); + } + return exist_remove && pts.size() < 3; + }; + + bool exist_remove = false; + for (ExPolygon &expolygon : expolygons) { + BoundingBox bb(to_points(expolygon.contour)); + for (const Point &d : duplicates) { + if (!bb.contains(d)) + continue; + exist_remove |= check(expolygon.contour, d); + for (Polygon &hole : expolygon.holes) + exist_remove |= check(hole, d); + } + } + + if (exist_remove) + remove_bad(expolygons); +} + +bool is_valid(const FontFile &font, unsigned int index) { + if (font.data == nullptr) return false; + if (font.data->empty()) return false; + if (index >= font.infos.size()) return false; + return true; +} + +fontinfo_opt load_font_info( + const unsigned char *data, unsigned int index) +{ + int font_offset = stbtt_GetFontOffsetForIndex(data, index); + if (font_offset < 0) { + assert(false); + // "Font index(" << index << ") doesn't exist."; + return {}; + } + stbtt_fontinfo font_info; + if (stbtt_InitFont(&font_info, data, font_offset) == 0) { + // Can't initialize font. + assert(false); + return {}; + } + return font_info; +} + +void remove_bad(Polygons &polygons) { + polygons.erase( + std::remove_if(polygons.begin(), polygons.end(), + [](const Polygon &p) { return p.size() < 3; }), + polygons.end()); +} + +void remove_bad(ExPolygons &expolygons) { + expolygons.erase( + std::remove_if(expolygons.begin(), expolygons.end(), + [](const ExPolygon &p) { return p.contour.size() < 3; }), + expolygons.end()); + + for (ExPolygon &expolygon : expolygons) + remove_bad(expolygon.holes); +} +} // end namespace + +bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double distance) +{ + if (expolygons.empty()) return false; + if (distance < 0.) return false; + + // ExPolygons can't contain same neigbours + remove_same_neighbor(expolygons); + + // IMPROVE: use int(insted of double) lines and tree + const ExPolygonsIndices ids(expolygons); + const std::vector lines = Slic3r::to_linesf(expolygons, ids.get_count()); + AABBTreeIndirect::Tree<2, double> tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + using Div = std::pair; + std::vector
divs; + size_t point_index = 0; + auto check_points = [&divs, &point_index, &lines, &tree, &distance, &ids, &expolygons](const Points &pts) { + for (const Point &p : pts) { + Vec2d p_d = p.cast(); + std::vector close_lines = AABBTreeLines::all_lines_in_radius(lines, tree, p_d, distance); + for (size_t index : close_lines) { + // skip point neighbour lines indices + if (index == point_index) continue; + if (&p != &pts.front()) { + if (index == point_index - 1) continue; + } else if (index == (pts.size()-1)) continue; + + // do not doubled side point of segment + const ExPolygonsIndex id = ids.cvt(index); + const ExPolygon &expoly = expolygons[id.expolygons_index]; + const Polygon &poly = id.is_contour() ? expoly.contour : expoly.holes[id.hole_index()]; + const Points &poly_pts = poly.points; + const Point &line_a = poly_pts[id.point_index]; + const Point &line_b = (!ids.is_last_point(id)) ? poly_pts[id.point_index + 1] : poly_pts.front(); + assert(line_a == lines[index].a.cast()); + assert(line_b == lines[index].b.cast()); + if (p == line_a || p == line_b) continue; + + divs.emplace_back(p, index); + } + ++point_index; + } + }; + for (const ExPolygon &expoly : expolygons) { + check_points(expoly.contour.points); + for (const Polygon &hole : expoly.holes) + check_points(hole.points); + } + + // check if exist division + if (divs.empty()) return false; + + // sort from biggest index to zero + // to be able add points and not interupt indices + std::sort(divs.begin(), divs.end(), + [](const Div &d1, const Div &d2) { return d1.second > d2.second; }); + + auto it = divs.begin(); + // divide close line + while (it != divs.end()) { + // colect division of a line segmen + size_t index = it->second; + auto it2 = it+1; + while (it2 != divs.end() && it2->second == index) ++it2; + + ExPolygonsIndex id = ids.cvt(index); + ExPolygon &expoly = expolygons[id.expolygons_index]; + Polygon &poly = id.is_contour() ? expoly.contour : expoly.holes[id.hole_index()]; + Points &pts = poly.points; + size_t count = it2 - it; + + // add points into polygon to divide in place of near point + if (count == 1) { + pts.insert(pts.begin() + id.point_index + 1, it->first); + ++it; + } else { + // collect points to add into polygon + Points points; + points.reserve(count); + for (; it < it2; ++it) + points.push_back(it->first); + + // need sort by line direction + const Linef &line = lines[index]; + Vec2d dir = line.b - line.a; + // select mayorit direction + int axis = (abs(dir.x()) > abs(dir.y())) ? 0 : 1; + using Fnc = std::function; + Fnc fnc = (dir[axis] < 0) ? Fnc([axis](const Point &p1, const Point &p2) { return p1[axis] > p2[axis]; }) : + Fnc([axis](const Point &p1, const Point &p2) { return p1[axis] < p2[axis]; }) ; + std::sort(points.begin(), points.end(), fnc); + + // use only unique points + points.erase(std::unique(points.begin(), points.end()), points.end()); + + // divide line by adding points into polygon + pts.insert(pts.begin() + id.point_index + 1, + points.begin(), points.end()); + } + assert(it == it2); + } + return true; +} + +HealedExPolygons Emboss::heal_polygons(const Polygons &shape, bool is_non_zero, unsigned int max_iteration) +{ + const double clean_distance = 1.415; // little grater than sqrt(2) + ClipperLib::PolyFillType fill_type = is_non_zero ? + ClipperLib::pftNonZero : ClipperLib::pftEvenOdd; + + // When edit this code check that font 'ALIENATE.TTF' and glyph 'i' still work + // fix of self intersections + // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm + ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), fill_type); + ClipperLib::CleanPolygons(paths, clean_distance); + Polygons polygons = to_polygons(paths); + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), + [](const Polygon &p) { return p.size() < 3; }), polygons.end()); + + if (polygons.empty()) + return {{}, false}; + + // Do not remove all duplicates but do it better way + // Overlap all duplicit points by rectangle 3x3 + Points duplicits = collect_duplicates(to_points(polygons)); + if (!duplicits.empty()) { + polygons.reserve(polygons.size() + duplicits.size()); + for (const Point &p : duplicits) { + Polygon rect_3x3(pts_3x3); + rect_3x3.translate(p); + polygons.push_back(rect_3x3); + } + } + ExPolygons res = Slic3r::union_ex(polygons, fill_type); + bool is_healed = heal_expolygons(res, max_iteration); + return {res, is_healed}; +} + + +bool Emboss::heal_expolygons(ExPolygons &shape, unsigned max_iteration) +{ + return ::heal_dupl_inter(shape, max_iteration); +} + +namespace { + +Points get_unique_intersections(const Slic3r::IntersectionsLines &intersections) +{ + Points result; + if (intersections.empty()) + return result; + + // convert intersections into Points + result.reserve(intersections.size()); + std::transform(intersections.begin(), intersections.end(), std::back_inserter(result), + [](const Slic3r::IntersectionLines &i) { return Point( + std::floor(i.intersection.x()), + std::floor(i.intersection.y())); + }); + // intersections should be unique poits + std::sort(result.begin(), result.end()); + auto it = std::unique(result.begin(), result.end()); + result.erase(it, result.end()); + return result; +} + +Polygons get_holes_with_points(const Polygons &holes, const Points &points) +{ + Polygons result; + for (const Slic3r::Polygon &hole : holes) + for (const Point &p : points) + for (const Point &h : hole) + if (p == h) { + result.push_back(hole); + break; + } + return result; +} + +/// +/// Fill holes which create duplicits or intersections +/// When healing hole creates trouble in shape again try to heal by an union instead of diff_ex +/// +/// Holes which was substracted from shape previous +/// Current duplicates in shape +/// Current intersections in shape +/// Partialy healed shape[could be modified] +/// True when modify shape otherwise False +bool fill_trouble_holes(const Polygons &holes, const Points &duplicates, const Points &intersections, ExPolygons &shape) +{ + if (holes.empty()) + return false; + if (duplicates.empty() && intersections.empty()) + return false; + + Polygons fill = get_holes_with_points(holes, duplicates); + append(fill, get_holes_with_points(holes, intersections)); + if (fill.empty()) + return false; + + shape = union_ex(shape, fill); + return true; +} + +// extend functionality from Points.cpp --> collect_duplicates +// with address of duplicated points +struct Duplicate { + Point point; + std::vector indices; +}; +using Duplicates = std::vector; +Duplicates collect_duplicit_indices(const ExPolygons &expoly) +{ + Points pts = to_points(expoly); + + // initialize original index locations + std::vector idx(pts.size()); + std::iota(idx.begin(), idx.end(), 0); + std::sort(idx.begin(), idx.end(), + [&pts](uint32_t i1, uint32_t i2) { return pts[i1] < pts[i2]; }); + + Duplicates result; + const Point *prev = &pts[idx.front()]; + for (size_t i = 1; i < idx.size(); ++i) { + uint32_t index = idx[i]; + const Point *act = &pts[index]; + if (*prev == *act) { + // duplicit point + if (!result.empty() && result.back().point == *act) { + // more than 2 points with same coordinate + result.back().indices.push_back(index); + } else { + uint32_t prev_index = idx[i-1]; + result.push_back({*act, {prev_index, index}}); + } + continue; + } + prev = act; + } + return result; +} + +Points get_points(const Duplicates& duplicate_indices) +{ + Points result; + if (duplicate_indices.empty()) + return result; + + // convert intersections into Points + result.reserve(duplicate_indices.size()); + std::transform(duplicate_indices.begin(), duplicate_indices.end(), std::back_inserter(result), + [](const Duplicate &d) { return d.point; }); + return result; +} + +bool heal_dupl_inter(ExPolygons &shape, unsigned max_iteration) +{ + if (shape.empty()) return true; + remove_same_neighbor(shape); + + // create loop permanent memory + Polygons holes; + while (--max_iteration) { + Duplicates duplicate_indices = collect_duplicit_indices(shape); + //Points duplicates = collect_duplicates(to_points(shape)); + IntersectionsLines intersections = get_intersections(shape); + + // Check whether shape is already healed + if (intersections.empty() && duplicate_indices.empty()) + return true; + + Points duplicate_points = get_points(duplicate_indices); + Points intersection_points = get_unique_intersections(intersections); + + if (fill_trouble_holes(holes, duplicate_points, intersection_points, shape)) { + holes.clear(); + continue; + } + + holes.clear(); + holes.reserve(intersections.size() + duplicate_points.size()); + + remove_spikes_in_duplicates(shape, duplicate_points); + + // Fix self intersection in result by subtracting hole 2x2 + for (const Point &p : intersection_points) { + Polygon hole(pts_2x2); + hole.translate(p); + holes.push_back(hole); + } + + // Fix duplicit points by hole 3x3 around duplicit point + for (const Point &p : duplicate_points) { + Polygon hole(pts_3x3); + hole.translate(p); + holes.push_back(hole); + } + + shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::No); + // ApplySafetyOffset::Yes is incompatible with function fill_trouble_holes + } + + // Create partialy healed output + Duplicates duplicates = collect_duplicit_indices(shape); + IntersectionsLines intersections = get_intersections(shape); + if (duplicates.empty() && intersections.empty()){ + // healed in the last loop + return true; + } + + #ifdef VISUALIZE_HEAL + visualize_heal(visualize_heal_svg_filepath, shape); + #endif // VISUALIZE_HEAL + + assert(false); // Can not heal this shape + // investigate how to heal better way + + ExPolygonsIndices ei(shape); + std::vector is_healed(shape.size(), {true}); + for (const Duplicate &duplicate : duplicates){ + for (uint32_t i : duplicate.indices) + is_healed[ei.cvt(i).expolygons_index] = false; + } + for (const IntersectionLines &intersection : intersections) { + is_healed[ei.cvt(intersection.line_index1).expolygons_index] = false; + is_healed[ei.cvt(intersection.line_index2).expolygons_index] = false; + } + + for (size_t shape_index = 0; shape_index < shape.size(); shape_index++) { + if (!is_healed[shape_index]) { + // exchange non healed expoly with bb rect + ExPolygon &expoly = shape[shape_index]; + expoly = create_bounding_rect({expoly}); + } + } + return false; +} + +ExPolygon create_bounding_rect(const ExPolygons &shape) { + BoundingBox bb = get_extents(shape); + Point size = bb.size(); + if (size.x() < 10) + bb.max.x() += 10; + if (size.y() < 10) + bb.max.y() += 10; + + Polygon rect({// CCW + bb.min, + {bb.max.x(), bb.min.y()}, + bb.max, + {bb.min.x(), bb.max.y()}}); + + Point offset = bb.size() * 0.1; + Polygon hole({// CW + bb.min + offset, + {bb.min.x() + offset.x(), bb.max.y() - offset.y()}, + bb.max - offset, + {bb.max.x() - offset.x(), bb.min.y() + offset.y()}}); + + return ExPolygon(rect, hole); +} + +#ifdef REMOVE_SMALL_ISLANDS +void remove_small_islands(ExPolygons &expolygons, double minimal_area) { + if (expolygons.empty()) + return; + + // remove small expolygons contours + auto expoly_it = std::remove_if(expolygons.begin(), expolygons.end(), + [&minimal_area](const ExPolygon &p) { return p.contour.area() < minimal_area; }); + expolygons.erase(expoly_it, expolygons.end()); + + // remove small holes in expolygons + for (ExPolygon &expoly : expolygons) { + Polygons& holes = expoly.holes; + auto it = std::remove_if(holes.begin(), holes.end(), + [&minimal_area](const Polygon &p) { return -p.area() < minimal_area; }); + holes.erase(it, holes.end()); + } +} +#endif // REMOVE_SMALL_ISLANDS + +std::optional get_glyph(const stbtt_fontinfo &font_info, int unicode_letter, float flatness) +{ + int glyph_index = stbtt_FindGlyphIndex(&font_info, unicode_letter); + if (glyph_index == 0) { + //wchar_t wchar = static_cast(unicode_letter); + //<< "Character unicode letter (" + //<< "decimal value = " << std::dec << unicode_letter << ", " + //<< "hexadecimal value = U+" << std::hex << unicode_letter << std::dec << ", " + //<< "wchar value = " << wchar + //<< ") is NOT defined inside of the font. \n"; + return {}; + } + + Glyph glyph; + stbtt_GetGlyphHMetrics(&font_info, glyph_index, &glyph.advance_width, &glyph.left_side_bearing); + + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(&font_info, glyph_index, &vertices); + if (num_verts <= 0) return glyph; // no shape + ScopeGuard sg1([&vertices]() { free(vertices); }); + + int *contour_lengths = NULL; + int num_countour_int = 0; + stbtt__point *points = stbtt_FlattenCurves(vertices, num_verts, + flatness, &contour_lengths, &num_countour_int, font_info.userdata); + if (!points) return glyph; // no valid flattening + ScopeGuard sg2([&contour_lengths, &points]() { + free(contour_lengths); + free(points); + }); + + size_t num_contour = static_cast(num_countour_int); + Polygons glyph_polygons; + glyph_polygons.reserve(num_contour); + size_t pi = 0; // point index + for (size_t ci = 0; ci < num_contour; ++ci) { + int length = contour_lengths[ci]; + // check minimal length for triangle + if (length < 4) { + // weird font + pi+=length; + continue; + } + // last point is first point + --length; + Points pts; + pts.reserve(length); + for (int i = 0; i < length; ++i) + pts.emplace_back(to_point(points[pi++])); + + // last point is first point --> closed contour + assert(pts.front() == to_point(points[pi])); + ++pi; + + // change outer cw to ccw and inner ccw to cw order + std::reverse(pts.begin(), pts.end()); + glyph_polygons.emplace_back(pts); + } + if (!glyph_polygons.empty()) { + unsigned max_iteration = 10; + // TrueTypeFonts use non zero winding number + // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01 + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html + bool is_non_zero = true; + glyph.shape = Emboss::heal_polygons(glyph_polygons, is_non_zero, max_iteration); + } + return glyph; +} + +const Glyph* get_glyph( + int unicode, + const FontFile & font, + const FontProp & font_prop, + Glyphs & cache, + fontinfo_opt &font_info_opt) +{ + // TODO: Use resolution by printer configuration, or add it into FontProp + const float RESOLUTION = 0.0125f; // [in mm] + auto glyph_item = cache.find(unicode); + if (glyph_item != cache.end()) return &glyph_item->second; + + unsigned int font_index = font_prop.collection_number.value_or(0); + if (!is_valid(font, font_index)) return nullptr; + + if (!font_info_opt.has_value()) { + + font_info_opt = load_font_info(font.data->data(), font_index); + // can load font info? + if (!font_info_opt.has_value()) return nullptr; + } + + float flatness = font.infos[font_index].ascent * RESOLUTION / font_prop.size_in_mm; + + // Fix for very small flatness because it create huge amount of points from curve + if (flatness < RESOLUTION) flatness = RESOLUTION; + + std::optional glyph_opt = get_glyph(*font_info_opt, unicode, flatness); + + // IMPROVE: multiple loadig glyph without data + // has definition inside of font? + if (!glyph_opt.has_value()) return nullptr; + + Glyph &glyph = *glyph_opt; + if (font_prop.char_gap.has_value()) + glyph.advance_width += *font_prop.char_gap; + + // scale glyph size + glyph.advance_width = static_cast(glyph.advance_width / SHAPE_SCALE); + glyph.left_side_bearing = static_cast(glyph.left_side_bearing / SHAPE_SCALE); + + if (!glyph.shape.empty()) { + if (font_prop.boldness.has_value()) { + float delta = static_cast(*font_prop.boldness / SHAPE_SCALE / font_prop.size_in_mm); + glyph.shape = Slic3r::union_ex(offset_ex(glyph.shape, delta)); + } + if (font_prop.skew.has_value()) { + double ratio = *font_prop.skew; + auto skew = [&ratio](Polygon &polygon) { + for (Slic3r::Point &p : polygon.points) + p.x() += static_cast(std::round(p.y() * ratio)); + }; + for (ExPolygon &expolygon : glyph.shape) { + skew(expolygon.contour); + for (Polygon &hole : expolygon.holes) skew(hole); + } + } + } + auto [it, success] = cache.try_emplace(unicode, std::move(glyph)); + assert(success); + return &it->second; +} + +Point to_point(const stbtt__point &point) { + return Point(static_cast(std::round(point.x / SHAPE_SCALE)), + static_cast(std::round(point.y / SHAPE_SCALE))); +} + +} // namespace + +#ifdef _WIN32 +#include +#include +#include +#include + +namespace { +EmbossStyle create_style(const std::wstring& name, const std::wstring& path) { + return { boost::nowide::narrow(name.c_str()), + boost::nowide::narrow(path.c_str()), + EmbossStyle::Type::file_path, FontProp() }; +} +} // namespace + +// Get system font file path +std::optional Emboss::get_font_path(const std::wstring &font_face_name) +{ +// static const LPWSTR fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + static const LPCWSTR fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + HKEY hKey; + LONG result; + + // Open Windows font registry key + result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, fontRegistryPath, 0, KEY_READ, &hKey); + if (result != ERROR_SUCCESS) return {}; + + DWORD maxValueNameSize, maxValueDataSize; + result = RegQueryInfoKey(hKey, 0, 0, 0, 0, 0, 0, 0, &maxValueNameSize, &maxValueDataSize, 0, 0); + if (result != ERROR_SUCCESS) return {}; + + DWORD valueIndex = 0; + LPWSTR valueName = new WCHAR[maxValueNameSize]; + LPBYTE valueData = new BYTE[maxValueDataSize]; + DWORD valueNameSize, valueDataSize, valueType; + std::wstring wsFontFile; + + // Look for a matching font name + do { + wsFontFile.clear(); + valueDataSize = maxValueDataSize; + valueNameSize = maxValueNameSize; + + result = RegEnumValue(hKey, valueIndex, valueName, &valueNameSize, 0, &valueType, valueData, &valueDataSize); + + valueIndex++; + if (result != ERROR_SUCCESS || valueType != REG_SZ) { + continue; + } + + std::wstring wsValueName(valueName, valueNameSize); + + // Found a match + if (_wcsnicmp(font_face_name.c_str(), wsValueName.c_str(), font_face_name.length()) == 0) { + + wsFontFile.assign((LPWSTR)valueData, valueDataSize); + break; + } + }while (result != ERROR_NO_MORE_ITEMS); + + delete[] valueName; + delete[] valueData; + + RegCloseKey(hKey); + + if (wsFontFile.empty()) return {}; + + // Build full font file path + WCHAR winDir[MAX_PATH]; + GetWindowsDirectory(winDir, MAX_PATH); + + std::wstringstream ss; + ss << winDir << "\\Fonts\\" << wsFontFile; + wsFontFile = ss.str(); + + return wsFontFile; +} + +EmbossStyles Emboss::get_font_list() +{ + //EmbossStyles list1 = get_font_list_by_enumeration(); + //EmbossStyles list2 = get_font_list_by_register(); + //EmbossStyles list3 = get_font_list_by_folder(); + return get_font_list_by_register(); +} + +EmbossStyles Emboss::get_font_list_by_register() { +// static const LPWSTR fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + static const LPCWSTR fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + HKEY hKey; + LONG result; + + // Open Windows font registry key + result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, fontRegistryPath, 0, KEY_READ, &hKey); + if (result != ERROR_SUCCESS) { + assert(false); + //std::wcerr << L"Can not Open register key (" << fontRegistryPath << ")" + // << L", function 'RegOpenKeyEx' return code: " << result << std::endl; + return {}; + } + + DWORD maxValueNameSize, maxValueDataSize; + result = RegQueryInfoKey(hKey, 0, 0, 0, 0, 0, 0, 0, &maxValueNameSize, + &maxValueDataSize, 0, 0); + if (result != ERROR_SUCCESS) { + assert(false); + // Can not earn query key, function 'RegQueryInfoKey' return code: result + return {}; + } + + // Build full font file path + WCHAR winDir[MAX_PATH]; + GetWindowsDirectory(winDir, MAX_PATH); + std::wstring font_path = std::wstring(winDir) + L"\\Fonts\\"; + + EmbossStyles font_list; + DWORD valueIndex = 0; + // Look for a matching font name + LPWSTR font_name = new WCHAR[maxValueNameSize]; + LPBYTE fileTTF_name = new BYTE[maxValueDataSize]; + DWORD font_name_size, fileTTF_name_size, valueType; + do { + fileTTF_name_size = maxValueDataSize; + font_name_size = maxValueNameSize; + + result = RegEnumValue(hKey, valueIndex, font_name, &font_name_size, 0, + &valueType, fileTTF_name, &fileTTF_name_size); + valueIndex++; + if (result != ERROR_SUCCESS || valueType != REG_SZ) continue; + std::wstring font_name_w(font_name, font_name_size); + std::wstring file_name_w((LPWSTR) fileTTF_name, fileTTF_name_size); + std::wstring path_w = font_path + file_name_w; + + // filtrate .fon from lists + size_t pos = font_name_w.rfind(L" (TrueType)"); + if (pos >= font_name_w.size()) continue; + // remove TrueType text from name + font_name_w = std::wstring(font_name_w, 0, pos); + font_list.emplace_back(create_style(font_name_w, path_w)); + } while (result != ERROR_NO_MORE_ITEMS); + delete[] font_name; + delete[] fileTTF_name; + + RegCloseKey(hKey); + return font_list; +} + +// TODO: Fix global function +bool CALLBACK EnumFamCallBack(LPLOGFONT lplf, + LPNEWTEXTMETRIC lpntm, + DWORD FontType, + LPVOID aFontList) +{ + std::vector *fontList = + (std::vector *) (aFontList); + if (FontType & TRUETYPE_FONTTYPE) { + std::wstring name = lplf->lfFaceName; + fontList->push_back(name); + } + return true; + // UNREFERENCED_PARAMETER(lplf); + UNREFERENCED_PARAMETER(lpntm); +} + +EmbossStyles Emboss::get_font_list_by_enumeration() { + + HDC hDC = GetDC(NULL); + std::vector font_names; + EnumFontFamilies(hDC, (LPCTSTR) NULL, (FONTENUMPROC) EnumFamCallBack, + (LPARAM) &font_names); + + EmbossStyles font_list; + for (const std::wstring &font_name : font_names) { + font_list.emplace_back(create_style(font_name, L"")); + } + return font_list; +} + +EmbossStyles Emboss::get_font_list_by_folder() { + EmbossStyles result; + WCHAR winDir[MAX_PATH]; + UINT winDir_size = GetWindowsDirectory(winDir, MAX_PATH); + std::wstring search_dir = std::wstring(winDir, winDir_size) + L"\\Fonts\\"; + WIN32_FIND_DATA fd; + HANDLE hFind; + // By https://en.wikipedia.org/wiki/TrueType has also suffix .tte + std::vector suffixes = {L"*.ttf", L"*.ttc", L"*.tte"}; + for (const std::wstring &suffix : suffixes) { + hFind = ::FindFirstFile((search_dir + suffix).c_str(), &fd); + if (hFind == INVALID_HANDLE_VALUE) continue; + do { + // skip folder . and .. + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; + std::wstring file_name(fd.cFileName); + // TODO: find font name instead of filename + result.emplace_back(create_style(file_name, search_dir + file_name)); + } while (::FindNextFile(hFind, &fd)); + ::FindClose(hFind); + } + return result; +} + +#else +EmbossStyles Emboss::get_font_list() { + // not implemented + return {}; +} + +std::optional Emboss::get_font_path(const std::wstring &font_face_name){ + // not implemented + return {}; +} +#endif + +std::unique_ptr Emboss::create_font_file( + std::unique_ptr> data) +{ + int collection_size = stbtt_GetNumberOfFonts(data->data()); + // at least one font must be inside collection + if (collection_size < 1) { + assert(false); + // There is no font collection inside font data + return nullptr; + } + + unsigned int c_size = static_cast(collection_size); + std::vector infos; + infos.reserve(c_size); + for (unsigned int i = 0; i < c_size; ++i) { + auto font_info = load_font_info(data->data(), i); + if (!font_info.has_value()) return nullptr; + + const stbtt_fontinfo *info = &(*font_info); + // load information about line gap + int ascent, descent, linegap; + stbtt_GetFontVMetrics(info, &ascent, &descent, &linegap); + + float pixels = 1000.; // value is irelevant + float em_pixels = stbtt_ScaleForMappingEmToPixels(info, pixels); + int units_per_em = static_cast(std::round(pixels / em_pixels)); + + infos.emplace_back(FontFile::Info{ascent, descent, linegap, units_per_em}); + } + return std::make_unique(std::move(data), std::move(infos)); +} + +std::unique_ptr Emboss::create_font_file(const char *file_path) +{ + FILE *file = std::fopen(file_path, "rb"); + if (file == nullptr) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Couldn't open " << file_path << " for reading."; + return nullptr; + } + ScopeGuard sg([&file]() { std::fclose(file); }); + + // find size of file + if (fseek(file, 0L, SEEK_END) != 0) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Couldn't fseek file " << file_path << " for size measure."; + return nullptr; + } + size_t size = ftell(file); + if (size == 0) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Size of font file is zero. Can't read."; + return nullptr; + } + rewind(file); + auto buffer = std::make_unique>(size); + size_t count_loaded_bytes = fread((void *) &buffer->front(), 1, size, file); + if (count_loaded_bytes != size) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Different loaded(from file) data size."; + return nullptr; + } + return create_font_file(std::move(buffer)); +} + + +#ifdef _WIN32 +static bool load_hfont(void* hfont, DWORD &dwTable, DWORD &dwOffset, size_t& size, HDC hdc = nullptr){ + bool del_hdc = false; + if (hdc == nullptr) { + del_hdc = true; + hdc = ::CreateCompatibleDC(NULL); + if (hdc == NULL) return false; + } + + // To retrieve the data from the beginning of the file for TrueType + // Collection files specify 'ttcf' (0x66637474). + dwTable = 0x66637474; + dwOffset = 0; + + ::SelectObject(hdc, hfont); + size = ::GetFontData(hdc, dwTable, dwOffset, NULL, 0); + if (size == GDI_ERROR) { + // HFONT is NOT TTC(collection) + dwTable = 0; + size = ::GetFontData(hdc, dwTable, dwOffset, NULL, 0); + } + + if (size == 0 || size == GDI_ERROR) { + if (del_hdc) ::DeleteDC(hdc); + return false; + } + return true; +} + +void *Emboss::can_load(void *hfont) +{ + DWORD dwTable=0, dwOffset=0; + size_t size = 0; + if (!load_hfont(hfont, dwTable, dwOffset, size)) return nullptr; + return hfont; +} + +std::unique_ptr Emboss::create_font_file(void *hfont) +{ + HDC hdc = ::CreateCompatibleDC(NULL); + if (hdc == NULL) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Can't create HDC by CreateCompatibleDC(NULL)."; + return nullptr; + } + + DWORD dwTable=0,dwOffset = 0; + size_t size; + if (!load_hfont(hfont, dwTable, dwOffset, size, hdc)) { + ::DeleteDC(hdc); + return nullptr; + } + auto buffer = std::make_unique>(size); + size_t loaded_size = ::GetFontData(hdc, dwTable, dwOffset, buffer->data(), size); + ::DeleteDC(hdc); + if (size != loaded_size) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Different loaded(from HFONT) data size."; + return nullptr; + } + return create_font_file(std::move(buffer)); +} +#endif // _WIN32 + +std::optional Emboss::letter2glyph(const FontFile &font, + unsigned int font_index, + int letter, + float flatness) +{ + if (!is_valid(font, font_index)) return {}; + auto font_info_opt = load_font_info(font.data->data(), font_index); + if (!font_info_opt.has_value()) return {}; + return get_glyph(*font_info_opt, letter, flatness); +} + +const FontFile::Info &Emboss::get_font_info(const FontFile &font, const FontProp &prop) +{ + unsigned int font_index = prop.collection_number.value_or(0); + assert(is_valid(font, font_index)); + return font.infos[font_index]; +} + +int Emboss::get_line_height(const FontFile &font, const FontProp &prop) { + const FontFile::Info &info = get_font_info(font, prop); + int line_height = info.ascent - info.descent + info.linegap; + line_height += prop.line_gap.value_or(0); + return static_cast(line_height / SHAPE_SCALE); +} + +namespace { +ExPolygons letter2shapes( + wchar_t letter, Point &cursor, FontFileWithCache &font_with_cache, const FontProp &font_prop, fontinfo_opt& font_info_cache) +{ + assert(font_with_cache.has_value()); + if (!font_with_cache.has_value()) + return {}; + + Glyphs &cache = *font_with_cache.cache; + const FontFile &font = *font_with_cache.font_file; + + if (letter == '\n') { + cursor.x() = 0; + // 2d shape has opposit direction of y + cursor.y() -= get_line_height(font, font_prop); + return {}; + } + if (letter == '\t') { + // '\t' = 4*space => same as imgui + const int count_spaces = 4; + const Glyph *space = get_glyph(int(' '), font, font_prop, cache, font_info_cache); + if (space == nullptr) + return {}; + cursor.x() += count_spaces * space->advance_width; + return {}; + } + if (letter == '\r') + return {}; + + int unicode = static_cast(letter); + auto it = cache.find(unicode); + + // Create glyph from font file and cache it + const Glyph *glyph_ptr = (it != cache.end()) ? &it->second : get_glyph(unicode, font, font_prop, cache, font_info_cache); + if (glyph_ptr == nullptr) + return {}; + + // move glyph to cursor position + ExPolygons expolygons = glyph_ptr->shape; // copy + for (ExPolygon &expolygon : expolygons) + expolygon.translate(cursor); + + cursor.x() += glyph_ptr->advance_width; + return expolygons; +} + +// Check cancel every X letters in text +// Lower number - too much checks(slows down) +// Higher number - slows down response on cancelation +const int CANCEL_CHECK = 10; +} // namespace + +namespace { +HealedExPolygons union_with_delta(const ExPolygonsWithIds &shapes, float delta, unsigned max_heal_iteration) +{ + // unify to one expolygons + ExPolygons expolygons; + for (const ExPolygonsWithId &shape : shapes) { + if (shape.expoly.empty()) + continue; + expolygons_append(expolygons, offset_ex(shape.expoly, delta)); + } + ExPolygons result = union_ex(expolygons); + result = offset_ex(result, -delta); + bool is_healed = heal_expolygons(result, max_heal_iteration); + return {result, is_healed}; +} +} // namespace + +ExPolygons Slic3r::union_with_delta(EmbossShape &shape, float delta, unsigned max_heal_iteration) +{ + if (!shape.final_shape.expolygons.empty()) + return shape.final_shape; + + shape.final_shape = ::union_with_delta(shape.shapes_with_ids, delta, max_heal_iteration); + for (const ExPolygonsWithId &e : shape.shapes_with_ids) + if (!e.is_healed) + shape.final_shape.is_healed = false; + return shape.final_shape.expolygons; +} + +void Slic3r::translate(ExPolygonsWithIds &expolygons_with_ids, const Point &p) +{ + for (ExPolygonsWithId &expolygons_with_id : expolygons_with_ids) + translate(expolygons_with_id.expoly, p); +} + +BoundingBox Slic3r::get_extents(const ExPolygonsWithIds &expolygons_with_ids) +{ + BoundingBox bb; + for (const ExPolygonsWithId &expolygons_with_id : expolygons_with_ids) + bb.merge(get_extents(expolygons_with_id.expoly)); + return bb; +} + +void Slic3r::center(ExPolygonsWithIds &e) +{ + BoundingBox bb = get_extents(e); + translate(e, -bb.center()); +} + +HealedExPolygons Emboss::text2shapes(FontFileWithCache &font_with_cache, const char *text, const FontProp &font_prop, const std::function& was_canceled) +{ + std::wstring text_w = boost::nowide::widen(text); + ExPolygonsWithIds vshapes = text2vshapes(font_with_cache, text_w, font_prop, was_canceled); + + float delta = static_cast(1. / SHAPE_SCALE); + return ::union_with_delta(vshapes, delta, MAX_HEAL_ITERATION_OF_TEXT); +} + +namespace { +/// +/// Align shape against pivot +/// +/// Shapes to align +/// Prerequisities: shapes are aligned left top +/// To detect end of lines - to be able horizontal center the line +/// Containe Horizontal and vertical alignment +/// Needed for scale and font size +void align_shape(ExPolygonsWithIds &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font); +} + +ExPolygonsWithIds Emboss::text2vshapes(FontFileWithCache &font_with_cache, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled){ + assert(font_with_cache.has_value()); + const FontFile &font = *font_with_cache.font_file; + unsigned int font_index = font_prop.collection_number.value_or(0); + if (!is_valid(font, font_index)) + return {}; + + unsigned counter = 0; + Point cursor(0, 0); + + fontinfo_opt font_info_cache; + ExPolygonsWithIds result; + result.reserve(text.size()); + for (wchar_t letter : text) { + if (++counter == CANCEL_CHECK) { + counter = 0; + if (was_canceled()) + return {}; + } + unsigned id = static_cast(letter); + result.push_back({id, letter2shapes(letter, cursor, font_with_cache, font_prop, font_info_cache)}); + } + + align_shape(result, text, font_prop, font); + return result; +} + +#include +unsigned Emboss::get_count_lines(const std::wstring& ws) +{ + if (ws.empty()) + return 0; + + unsigned count = 1; + for (wchar_t wc : ws) + if (wc == '\n') + ++count; + return count; + + // unsigned prev_count = 0; + // for (wchar_t wc : ws) + // if (wc == '\n') + // ++prev_count; + // else + // break; + // + // unsigned post_count = 0; + // for (wchar_t wc : boost::adaptors::reverse(ws)) + // if (wc == '\n') + // ++post_count; + // else + // break; + //return count - prev_count - post_count; +} + +unsigned Emboss::get_count_lines(const std::string &text) +{ + std::wstring ws = boost::nowide::widen(text.c_str()); + return get_count_lines(ws); +} + +unsigned Emboss::get_count_lines(const ExPolygonsWithIds &shapes) { + if (shapes.empty()) + return 0; // no glyphs + unsigned result = 1; // one line is minimum + for (const ExPolygonsWithId &shape_id : shapes) + if (shape_id.id == ENTER_UNICODE) + ++result; + return result; +} + +void Emboss::apply_transformation(const std::optional& angle, const std::optional& distance, Transform3d &transformation) { + if (angle.has_value()) { + double angle_z = *angle; + transformation *= Eigen::AngleAxisd(angle_z, Vec3d::UnitZ()); + } + if (distance.has_value()) { + Vec3d translate = Vec3d::UnitZ() * (*distance); + transformation.translate(translate); + } +} + +bool Emboss::is_italic(const FontFile &font, unsigned int font_index) +{ + if (font_index >= font.infos.size()) return false; + fontinfo_opt font_info_opt = load_font_info(font.data->data(), font_index); + + if (!font_info_opt.has_value()) return false; + stbtt_fontinfo *info = &(*font_info_opt); + + // https://docs.microsoft.com/cs-cz/typography/opentype/spec/name + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + // 2 ==> Style / Subfamily name + int name_id = 2; + int length; + const char* value = stbtt_GetFontNameString(info, &length, + STBTT_PLATFORM_ID_MICROSOFT, + STBTT_MS_EID_UNICODE_BMP, + STBTT_MS_LANG_ENGLISH, + name_id); + + // value is big endian utf-16 i need extract only normal chars + std::string value_str; + value_str.reserve(length / 2); + for (int i = 1; i < length; i += 2) + value_str.push_back(value[i]); + + // lower case + std::transform(value_str.begin(), value_str.end(), value_str.begin(), + [](unsigned char c) { return std::tolower(c); }); + + const std::vector italics({"italic", "oblique"}); + for (const std::string &it : italics) { + if (value_str.find(it) != std::string::npos) { + return true; + } + } + return false; +} + +std::string Emboss::create_range_text(const std::string &text, + const FontFile &font, + unsigned int font_index, + bool *exist_unknown) +{ + if (!is_valid(font, font_index)) return {}; + + std::wstring ws = boost::nowide::widen(text); + + // need remove symbols not contained in font + std::sort(ws.begin(), ws.end()); + + auto font_info_opt = load_font_info(font.data->data(), 0); + if (!font_info_opt.has_value()) return {}; + const stbtt_fontinfo *font_info = &(*font_info_opt); + + if (exist_unknown != nullptr) *exist_unknown = false; + int prev_unicode = -1; + ws.erase(std::remove_if(ws.begin(), ws.end(), + [&prev_unicode, font_info, exist_unknown](wchar_t wc) -> bool { + int unicode = static_cast(wc); + + // skip white spaces + if (unicode == '\n' || + unicode == '\r' || + unicode == '\t') return true; + + // is duplicit? + if (prev_unicode == unicode) return true; + prev_unicode = unicode; + + // can find in font? + bool is_unknown = !stbtt_FindGlyphIndex(font_info, unicode); + if (is_unknown && exist_unknown != nullptr) + *exist_unknown = true; + return is_unknown; + }), ws.end()); + + return boost::nowide::narrow(ws); +} + +double Emboss::get_text_shape_scale(const FontProp &fp, const FontFile &ff) +{ + const FontFile::Info &info = get_font_info(ff, fp); + double scale = fp.size_in_mm / (double) info.unit_per_em; + // Shape is scaled for store point coordinate as integer + return scale * SHAPE_SCALE; +} + +namespace { + +void add_quad(uint32_t i1, + uint32_t i2, + indexed_triangle_set &result, + uint32_t count_point) +{ + // bottom indices + uint32_t i1_ = i1 + count_point; + uint32_t i2_ = i2 + count_point; + result.indices.emplace_back(i2, i2_, i1); + result.indices.emplace_back(i1_, i1, i2_); +}; + +indexed_triangle_set polygons2model_unique( + const ExPolygons &shape2d, + const IProjection &projection, + const Points &points) +{ + // CW order of triangle indices + std::vector shape_triangles=Triangulation::triangulate(shape2d, points); + uint32_t count_point = points.size(); + + indexed_triangle_set result; + result.vertices.reserve(2 * count_point); + std::vector &front_points = result.vertices; // alias + std::vector back_points; + back_points.reserve(count_point); + + for (const Point &p : points) { + auto p2 = projection.create_front_back(p); + front_points.push_back(p2.first.cast()); + back_points.push_back(p2.second.cast()); + } + + // insert back points, front are already in + result.vertices.insert(result.vertices.end(), + std::make_move_iterator(back_points.begin()), + std::make_move_iterator(back_points.end())); + result.indices.reserve(shape_triangles.size() * 2 + points.size() * 2); + // top triangles - change to CCW + for (const Vec3i32 &t : shape_triangles) + result.indices.emplace_back(t.x(), t.z(), t.y()); + // bottom triangles - use CW + for (const Vec3i32 &t : shape_triangles) + result.indices.emplace_back(t.x() + count_point, + t.y() + count_point, + t.z() + count_point); + + // quads around - zig zag by triangles + size_t polygon_offset = 0; + auto add_quads = [&polygon_offset,&result, &count_point] + (const Polygon& polygon) { + uint32_t polygon_points = polygon.points.size(); + // previous index + uint32_t prev = polygon_offset + polygon_points - 1; + for (uint32_t p = 0; p < polygon_points; ++p) { + uint32_t index = polygon_offset + p; + add_quad(prev, index, result, count_point); + prev = index; + } + polygon_offset += polygon_points; + }; + + for (const ExPolygon &expolygon : shape2d) { + add_quads(expolygon.contour); + for (const Polygon &hole : expolygon.holes) add_quads(hole); + } + + return result; +} + +indexed_triangle_set polygons2model_duplicit( + const ExPolygons &shape2d, + const IProjection &projection, + const Points &points, + const Points &duplicits) +{ + // CW order of triangle indices + std::vector changes = Triangulation::create_changes(points, duplicits); + std::vector shape_triangles = Triangulation::triangulate(shape2d, points, changes); + uint32_t count_point = *std::max_element(changes.begin(), changes.end()) + 1; + + indexed_triangle_set result; + result.vertices.reserve(2 * count_point); + std::vector &front_points = result.vertices; + std::vector back_points; + back_points.reserve(count_point); + + uint32_t max_index = std::numeric_limits::max(); + for (uint32_t i = 0; i < changes.size(); ++i) { + uint32_t index = changes[i]; + if (max_index != std::numeric_limits::max() && + index <= max_index) continue; // duplicit point + assert(index == max_index + 1); + assert(front_points.size() == index); + assert(back_points.size() == index); + max_index = index; + const Point &p = points[i]; + auto p2 = projection.create_front_back(p); + front_points.push_back(p2.first.cast()); + back_points.push_back(p2.second.cast()); + } + assert(max_index+1 == count_point); + + // insert back points, front are already in + result.vertices.insert(result.vertices.end(), + std::make_move_iterator(back_points.begin()), + std::make_move_iterator(back_points.end())); + + result.indices.reserve(shape_triangles.size() * 2 + points.size() * 2); + // top triangles - change to CCW + for (const Vec3i32 &t : shape_triangles) + result.indices.emplace_back(t.x(), t.z(), t.y()); + // bottom triangles - use CW + for (const Vec3i32 &t : shape_triangles) + result.indices.emplace_back(t.x() + count_point, t.y() + count_point, + t.z() + count_point); + + // quads around - zig zag by triangles + size_t polygon_offset = 0; + auto add_quads = [&polygon_offset, &result, count_point, &changes] + (const Polygon &polygon) { + uint32_t polygon_points = polygon.points.size(); + // previous index + uint32_t prev = changes[polygon_offset + polygon_points - 1]; + for (uint32_t p = 0; p < polygon_points; ++p) { + uint32_t index = changes[polygon_offset + p]; + if (prev == index) continue; + add_quad(prev, index, result, count_point); + prev = index; + } + polygon_offset += polygon_points; + }; + + for (const ExPolygon &expolygon : shape2d) { + add_quads(expolygon.contour); + for (const Polygon &hole : expolygon.holes) add_quads(hole); + } + return result; +} +} // namespace + +indexed_triangle_set Emboss::polygons2model(const ExPolygons &shape2d, + const IProjection &projection) +{ + Points points = to_points(shape2d); + Points duplicits = collect_duplicates(points); + return (duplicits.empty()) ? + polygons2model_unique(shape2d, projection, points) : + polygons2model_duplicit(shape2d, projection, points, duplicits); +} + +std::pair Emboss::ProjectZ::create_front_back(const Point &p) const +{ + Vec3d front(p.x(), p.y(), 0.); + return std::make_pair(front, project(front)); +} + +Vec3d Emboss::ProjectZ::project(const Vec3d &point) const +{ + Vec3d res = point; // copy + res.z() = m_depth; + return res; +} + +std::optional Emboss::ProjectZ::unproject(const Vec3d &p, double *depth) const { + return Vec2d(p.x(), p.y()); +} + + +Vec3d Emboss::suggest_up(const Vec3d normal, double up_limit) +{ + // Normal must be 1 + assert(is_approx(normal.squaredNorm(), 1.)); + + // wanted up direction of result + Vec3d wanted_up_side = + (std::fabs(normal.z()) > up_limit)? + Vec3d::UnitY() : Vec3d::UnitZ(); + + // create perpendicular unit vector to surface triangle normal vector + // lay on surface of triangle and define up vector for text + Vec3d wanted_up_dir = normal.cross(wanted_up_side).cross(normal); + // normal3d is NOT perpendicular to normal_up_dir + wanted_up_dir.normalize(); + + return wanted_up_dir; +} + +std::optional Emboss::calc_up(const Transform3d &tr, double up_limit) +{ + auto tr_linear = tr.linear(); + // z base of transformation ( tr * UnitZ ) + Vec3d normal = tr_linear.col(2); + // scaled matrix has base with different size + normal.normalize(); + Vec3d suggested = suggest_up(normal, up_limit); + assert(is_approx(suggested.squaredNorm(), 1.)); + + Vec3d up = tr_linear.col(1); // tr * UnitY() + up.normalize(); + Matrix3d m; + m.row(0) = up; + m.row(1) = suggested; + m.row(2) = normal; + double det = m.determinant(); + double dot = suggested.dot(up); + double res = -atan2(det, dot); + if (is_approx(res, 0.)) + return {}; + return res; +} + +Transform3d Emboss::create_transformation_onto_surface(const Vec3d &position, + const Vec3d &normal, + double up_limit) +{ + // is normalized ? + assert(is_approx(normal.squaredNorm(), 1.)); + + // up and emboss direction for generated model + Vec3d up_dir = Vec3d::UnitY(); + Vec3d emboss_dir = Vec3d::UnitZ(); + + // after cast from float it needs to be normalized again + Vec3d wanted_up_dir = suggest_up(normal, up_limit); + + // perpendicular to emboss vector of text and normal + Vec3d axis_view; + double angle_view; + if (normal == -Vec3d::UnitZ()) { + // text_emboss_dir has opposit direction to wanted_emboss_dir + axis_view = Vec3d::UnitY(); + angle_view = M_PI; + } else { + axis_view = emboss_dir.cross(normal); + angle_view = std::acos(emboss_dir.dot(normal)); // in rad + axis_view.normalize(); + } + + Eigen::AngleAxis view_rot(angle_view, axis_view); + Vec3d wanterd_up_rotated = view_rot.matrix().inverse() * wanted_up_dir; + wanterd_up_rotated.normalize(); + double angle_up = std::acos(up_dir.dot(wanterd_up_rotated)); + + Vec3d text_view = up_dir.cross(wanterd_up_rotated); + Vec3d diff_view = emboss_dir - text_view; + if (std::fabs(diff_view.x()) > 1. || + std::fabs(diff_view.y()) > 1. || + std::fabs(diff_view.z()) > 1.) // oposit direction + angle_up *= -1.; + + Eigen::AngleAxis up_rot(angle_up, emboss_dir); + + Transform3d transform = Transform3d::Identity(); + transform.translate(position); + transform.rotate(view_rot); + transform.rotate(up_rot); + return transform; +} + + +// OrthoProject + +std::pair Emboss::OrthoProject::create_front_back(const Point &p) const { + Vec3d front(p.x(), p.y(), 0.); + Vec3d front_tr = m_matrix * front; + return std::make_pair(front_tr, project(front_tr)); +} + +Vec3d Emboss::OrthoProject::project(const Vec3d &point) const +{ + return point + m_direction; +} + +std::optional Emboss::OrthoProject::unproject(const Vec3d &p, double *depth) const +{ + Vec3d pp = m_matrix_inv * p; + if (depth != nullptr) *depth = pp.z(); + return Vec2d(pp.x(), pp.y()); +} + +// sample slice +namespace { + +// using coor2 = int64_t; +using Coord2 = double; +using P2 = Eigen::Matrix; + +bool point_in_distance(const Coord2 &distance_sq, PolygonPoint &polygon_point, const size_t &i, const Slic3r::Polygon &polygon, bool is_first, bool is_reverse = false) +{ + size_t s = polygon.size(); + size_t ii = (i + polygon_point.index) % s; + + // second point of line + const Point &p = polygon[ii]; + Point p_d = p - polygon_point.point; + + P2 p_d2 = p_d.cast(); + Coord2 p_distance_sq = p_d2.squaredNorm(); + if (p_distance_sq < distance_sq) + return false; + + // found line + if (is_first) { + // on same line + // center also lay on line + // new point is distance moved from point by direction + polygon_point.point += p_d * sqrt(distance_sq / p_distance_sq); + return true; + } + + // line cross circle + + // start point of line + size_t ii2 = (is_reverse) ? (ii + 1) % s : (ii + s - 1) % s; + polygon_point.index = (is_reverse) ? ii : ii2; + const Point &p2 = polygon[ii2]; + + Point line_dir = p2 - p; + P2 line_dir2 = line_dir.cast(); + + Coord2 a = line_dir2.dot(line_dir2); + Coord2 b = 2 * p_d2.dot(line_dir2); + Coord2 c = p_d2.dot(p_d2) - distance_sq; + + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) { + assert(false); + // no intersection + polygon_point.point = p; + return true; + } + + // ray didn't totally miss sphere, + // so there is a solution to + // the equation. + discriminant = sqrt(discriminant); + + // either solution may be on or off the ray so need to test both + // t1 is always the smaller value, because BOTH discriminant and + // a are nonnegative. + double t1 = (-b - discriminant) / (2 * a); + double t2 = (-b + discriminant) / (2 * a); + + double t = std::min(t1, t2); + if (t < 0. || t > 1.) { + // Bad intersection + assert(false); + polygon_point.point = p; + return true; + } + + polygon_point.point = p + (t * line_dir2).cast(); + return true; +} + +void point_in_distance(int32_t distance, PolygonPoint &p, const Slic3r::Polygon &polygon) +{ + Coord2 distance_sq = static_cast(distance) * distance; + bool is_first = true; + for (size_t i = 1; i < polygon.size(); ++i) { + if (point_in_distance(distance_sq, p, i, polygon, is_first)) + return; + is_first = false; + } + // There is not point on polygon with this distance +} + +void point_in_reverse_distance(int32_t distance, PolygonPoint &p, const Slic3r::Polygon &polygon) +{ + Coord2 distance_sq = static_cast(distance) * distance; + bool is_first = true; + bool is_reverse = true; + for (size_t i = polygon.size(); i > 0; --i) { + if (point_in_distance(distance_sq, p, i, polygon, is_first, is_reverse)) + return; + is_first = false; + } + // There is not point on polygon with this distance +} +} // namespace + +// calculate rotation, need copy of polygon point +double Emboss::calculate_angle(int32_t distance, PolygonPoint polygon_point, const Polygon &polygon) +{ + PolygonPoint polygon_point2 = polygon_point; // copy + point_in_distance(distance, polygon_point, polygon); + point_in_reverse_distance(distance, polygon_point2, polygon); + + Point surface_dir = polygon_point2.point - polygon_point.point; + Point norm(-surface_dir.y(), surface_dir.x()); + Vec2d norm_d = norm.cast(); + //norm_d.normalize(); + return std::atan2(norm_d.y(), norm_d.x()); +} + +std::vector Emboss::calculate_angles(int32_t distance, const PolygonPoints& polygon_points, const Polygon &polygon) +{ + std::vector result; + result.reserve(polygon_points.size()); + for(const PolygonPoint& pp: polygon_points) + result.emplace_back(calculate_angle(distance, pp, polygon)); + return result; +} + +PolygonPoints Emboss::sample_slice(const TextLine &slice, const BoundingBoxes &bbs, double scale) +{ + // find BB in center of line + size_t first_right_index = 0; + for (const BoundingBox &bb : bbs) + if (!bb.defined) // white char do not have bb + continue; + else if (bb.min.x() < 0) + ++first_right_index; + else + break; + + PolygonPoints samples(bbs.size()); + int32_t shapes_x_cursor = 0; + + PolygonPoint cursor = slice.start; //copy + + auto create_sample = [&] //polygon_cursor, &polygon_line_index, &line_bbs, &shapes_x_cursor, &shape_scale, &em_2_polygon, &line, &offsets] + (const BoundingBox &bb, bool is_reverse) { + if (!bb.defined) + return cursor; + Point letter_center = bb.center(); + int32_t shape_distance = shapes_x_cursor - letter_center.x(); + shapes_x_cursor = letter_center.x(); + double distance_mm = shape_distance * scale; + int32_t distance_polygon = static_cast(std::round(scale_(distance_mm))); + if (is_reverse) + point_in_distance(distance_polygon, cursor, slice.polygon); + else + point_in_reverse_distance(distance_polygon, cursor, slice.polygon); + return cursor; + }; + + // calc transformation for letters on the Right side from center + bool is_reverse = true; + for (size_t index = first_right_index; index < bbs.size(); ++index) + samples[index] = create_sample(bbs[index], is_reverse); + + // calc transformation for letters on the Left side from center + if (first_right_index < bbs.size()) { + shapes_x_cursor = bbs[first_right_index].center().x(); + cursor = samples[first_right_index]; + }else{ + // only left side exists + shapes_x_cursor = 0; + cursor = slice.start; // copy + } + is_reverse = false; + for (size_t index_plus_one = first_right_index; index_plus_one > 0; --index_plus_one) { + size_t index = index_plus_one - 1; + samples[index] = create_sample(bbs[index], is_reverse); + } + return samples; +} + +namespace { +float get_align_y_offset(FontProp::VerticalAlign align, unsigned count_lines, const FontFile &ff, const FontProp &fp) +{ + assert(count_lines != 0); + int line_height = get_line_height(ff, fp); + int ascent = get_font_info(ff, fp).ascent / SHAPE_SCALE; + float line_center = static_cast(std::round(ascent * ASCENT_CENTER)); + + // direction of Y in 2d is from top to bottom + // zero is on base line of first line + switch (align) { + case FontProp::VerticalAlign::bottom: return line_height * (count_lines - 1); + case FontProp::VerticalAlign::top: return -ascent; + case FontProp::VerticalAlign::center: + default: + return -line_center + line_height * (count_lines - 1) / 2.; + } +} + +int32_t get_align_x_offset(FontProp::HorizontalAlign align, const BoundingBox &shape_bb, const BoundingBox &line_bb) +{ + switch (align) { + case FontProp::HorizontalAlign::right: return -shape_bb.max.x() + (shape_bb.size().x() - line_bb.size().x()); + case FontProp::HorizontalAlign::center: return -shape_bb.center().x() + (shape_bb.size().x() - line_bb.size().x()) / 2; + case FontProp::HorizontalAlign::left: // no change + default: break; + } + return 0; +} + +void align_shape(ExPolygonsWithIds &shapes, const std::wstring &text, const FontProp &prop, const FontFile &font) +{ + // Shapes have to match letters in text + assert(shapes.size() == text.length()); + + unsigned count_lines = get_count_lines(text); + int y_offset = get_align_y_offset(prop.align.second, count_lines, font, prop); + + // Speed up for left aligned text + if (prop.align.first == FontProp::HorizontalAlign::left){ + // already horizontaly aligned + for (ExPolygonsWithId& shape : shapes) + for (ExPolygon &s : shape.expoly) + s.translate(Point(0, y_offset)); + return; + } + + BoundingBox shape_bb; + for (const ExPolygonsWithId& shape: shapes) + shape_bb.merge(get_extents(shape.expoly)); + + auto get_line_bb = [&](size_t j) { + BoundingBox line_bb; + for (; j < text.length() && text[j] != '\n'; ++j) + line_bb.merge(get_extents(shapes[j].expoly)); + return line_bb; + }; + + // Align x line by line + Point offset( + get_align_x_offset(prop.align.first, shape_bb, get_line_bb(0)), + y_offset); + for (size_t i = 0; i < shapes.size(); ++i) { + wchar_t letter = text[i]; + if (letter == '\n'){ + offset.x() = get_align_x_offset(prop.align.first, shape_bb, get_line_bb(i + 1)); + continue; + } + ExPolygons &shape = shapes[i].expoly; + for (ExPolygon &s : shape) + s.translate(offset); + } +} +} // namespace + +double Emboss::get_align_y_offset_in_mm(FontProp::VerticalAlign align, unsigned count_lines, const FontFile &ff, const FontProp &fp){ + float offset_in_font_point = get_align_y_offset(align, count_lines, ff, fp); + double scale = get_text_shape_scale(fp, ff); + return scale * offset_in_font_point; +} + +#ifdef REMOVE_SPIKES +#include +void remove_spikes(Polygon &polygon, const SpikeDesc &spike_desc) +{ + enum class Type { + add, // Move with point B on A-side and add new point on C-side + move, // Only move with point B + erase // left only points A and C without move + }; + struct SpikeHeal + { + Type type; + size_t index; + Point b; + Point add; + }; + using SpikeHeals = std::vector; + SpikeHeals heals; + + size_t count = polygon.size(); + if (count < 3) + return; + + const Point *ptr_a = &polygon[count - 2]; + const Point *ptr_b = &polygon[count - 1]; + for (const Point &c : polygon) { + const Point &a = *ptr_a; + const Point &b = *ptr_b; + ScopeGuard sg([&ptr_a, &ptr_b, &c]() { + // prepare for next loop + ptr_a = ptr_b; + ptr_b = &c; + }); + + // calc sides + Point ba = a - b; + Point bc = c - b; + + Vec2d ba_f = ba.cast(); + Vec2d bc_f = bc.cast(); + double dot_product = ba_f.dot(bc_f); + + // sqrt together after multiplication save one sqrt + double ba_size_sq = ba_f.squaredNorm(); + double bc_size_sq = bc_f.squaredNorm(); + double norm = sqrt(ba_size_sq * bc_size_sq); + double cos_angle = dot_product / norm; + + // small angle are around 1 --> cos(0) = 1 + if (cos_angle < spike_desc.cos_angle) + continue; + + SpikeHeal heal; + heal.index = &b - &polygon.points.front(); + + // has to be in range <-1, 1> + // Due to preccission of floating point number could be sligtly out of range + if (cos_angle > 1.) + cos_angle = 1.; + if (cos_angle < -1.) + cos_angle = -1.; + + // Current Spike angle + double angle = acos(cos_angle); + double wanted_size = spike_desc.half_bevel / cos(angle / 2.); + double wanted_size_sq = wanted_size * wanted_size; + + bool is_ba_short = ba_size_sq < wanted_size_sq; + bool is_bc_short = bc_size_sq < wanted_size_sq; + auto a_side = [&b, &ba_f, &ba_size_sq, &wanted_size]() { + Vec2d ba_norm = ba_f / sqrt(ba_size_sq); + return b + (wanted_size * ba_norm).cast(); + }; + auto c_side = [&b, &bc_f, &bc_size_sq, &wanted_size]() { + Vec2d bc_norm = bc_f / sqrt(bc_size_sq); + return b + (wanted_size * bc_norm).cast(); + }; + if (is_ba_short && is_bc_short) { + // remove short spike + heal.type = Type::erase; + } else if (is_ba_short){ + // move point B on C-side + heal.type = Type::move; + heal.b = c_side(); + } else if (is_bc_short) { + // move point B on A-side + heal.type = Type::move; + heal.b = a_side(); + } else { + // move point B on A-side and add point on C-side + heal.type = Type::add; + heal.b = a_side(); + heal.add = c_side(); + } + heals.push_back(heal); + } + + if (heals.empty()) + return; + + // sort index from high to low + if (heals.front().index == (count - 1)) + std::rotate(heals.begin(), heals.begin()+1, heals.end()); + std::reverse(heals.begin(), heals.end()); + + int extend = 0; + int curr_extend = 0; + for (const SpikeHeal &h : heals) + switch (h.type) { + case Type::add: + ++curr_extend; + if (extend < curr_extend) + extend = curr_extend; + break; + case Type::erase: + --curr_extend; + } + + Points &pts = polygon.points; + if (extend > 0) + pts.reserve(pts.size() + extend); + + for (const SpikeHeal &h : heals) { + switch (h.type) { + case Type::add: + pts[h.index] = h.b; + pts.insert(pts.begin() + h.index+1, h.add); + break; + case Type::erase: + pts.erase(pts.begin() + h.index); + break; + case Type::move: + pts[h.index] = h.b; + break; + default: break; + } + } +} + +void remove_spikes(Polygons &polygons, const SpikeDesc &spike_desc) +{ + for (Polygon &polygon : polygons) + remove_spikes(polygon, spike_desc); + remove_bad(polygons); +} + +void remove_spikes(ExPolygons &expolygons, const SpikeDesc &spike_desc) +{ + for (ExPolygon &expolygon : expolygons) { + remove_spikes(expolygon.contour, spike_desc); + remove_spikes(expolygon.holes, spike_desc); + } + remove_bad(expolygons); +} + +#endif // REMOVE_SPIKES diff --git a/src/libslic3r/Emboss.hpp b/src/libslic3r/Emboss.hpp new file mode 100644 index 000000000..c26177c01 --- /dev/null +++ b/src/libslic3r/Emboss.hpp @@ -0,0 +1,473 @@ +#ifndef slic3r_Emboss_hpp_ +#define slic3r_Emboss_hpp_ + +#include +#include +#include +#include +#include // indexed_triangle_set +#include "Polygon.hpp" +#include "ExPolygon.hpp" +#include "EmbossShape.hpp" // ExPolygonsWithIds +#include "BoundingBox.hpp" +#include "TextConfiguration.hpp" + +namespace Slic3r { + +/// +/// class with only static function add ability to engraved OR raised +/// text OR polygons onto model surface +/// +namespace Emboss +{ + static const float UNION_DELTA = 50.0f; // [approx in nano meters depends on volume scale] + static const unsigned UNION_MAX_ITERATIN = 10; // [count] + + /// + /// Collect fonts registred inside OS + /// + /// OS registred TTF font files(full path) with names + EmbossStyles get_font_list(); +#ifdef _WIN32 + EmbossStyles get_font_list_by_register(); + EmbossStyles get_font_list_by_enumeration(); + EmbossStyles get_font_list_by_folder(); +#endif + + /// + /// OS dependent function to get location of font by its name descriptor + /// + /// Unique identificator for font + /// File path to font when found + std::optional get_font_path(const std::wstring &font_face_name); + + // description of one letter + struct Glyph + { + // NOTE: shape is scaled by SHAPE_SCALE + // to be able store points without floating points + ExPolygons shape; + + // values are in font points + int advance_width=0, left_side_bearing=0; + }; + // cache for glyph by unicode + using Glyphs = std::map; + + /// + /// keep information from file about font + /// (store file data itself) + /// + cache data readed from buffer + /// + struct FontFile + { + // loaded data from font file + // must store data size for imgui rasterization + // To not store data on heap and To prevent unneccesary copy + // data are stored inside unique_ptr + std::unique_ptr> data; + + struct Info + { + // vertical position is "scale*(ascent - descent + lineGap)" + int ascent, descent, linegap; + + // for convert font units to pixel + int unit_per_em; + }; + // info for each font in data + std::vector infos; + + FontFile(std::unique_ptr> data, + std::vector &&infos) + : data(std::move(data)), infos(std::move(infos)) + { + assert(this->data != nullptr); + assert(!this->data->empty()); + } + + bool operator==(const FontFile &other) const { + if (data->size() != other.data->size()) + return false; + //if(*data != *other.data) return false; + for (size_t i = 0; i < infos.size(); i++) + if (infos[i].ascent != other.infos[i].ascent || + infos[i].descent == other.infos[i].descent || + infos[i].linegap == other.infos[i].linegap) + return false; + return true; + } + }; + + /// + /// Add caching for shape of glyphs + /// + struct FontFileWithCache + { + // Pointer on data of the font file + std::shared_ptr font_file; + + // Cache for glyph shape + // IMPORTANT: accessible only in plater job thread !!! + // main thread only clear cache by set to another shared_ptr + std::shared_ptr cache; + + FontFileWithCache() : font_file(nullptr), cache(nullptr) {} + explicit FontFileWithCache(std::unique_ptr font_file) + : font_file(std::move(font_file)) + , cache(std::make_shared()) + {} + bool has_value() const { return font_file != nullptr && cache != nullptr; } + }; + + /// + /// Load font file into buffer + /// + /// Location of .ttf or .ttc font file + /// Font object when loaded. + std::unique_ptr create_font_file(const char *file_path); + // data = raw file data + std::unique_ptr create_font_file(std::unique_ptr> data); +#ifdef _WIN32 + // fix for unknown pointer HFONT is replaced with "void *" + void * can_load(void* hfont); + std::unique_ptr create_font_file(void * hfont); +#endif // _WIN32 + + /// + /// convert letter into polygons + /// + /// Define fonts + /// Index of font in collection + /// One character defined by unicode codepoint + /// Precision of lettter outline curve in conversion to lines + /// inner polygon cw(outer ccw) + std::optional letter2glyph(const FontFile &font, unsigned int font_index, int letter, float flatness); + + /// + /// Convert text into polygons + /// + /// Define fonts + cache, which could extend + /// Characters to convert + /// User defined property of the font + /// Way to interupt processing + /// Inner polygon cw(outer ccw) + HealedExPolygons text2shapes (FontFileWithCache &font, const char *text, const FontProp &font_prop, const std::function &was_canceled = []() {return false;}); + ExPolygonsWithIds text2vshapes(FontFileWithCache &font, const std::wstring& text, const FontProp &font_prop, const std::function& was_canceled = []() {return false;}); + + const unsigned ENTER_UNICODE = static_cast('\n'); + /// Sum of character '\n' + unsigned get_count_lines(const std::wstring &ws); + unsigned get_count_lines(const std::string &text); + unsigned get_count_lines(const ExPolygonsWithIds &shape); + + /// + /// Fix duplicit points and self intersections in polygons. + /// Also try to reduce amount of points and remove useless polygon parts + /// + /// Fill type ClipperLib::pftNonZero for overlapping otherwise + /// Look at heal_expolygon()::max_iteration + /// Healed shapes with flag is fully healed + HealedExPolygons heal_polygons(const Polygons &shape, bool is_non_zero = true, unsigned max_iteration = 10); + + /// + /// NOTE: call Slic3r::union_ex before this call + /// + /// Heal (read: Fix) issues in expolygons: + /// - self intersections + /// - duplicit points + /// - points close to line segments + /// + /// In/Out shape to heal + /// Heal could create another issue, + /// After healing it is checked again until shape is good or maximal count of iteration + /// True when shapes is good otherwise False + bool heal_expolygons(ExPolygons &shape, unsigned max_iteration = 10); + + /// + /// Divide line segments in place near to point + /// (which could lead to self intersection due to preccision) + /// Remove same neighbors + /// Note: Possible part of heal shape + /// + /// Expolygon to edit + /// (epsilon)Euclidean distance from point to line which divide line + /// True when some division was made otherwise false + bool divide_segments_for_close_point(ExPolygons &expolygons, double distance); + + /// + /// Use data from font property to modify transformation + /// + /// Z-rotation as angle to Y axis + /// Z-move as surface distance + /// In / Out transformation to modify by property + void apply_transformation(const std::optional &angle, const std::optional &distance, Transform3d &transformation); + + /// + /// Read information from naming table of font file + /// search for italic (or oblique), bold italic (or bold oblique) + /// + /// Selector of font + /// Index of font in collection + /// True when the font description contains italic/obligue otherwise False + bool is_italic(const FontFile &font, unsigned int font_index); + + /// + /// Create unique character set from string with filtered from text with only character from font + /// + /// Source vector of glyphs + /// Font descriptor + /// Define font in collection + /// True when text contain glyph unknown in font + /// Unique set of character from text contained in font + std::string create_range_text(const std::string &text, const FontFile &font, unsigned int font_index, bool* exist_unknown = nullptr); + + /// + /// Calculate scale for glyph shape convert from shape points to mm + /// + /// Property of font + /// Font data + /// Conversion to mm + double get_text_shape_scale(const FontProp &fp, const FontFile &ff); + + /// + /// getter of font info by collection defined in prop + /// + /// Contain infos about all fonts(collections) in file + /// Index of collection + /// Ascent, descent, line gap + const FontFile::Info &get_font_info(const FontFile &font, const FontProp &prop); + + /// + /// Read from font file and properties height of line with spacing + /// + /// Infos for collections + /// Collection index + Additional line gap + /// Line height with spacing in scaled font points (same as ExPolygons) + int get_line_height(const FontFile &font, const FontProp &prop); + + /// + /// Calculate Vertical align + /// + /// Top | Center | Bottom + /// + /// Return align Y offset in mm + double get_align_y_offset_in_mm(FontProp::VerticalAlign align, unsigned count_lines, const FontFile &ff, const FontProp &fp); + + /// + /// Project spatial point + /// + class IProject3d + { + public: + virtual ~IProject3d() = default; + /// + /// Move point with respect to projection direction + /// e.g. Orthogonal projection will move with point by direction + /// e.g. Spherical projection need to use center of projection + /// + /// Spatial point coordinate + /// Projected spatial point + virtual Vec3d project(const Vec3d &point) const = 0; + }; + + /// + /// Project 2d point into space + /// Could be plane, sphere, cylindric, ... + /// + class IProjection : public IProject3d + { + public: + virtual ~IProjection() = default; + + /// + /// convert 2d point to 3d points + /// + /// 2d coordinate + /// + /// first - front spatial point + /// second - back spatial point + /// + virtual std::pair create_front_back(const Point &p) const = 0; + + /// + /// Back projection + /// + /// Point to project + /// [optional] Depth of 2d projected point. Be careful number is in 2d scale + /// Uprojected point when it is possible + virtual std::optional unproject(const Vec3d &p, double * depth = nullptr) const = 0; + }; + + /// + /// Create triangle model for text + /// + /// text or image + /// Define transformation from 2d to 3d(orientation, position, scale, ...) + /// Projected shape into space + indexed_triangle_set polygons2model(const ExPolygons &shape2d, const IProjection& projection); + + /// + /// Suggest wanted up vector of embossed text by emboss direction + /// + /// Normalized vector of emboss direction in world + /// Is compared with normal.z to suggest up direction + /// Wanted up vector + Vec3d suggest_up(const Vec3d normal, double up_limit = 0.9); + + /// + /// By transformation calculate angle between suggested and actual up vector + /// + /// Transformation of embossed volume in world + /// Is compared with normal.z to suggest up direction + /// Rotation of suggested up-vector[in rad] in the range [-Pi, Pi], When rotation is not zero + std::optional calc_up(const Transform3d &tr, double up_limit = 0.9); + + /// + /// Create transformation for emboss text object to lay on surface point + /// + /// Position of surface point + /// Normal of surface point + /// Is compared with normal.z to suggest up direction + /// Transformation onto surface point + Transform3d create_transformation_onto_surface( + const Vec3d &position, const Vec3d &normal, double up_limit = 0.9); + + class ProjectZ : public IProjection + { + public: + explicit ProjectZ(double depth) : m_depth(depth) {} + // Inherited via IProject + std::pair create_front_back(const Point &p) const override; + Vec3d project(const Vec3d &point) const override; + std::optional unproject(const Vec3d &p, double * depth = nullptr) const override; + double m_depth; + }; + + class ProjectScale : public IProjection + { + std::unique_ptr core; + double m_scale; + public: + ProjectScale(std::unique_ptr core, double scale) + : core(std::move(core)), m_scale(scale) + {} + + // Inherited via IProject + std::pair create_front_back(const Point &p) const override + { + auto res = core->create_front_back(p); + return std::make_pair(res.first * m_scale, res.second * m_scale); + } + Vec3d project(const Vec3d &point) const override{ + return core->project(point); + } + std::optional unproject(const Vec3d &p, double *depth = nullptr) const override { + auto res = core->unproject(p / m_scale, depth); + if (depth != nullptr) *depth *= m_scale; + return res; + } + }; + + class ProjectTransform : public IProjection + { + std::unique_ptr m_core; + Transform3d m_tr; + Transform3d m_tr_inv; + double z_scale; + public: + ProjectTransform(std::unique_ptr core, const Transform3d &tr) : m_core(std::move(core)), m_tr(tr) + { + m_tr_inv = m_tr.inverse(); + z_scale = (m_tr.linear() * Vec3d::UnitZ()).norm(); + } + + // Inherited via IProject + std::pair create_front_back(const Point &p) const override + { + auto [front, back] = m_core->create_front_back(p); + return std::make_pair(m_tr * front, m_tr * back); + } + Vec3d project(const Vec3d &point) const override{ + return m_core->project(point); + } + std::optional unproject(const Vec3d &p, double *depth = nullptr) const override { + auto res = m_core->unproject(m_tr_inv * p, depth); + if (depth != nullptr) + *depth *= z_scale; + return res; + } + }; + + class OrthoProject3d : public Emboss::IProject3d + { + // size and direction of emboss for ortho projection + Vec3d m_direction; + public: + OrthoProject3d(Vec3d direction) : m_direction(direction) {} + Vec3d project(const Vec3d &point) const override{ return point + m_direction;} + }; + + class OrthoProject: public Emboss::IProjection { + Transform3d m_matrix; + // size and direction of emboss for ortho projection + Vec3d m_direction; + Transform3d m_matrix_inv; + public: + OrthoProject(Transform3d matrix, Vec3d direction) + : m_matrix(matrix), m_direction(direction), m_matrix_inv(matrix.inverse()) + {} + // Inherited via IProject + std::pair create_front_back(const Point &p) const override; + Vec3d project(const Vec3d &point) const override; + std::optional unproject(const Vec3d &p, double * depth = nullptr) const override; + }; + + /// + /// Define polygon for draw letters + /// + struct TextLine + { + // slice of object + Polygon polygon; + + // point laying on polygon closest to zero + PolygonPoint start; + + // offset of text line in volume mm + float y; + }; + using TextLines = std::vector; + + /// + /// Sample slice polygon by bounding boxes centers + /// slice start point has shape_center_x coor + /// + /// Polygon and start point[Slic3r scaled milimeters] + /// Bounding boxes of letter on one line[in font scales] + /// Scale for bbs (after multiply bb is in milimeters) + /// Sampled polygon by bounding boxes + PolygonPoints sample_slice(const TextLine &slice, const BoundingBoxes &bbs, double scale); + + /// + /// Calculate angle for polygon point + /// + /// Distance for found normal in point + /// Select point on polygon + /// Polygon know neighbor of point + /// angle(atan2) of normal in polygon point + double calculate_angle(int32_t distance, PolygonPoint polygon_point, const Polygon &polygon); + std::vector calculate_angles(int32_t distance, const PolygonPoints& polygon_points, const Polygon &polygon); + +} // namespace Emboss + +/////////////////////// +// Move to ExPolygonsWithIds Utils +void translate(ExPolygonsWithIds &e, const Point &p); +BoundingBox get_extents(const ExPolygonsWithIds &e); +void center(ExPolygonsWithIds &e); +// delta .. safe offset before union (use as boolean close) +// NOTE: remove unprintable spaces between neighbor curves (made by linearization of curve) +ExPolygons union_with_delta(EmbossShape &shape, float delta, unsigned max_heal_iteration); +} // namespace Slic3r +#endif // slic3r_Emboss_hpp_ diff --git a/src/libslic3r/EmbossShape.hpp b/src/libslic3r/EmbossShape.hpp new file mode 100644 index 000000000..d7051782d --- /dev/null +++ b/src/libslic3r/EmbossShape.hpp @@ -0,0 +1,143 @@ +#ifndef slic3r_EmbossShape_hpp_ +#define slic3r_EmbossShape_hpp_ + +#include +#include +#include // unique_ptr +#include +#include +#include +#include +#include +#include "Point.hpp" // Transform3d +#include "ExPolygon.hpp" +#include "ExPolygonSerialize.hpp" +#include "nanosvg/nanosvg.h" // NSVGimage + +namespace Slic3r { + +struct EmbossProjection{ + // Emboss depth, Size in local Z direction + double depth = 1.; // [in loacal mm] + // NOTE: User should see and modify mainly world size not local + + // Flag that result volume use surface cutted from source objects + bool use_surface = false; + + bool operator==(const EmbossProjection &other) const { + return depth == other.depth && use_surface == other.use_surface; + } + + // undo / redo stack recovery + template void serialize(Archive &ar) { ar(depth, use_surface); } +}; + +// Extend expolygons with information whether it was successfull healed +struct HealedExPolygons{ + ExPolygons expolygons; + bool is_healed; + operator ExPolygons&() { return expolygons; } +}; + +// Help structure to identify expolygons grups +// e.g. emboss -> per glyph -> identify character +struct ExPolygonsWithId +{ + // Identificator for shape + // In text it separate letters and the name is unicode value of letter + // Is svg it is id of path + unsigned id; + + // shape defined by integer point contain only lines + // Curves are converted to sequence of lines + ExPolygons expoly; + + // flag whether expolygons are fully healed(without duplication) + bool is_healed = true; +}; +using ExPolygonsWithIds = std::vector; + +/// +/// Contain plane shape information to be able emboss it and edit it +/// +struct EmbossShape +{ + // shapes to to emboss separately over surface + ExPolygonsWithIds shapes_with_ids; + + // Only cache for final shape + // It is calculated from ExPolygonsWithIds + // Flag is_healed --> whether union of shapes is healed + // Healed mean without selfintersection and point duplication + HealedExPolygons final_shape; + + // scale of shape, multiplier to get 3d point in mm from integer shape + double scale = SCALING_FACTOR; + + // Define how to emboss shape + EmbossProjection projection; + + // !!! Volume stored in .3mf has transformed vertices. + // (baked transformation into vertices position) + // Only place for fill this is when load from .3mf + // This is correction for volume transformation + // Stored_Transform3d * fix_3mf_tr = Transform3d_before_store_to_3mf + std::optional fix_3mf_tr; + + struct SvgFile { + // File(.svg) path on local computer + // When empty can't reload from disk + std::string path; + + // File path into .3mf(.zip) + // When empty svg is not stored into .3mf file yet. + // and will create dialog to delete private data on save. + std::string path_in_3mf; + + // Loaded svg file data. + // !!! It is not serialized on undo/redo stack + std::shared_ptr image = nullptr; + + // Loaded string data from file + std::shared_ptr file_data = nullptr; + + template void save(Archive &ar) const { + // Note: image is only cache it is not neccessary to store + + // Store file data as plain string + // For Embossed text file_data are nullptr + ar(path, path_in_3mf, (file_data != nullptr) ? *file_data : std::string("")); + } + template void load(Archive &ar) { + // for restore shared pointer on file data + std::string file_data_str; + ar(path, path_in_3mf, file_data_str); + if (!file_data_str.empty()) + file_data = std::make_unique(file_data_str); + } + }; + // When embossing shape is made by svg file this is source data + std::optional svg_file; + + // undo / redo stack recovery + template void save(Archive &ar) const + { + // final_shape is not neccessary to store - it is only cache + ar(shapes_with_ids, final_shape, scale, projection, svg_file); + cereal::save(ar, fix_3mf_tr); + } + template void load(Archive &ar) + { + ar(shapes_with_ids, final_shape, scale, projection, svg_file); + cereal::load(ar, fix_3mf_tr); + } +}; +} // namespace Slic3r + +// Serialization through the Cereal library +namespace cereal { +template void serialize(Archive &ar, Slic3r::ExPolygonsWithId &o) { ar(o.id, o.expoly, o.is_healed); } +template void serialize(Archive &ar, Slic3r::HealedExPolygons &o) { ar(o.expolygons, o.is_healed); } +}; // namespace cereal + +#endif // slic3r_EmbossShape_hpp_ diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index a1b5691ff..f2b952627 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -1,523 +1,538 @@ -#include "BoundingBox.hpp" -#include "ExPolygon.hpp" -#include "Exception.hpp" -#include "Geometry/MedialAxis.hpp" -#include "Polygon.hpp" -#include "Line.hpp" -#include "ClipperUtils.hpp" -#include "SVG.hpp" -#include -#include -#include - -namespace Slic3r { - -void ExPolygon::scale(double factor) -{ - contour.scale(factor); - for (Polygon &hole : holes) - hole.scale(factor); -} - -void ExPolygon::scale(double factor_x, double factor_y) -{ - contour.scale(factor_x, factor_y); - for (Polygon &hole : holes) - hole.scale(factor_x, factor_y); -} - -void ExPolygon::translate(const Point &p) -{ - contour.translate(p); - for (Polygon &hole : holes) - hole.translate(p); -} - -void ExPolygon::rotate(double angle) -{ - contour.rotate(angle); - for (Polygon &hole : holes) - hole.rotate(angle); -} - -void ExPolygon::rotate(double angle, const Point ¢er) -{ - contour.rotate(angle, center); - for (Polygon &hole : holes) - hole.rotate(angle, center); -} - -double ExPolygon::area() const -{ - double a = this->contour.area(); - for (const Polygon &hole : holes) - a -= - hole.area(); // holes have negative area - return a; -} - -bool ExPolygon::is_valid() const -{ - if (!this->contour.is_valid() || !this->contour.is_counter_clockwise()) return false; - for (Polygons::const_iterator it = this->holes.begin(); it != this->holes.end(); ++it) { - if (!(*it).is_valid() || (*it).is_counter_clockwise()) return false; - } - return true; -} - -void ExPolygon::douglas_peucker(double tolerance) -{ - this->contour.douglas_peucker(tolerance); - for (Polygon &poly : this->holes) - poly.douglas_peucker(tolerance); -} - -bool ExPolygon::contains(const Line &line) const -{ - return this->contains(Polyline(line.a, line.b)); -} - -bool ExPolygon::contains(const Polyline &polyline) const -{ - BoundingBox bbox1 = get_extents(*this); - BoundingBox bbox2 = get_extents(polyline); - bbox2.inflated(1); - if (!bbox1.overlap(bbox2)) - return false; - - return diff_pl(polyline, *this).empty(); -} - -bool ExPolygon::contains(const Polylines &polylines) const -{ - #if 0 - BoundingBox bbox = get_extents(polylines); - bbox.merge(get_extents(*this)); - SVG svg(debug_out_path("ExPolygon_contains.svg"), bbox); - svg.draw(*this); - svg.draw_outline(*this); - svg.draw(polylines, "blue"); - #endif - Polylines pl_out = diff_pl(polylines, *this); - #if 0 - svg.draw(pl_out, "red"); - #endif - return pl_out.empty(); -} - -bool ExPolygon::contains(const Point &point, bool border_result /* = true */) const -{ - if (! Slic3r::contains(contour, point, border_result)) - // Outside the outer contour, not on the contour boundary. - return false; - for (const Polygon &hole : this->holes) - if (Slic3r::contains(hole, point, ! border_result)) - // Inside a hole, not on the hole boundary. - return false; - return true; -} - -bool ExPolygon::on_boundary(const Point &point, double eps) const -{ - if (this->contour.on_boundary(point, eps)) - return true; - for (const Polygon &hole : this->holes) - if (hole.on_boundary(point, eps)) - return true; - return false; -} - -// Projection of a point onto the polygon. -Point ExPolygon::point_projection(const Point &point) const -{ - if (this->holes.empty()) { - return this->contour.point_projection(point); - } else { - double dist_min2 = std::numeric_limits::max(); - Point closest_pt_min; - for (size_t i = 0; i < this->num_contours(); ++ i) { - Point closest_pt = this->contour_or_hole(i).point_projection(point); - double d2 = (closest_pt - point).cast().squaredNorm(); - if (d2 < dist_min2) { - dist_min2 = d2; - closest_pt_min = closest_pt; - } - } - return closest_pt_min; - } -} - -bool ExPolygon::overlaps(const ExPolygon &other) const -{ - if (this->empty() || other.empty()) - return false; - - #if 0 - BoundingBox bbox = get_extents(other); - bbox.merge(get_extents(*this)); - static int iRun = 0; - SVG svg(debug_out_path("ExPolygon_overlaps-%d.svg", iRun ++), bbox); - svg.draw(*this); - svg.draw_outline(*this); - svg.draw_outline(other, "blue"); - #endif - - Polylines pl_out = intersection_pl(to_polylines(other), *this); - - #if 0 - svg.draw(pl_out, "red"); - #endif - - // See unit test SCENARIO("Clipper diff with polyline", "[Clipper]") - // for in which case the intersection_pl produces any intersection. - return ! pl_out.empty() || - // If *this is completely inside other, then pl_out is empty, but the expolygons overlap. Test for that situation. - other.contains(this->contour.points.front()); -} - -bool overlaps(const ExPolygons& expolys1, const ExPolygons& expolys2) -{ - for (const ExPolygon& expoly1 : expolys1) { - for (const ExPolygon& expoly2 : expolys2) { - if (expoly1.overlaps(expoly2)) - return true; - } - } - return false; +#include "BoundingBox.hpp" +#include "ExPolygon.hpp" +#include "Exception.hpp" +#include "Geometry/MedialAxis.hpp" +#include "Polygon.hpp" +#include "Line.hpp" +#include "ClipperUtils.hpp" +#include "SVG.hpp" +#include +#include +#include + +namespace Slic3r { + +void ExPolygon::scale(double factor) +{ + contour.scale(factor); + for (Polygon &hole : holes) + hole.scale(factor); +} + +void ExPolygon::scale(double factor_x, double factor_y) +{ + contour.scale(factor_x, factor_y); + for (Polygon &hole : holes) + hole.scale(factor_x, factor_y); +} + +void ExPolygon::translate(const Point &p) +{ + contour.translate(p); + for (Polygon &hole : holes) + hole.translate(p); +} + +void ExPolygon::rotate(double angle) +{ + contour.rotate(angle); + for (Polygon &hole : holes) + hole.rotate(angle); +} + +void ExPolygon::rotate(double angle, const Point ¢er) +{ + contour.rotate(angle, center); + for (Polygon &hole : holes) + hole.rotate(angle, center); +} + +double ExPolygon::area() const +{ + double a = this->contour.area(); + for (const Polygon &hole : holes) + a -= - hole.area(); // holes have negative area + return a; +} + +bool ExPolygon::is_valid() const +{ + if (!this->contour.is_valid() || !this->contour.is_counter_clockwise()) return false; + for (Polygons::const_iterator it = this->holes.begin(); it != this->holes.end(); ++it) { + if (!(*it).is_valid() || (*it).is_counter_clockwise()) return false; + } + return true; +} + +void ExPolygon::douglas_peucker(double tolerance) +{ + this->contour.douglas_peucker(tolerance); + for (Polygon &poly : this->holes) + poly.douglas_peucker(tolerance); +} + +bool ExPolygon::contains(const Line &line) const +{ + return this->contains(Polyline(line.a, line.b)); +} + +bool ExPolygon::contains(const Polyline &polyline) const +{ + BoundingBox bbox1 = get_extents(*this); + BoundingBox bbox2 = get_extents(polyline); + bbox2.inflated(1); + if (!bbox1.overlap(bbox2)) + return false; + + return diff_pl(polyline, *this).empty(); +} + +bool ExPolygon::contains(const Polylines &polylines) const +{ + #if 0 + BoundingBox bbox = get_extents(polylines); + bbox.merge(get_extents(*this)); + SVG svg(debug_out_path("ExPolygon_contains.svg"), bbox); + svg.draw(*this); + svg.draw_outline(*this); + svg.draw(polylines, "blue"); + #endif + Polylines pl_out = diff_pl(polylines, *this); + #if 0 + svg.draw(pl_out, "red"); + #endif + return pl_out.empty(); +} + +bool ExPolygon::contains(const Point &point, bool border_result /* = true */) const +{ + if (! Slic3r::contains(contour, point, border_result)) + // Outside the outer contour, not on the contour boundary. + return false; + for (const Polygon &hole : this->holes) + if (Slic3r::contains(hole, point, ! border_result)) + // Inside a hole, not on the hole boundary. + return false; + return true; +} + +bool ExPolygon::on_boundary(const Point &point, double eps) const +{ + if (this->contour.on_boundary(point, eps)) + return true; + for (const Polygon &hole : this->holes) + if (hole.on_boundary(point, eps)) + return true; + return false; +} + +// Projection of a point onto the polygon. +Point ExPolygon::point_projection(const Point &point) const +{ + if (this->holes.empty()) { + return this->contour.point_projection(point); + } else { + double dist_min2 = std::numeric_limits::max(); + Point closest_pt_min; + for (size_t i = 0; i < this->num_contours(); ++ i) { + Point closest_pt = this->contour_or_hole(i).point_projection(point); + double d2 = (closest_pt - point).cast().squaredNorm(); + if (d2 < dist_min2) { + dist_min2 = d2; + closest_pt_min = closest_pt; + } + } + return closest_pt_min; + } +} + +bool ExPolygon::overlaps(const ExPolygon &other) const +{ + if (this->empty() || other.empty()) + return false; + + #if 0 + BoundingBox bbox = get_extents(other); + bbox.merge(get_extents(*this)); + static int iRun = 0; + SVG svg(debug_out_path("ExPolygon_overlaps-%d.svg", iRun ++), bbox); + svg.draw(*this); + svg.draw_outline(*this); + svg.draw_outline(other, "blue"); + #endif + + Polylines pl_out = intersection_pl(to_polylines(other), *this); + + #if 0 + svg.draw(pl_out, "red"); + #endif + + // See unit test SCENARIO("Clipper diff with polyline", "[Clipper]") + // for in which case the intersection_pl produces any intersection. + return ! pl_out.empty() || + // If *this is completely inside other, then pl_out is empty, but the expolygons overlap. Test for that situation. + other.contains(this->contour.points.front()); +} + +bool overlaps(const ExPolygons& expolys1, const ExPolygons& expolys2) +{ + for (const ExPolygon& expoly1 : expolys1) { + for (const ExPolygon& expoly2 : expolys2) { + if (expoly1.overlaps(expoly2)) + return true; + } + } + return false; } Point projection_onto(const ExPolygons& polygons, const Point& from) { - Point projected_pt; - double min_dist = std::numeric_limits::max(); - - for (const auto& poly : polygons) { - for (int i = 0; i < poly.num_contours(); i++) { - Point p = from.projection_onto(poly.contour_or_hole(i)); - double dist = (from - p).cast().squaredNorm(); - if (dist < min_dist) { - projected_pt = p; - min_dist = dist; - } - } - } - - return projected_pt; -} - -void ExPolygon::simplify_p(double tolerance, Polygons* polygons) const -{ - Polygons pp = this->simplify_p(tolerance); - polygons->insert(polygons->end(), pp.begin(), pp.end()); -} - -Polygons ExPolygon::simplify_p(double tolerance) const -{ - Polygons pp; - pp.reserve(this->holes.size() + 1); - // contour - { - Polygon p = this->contour; - p.points.push_back(p.points.front()); - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); - p.points.pop_back(); - pp.emplace_back(std::move(p)); - } - // holes - for (Polygon p : this->holes) { - p.points.push_back(p.points.front()); - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); - p.points.pop_back(); - pp.emplace_back(std::move(p)); - } - return simplify_polygons(pp); -} - -ExPolygons ExPolygon::simplify(double tolerance) const -{ - return union_ex(this->simplify_p(tolerance)); -} - -void ExPolygon::simplify(double tolerance, ExPolygons* expolygons) const -{ - append(*expolygons, this->simplify(tolerance)); -} - -void ExPolygon::medial_axis(double min_width, double max_width, ThickPolylines* polylines) const -{ - // init helper object - Slic3r::Geometry::MedialAxis ma(min_width, max_width, *this); - - // compute the Voronoi diagram and extract medial axis polylines - ThickPolylines pp; - ma.build(&pp); - - /* - SVG svg("medial_axis.svg"); - svg.draw(*this); - svg.draw(pp); - svg.Close(); - */ - - /* Find the maximum width returned; we're going to use this for validating and - filtering the output segments. */ - double max_w = 0; - for (ThickPolylines::const_iterator it = pp.begin(); it != pp.end(); ++it) - max_w = fmaxf(max_w, *std::max_element(it->width.begin(), it->width.end())); - - /* Loop through all returned polylines in order to extend their endpoints to the - expolygon boundaries */ - bool removed = false; - for (size_t i = 0; i < pp.size(); ++i) { - ThickPolyline& polyline = pp[i]; - - // extend initial and final segments of each polyline if they're actual endpoints - /* We assign new endpoints to temporary variables because in case of a single-line - polyline, after we extend the start point it will be caught by the intersection() - call, so we keep the inner point until we perform the second intersection() as well */ - Point new_front = polyline.points.front(); - Point new_back = polyline.points.back(); - if (polyline.endpoints.first && !this->on_boundary(new_front, SCALED_EPSILON)) { - Vec2d p1 = polyline.points.front().cast(); - Vec2d p2 = polyline.points[1].cast(); - // prevent the line from touching on the other side, otherwise intersection() might return that solution - if (polyline.points.size() == 2) - p2 = (p1 + p2) * 0.5; - // Extend the start of the segment. - p1 -= (p2 - p1).normalized() * max_width; - this->contour.intersection(Line(p1.cast(), p2.cast()), &new_front); - } - if (polyline.endpoints.second && !this->on_boundary(new_back, SCALED_EPSILON)) { - Vec2d p1 = (polyline.points.end() - 2)->cast(); - Vec2d p2 = polyline.points.back().cast(); - // prevent the line from touching on the other side, otherwise intersection() might return that solution - if (polyline.points.size() == 2) - p1 = (p1 + p2) * 0.5; - // Extend the start of the segment. - p2 += (p2 - p1).normalized() * max_width; - this->contour.intersection(Line(p1.cast(), p2.cast()), &new_back); - } - polyline.points.front() = new_front; - polyline.points.back() = new_back; - - /* remove too short polylines - (we can't do this check before endpoints extension and clipping because we don't - know how long will the endpoints be extended since it depends on polygon thickness - which is variable - extension will be <= max_width/2 on each side) */ - if ((polyline.endpoints.first || polyline.endpoints.second) - && polyline.length() < max_w*2) { - pp.erase(pp.begin() + i); - --i; - removed = true; - continue; - } - } - - /* If we removed any short polylines we now try to connect consecutive polylines - in order to allow loop detection. Note that this algorithm is greedier than - MedialAxis::process_edge_neighbors() as it will connect random pairs of - polylines even when more than two start from the same point. This has no - drawbacks since we optimize later using nearest-neighbor which would do the - same, but should we use a more sophisticated optimization algorithm we should - not connect polylines when more than two meet. */ - if (removed) { - for (size_t i = 0; i < pp.size(); ++i) { - ThickPolyline& polyline = pp[i]; - if (polyline.endpoints.first && polyline.endpoints.second) continue; // optimization - - // find another polyline starting here - for (size_t j = i+1; j < pp.size(); ++j) { - ThickPolyline& other = pp[j]; - if (polyline.last_point() == other.last_point()) { - other.reverse(); - } else if (polyline.first_point() == other.last_point()) { - polyline.reverse(); - other.reverse(); - } else if (polyline.first_point() == other.first_point()) { - polyline.reverse(); - } else if (polyline.last_point() != other.first_point()) { - continue; - } - - polyline.points.insert(polyline.points.end(), other.points.begin() + 1, other.points.end()); - polyline.width.insert(polyline.width.end(), other.width.begin(), other.width.end()); - polyline.endpoints.second = other.endpoints.second; - assert(polyline.width.size() == polyline.points.size()*2 - 2); - - pp.erase(pp.begin() + j); - j = i; // restart search from i+1 - } - } - } - - polylines->insert(polylines->end(), pp.begin(), pp.end()); -} - -void ExPolygon::medial_axis(double min_width, double max_width, Polylines* polylines) const -{ - ThickPolylines tp; - this->medial_axis(min_width, max_width, &tp); - polylines->reserve(polylines->size() + tp.size()); - for (auto &pl : tp) - polylines->emplace_back(pl.points); -} - -Lines ExPolygon::lines() const -{ - Lines lines = this->contour.lines(); - for (Polygons::const_iterator h = this->holes.begin(); h != this->holes.end(); ++h) { - Lines hole_lines = h->lines(); - lines.insert(lines.end(), hole_lines.begin(), hole_lines.end()); - } - return lines; -} - -// Do expolygons match? If they match, they must have the same topology, -// however their contours may be rotated. -bool expolygons_match(const ExPolygon &l, const ExPolygon &r) -{ - if (l.holes.size() != r.holes.size() || ! polygons_match(l.contour, r.contour)) - return false; - for (size_t hole_idx = 0; hole_idx < l.holes.size(); ++ hole_idx) - if (! polygons_match(l.holes[hole_idx], r.holes[hole_idx])) - return false; - return true; -} - -BoundingBox get_extents(const ExPolygon &expolygon) -{ - return get_extents(expolygon.contour); -} - -BoundingBox get_extents(const ExPolygons &expolygons) -{ - BoundingBox bbox; - if (! expolygons.empty()) { - for (size_t i = 0; i < expolygons.size(); ++ i) - if (! expolygons[i].contour.points.empty()) - bbox.merge(get_extents(expolygons[i])); - } - return bbox; -} - -BoundingBox get_extents_rotated(const ExPolygon &expolygon, double angle) -{ - return get_extents_rotated(expolygon.contour, angle); -} - -BoundingBox get_extents_rotated(const ExPolygons &expolygons, double angle) -{ - BoundingBox bbox; - if (! expolygons.empty()) { - bbox = get_extents_rotated(expolygons.front().contour, angle); - for (size_t i = 1; i < expolygons.size(); ++ i) - bbox.merge(get_extents_rotated(expolygons[i].contour, angle)); - } - return bbox; -} - -extern std::vector get_extents_vector(const ExPolygons &polygons) -{ - std::vector out; - out.reserve(polygons.size()); - for (ExPolygons::const_iterator it = polygons.begin(); it != polygons.end(); ++ it) - out.push_back(get_extents(*it)); - return out; -} - -bool has_duplicate_points(const ExPolygon &expoly) -{ -#if 1 - // Check globally. - size_t cnt = expoly.contour.points.size(); - for (const Polygon &hole : expoly.holes) - cnt += hole.points.size(); - std::vector allpts; - allpts.reserve(cnt); - allpts.insert(allpts.begin(), expoly.contour.points.begin(), expoly.contour.points.end()); - for (const Polygon &hole : expoly.holes) - allpts.insert(allpts.end(), hole.points.begin(), hole.points.end()); - return has_duplicate_points(std::move(allpts)); -#else - // Check per contour. - if (has_duplicate_points(expoly.contour)) - return true; - for (const Polygon &hole : expoly.holes) - if (has_duplicate_points(hole)) - return true; - return false; -#endif -} - -bool has_duplicate_points(const ExPolygons &expolys) -{ -#if 1 - // Check globally. - size_t cnt = 0; - for (const ExPolygon &expoly : expolys) { - cnt += expoly.contour.points.size(); - for (const Polygon &hole : expoly.holes) - cnt += hole.points.size(); - } - std::vector allpts; - allpts.reserve(cnt); - for (const ExPolygon &expoly : expolys) { - allpts.insert(allpts.begin(), expoly.contour.points.begin(), expoly.contour.points.end()); - for (const Polygon &hole : expoly.holes) - allpts.insert(allpts.end(), hole.points.begin(), hole.points.end()); - } - return has_duplicate_points(std::move(allpts)); -#else - // Check per contour. - for (const ExPolygon &expoly : expolys) - if (has_duplicate_points(expoly)) - return true; - return false; -#endif -} - -bool remove_sticks(ExPolygon &poly) -{ - return remove_sticks(poly.contour) || remove_sticks(poly.holes); -} - -bool remove_small_and_small_holes(ExPolygons &expolygons, double min_area) -{ - bool modified = false; - size_t free_idx = 0; - for (size_t expoly_idx = 0; expoly_idx < expolygons.size(); ++expoly_idx) { - if (std::abs(expolygons[expoly_idx].area()) >= min_area) { - // Expolygon is big enough, so also check all its holes - modified |= remove_small(expolygons[expoly_idx].holes, min_area); - if (free_idx < expoly_idx) { - std::swap(expolygons[expoly_idx].contour, expolygons[free_idx].contour); - std::swap(expolygons[expoly_idx].holes, expolygons[free_idx].holes); - } - ++free_idx; - } else - modified = true; - } - if (free_idx < expolygons.size()) - expolygons.erase(expolygons.begin() + free_idx, expolygons.end()); - return modified; -} - -void keep_largest_contour_only(ExPolygons &polygons) -{ - if (polygons.size() > 1) { - double max_area = 0.; - ExPolygon* max_area_polygon = nullptr; - for (ExPolygon& p : polygons) { - double a = p.contour.area(); - if (a > max_area) { - max_area = a; - max_area_polygon = &p; - } - } - assert(max_area_polygon != nullptr); - ExPolygon p(std::move(*max_area_polygon)); - polygons.clear(); - polygons.emplace_back(std::move(p)); - } -} - -} // namespace Slic3r + Point projected_pt; + double min_dist = std::numeric_limits::max(); + + for (const auto& poly : polygons) { + for (int i = 0; i < poly.num_contours(); i++) { + Point p = from.projection_onto(poly.contour_or_hole(i)); + double dist = (from - p).cast().squaredNorm(); + if (dist < min_dist) { + projected_pt = p; + min_dist = dist; + } + } + } + + return projected_pt; +} + +void ExPolygon::simplify_p(double tolerance, Polygons* polygons) const +{ + Polygons pp = this->simplify_p(tolerance); + polygons->insert(polygons->end(), pp.begin(), pp.end()); +} + +Polygons ExPolygon::simplify_p(double tolerance) const +{ + Polygons pp; + pp.reserve(this->holes.size() + 1); + // contour + { + Polygon p = this->contour; + p.points.push_back(p.points.front()); + p.points = MultiPoint::_douglas_peucker(p.points, tolerance); + p.points.pop_back(); + pp.emplace_back(std::move(p)); + } + // holes + for (Polygon p : this->holes) { + p.points.push_back(p.points.front()); + p.points = MultiPoint::_douglas_peucker(p.points, tolerance); + p.points.pop_back(); + pp.emplace_back(std::move(p)); + } + return simplify_polygons(pp); +} + +ExPolygons ExPolygon::simplify(double tolerance) const +{ + return union_ex(this->simplify_p(tolerance)); +} + +void ExPolygon::simplify(double tolerance, ExPolygons* expolygons) const +{ + append(*expolygons, this->simplify(tolerance)); +} + +void ExPolygon::medial_axis(double min_width, double max_width, ThickPolylines* polylines) const +{ + // init helper object + Slic3r::Geometry::MedialAxis ma(min_width, max_width, *this); + + // compute the Voronoi diagram and extract medial axis polylines + ThickPolylines pp; + ma.build(&pp); + + /* + SVG svg("medial_axis.svg"); + svg.draw(*this); + svg.draw(pp); + svg.Close(); + */ + + /* Find the maximum width returned; we're going to use this for validating and + filtering the output segments. */ + double max_w = 0; + for (ThickPolylines::const_iterator it = pp.begin(); it != pp.end(); ++it) + max_w = fmaxf(max_w, *std::max_element(it->width.begin(), it->width.end())); + + /* Loop through all returned polylines in order to extend their endpoints to the + expolygon boundaries */ + bool removed = false; + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + + // extend initial and final segments of each polyline if they're actual endpoints + /* We assign new endpoints to temporary variables because in case of a single-line + polyline, after we extend the start point it will be caught by the intersection() + call, so we keep the inner point until we perform the second intersection() as well */ + Point new_front = polyline.points.front(); + Point new_back = polyline.points.back(); + if (polyline.endpoints.first && !this->on_boundary(new_front, SCALED_EPSILON)) { + Vec2d p1 = polyline.points.front().cast(); + Vec2d p2 = polyline.points[1].cast(); + // prevent the line from touching on the other side, otherwise intersection() might return that solution + if (polyline.points.size() == 2) + p2 = (p1 + p2) * 0.5; + // Extend the start of the segment. + p1 -= (p2 - p1).normalized() * max_width; + this->contour.intersection(Line(p1.cast(), p2.cast()), &new_front); + } + if (polyline.endpoints.second && !this->on_boundary(new_back, SCALED_EPSILON)) { + Vec2d p1 = (polyline.points.end() - 2)->cast(); + Vec2d p2 = polyline.points.back().cast(); + // prevent the line from touching on the other side, otherwise intersection() might return that solution + if (polyline.points.size() == 2) + p1 = (p1 + p2) * 0.5; + // Extend the start of the segment. + p2 += (p2 - p1).normalized() * max_width; + this->contour.intersection(Line(p1.cast(), p2.cast()), &new_back); + } + polyline.points.front() = new_front; + polyline.points.back() = new_back; + + /* remove too short polylines + (we can't do this check before endpoints extension and clipping because we don't + know how long will the endpoints be extended since it depends on polygon thickness + which is variable - extension will be <= max_width/2 on each side) */ + if ((polyline.endpoints.first || polyline.endpoints.second) + && polyline.length() < max_w*2) { + pp.erase(pp.begin() + i); + --i; + removed = true; + continue; + } + } + + /* If we removed any short polylines we now try to connect consecutive polylines + in order to allow loop detection. Note that this algorithm is greedier than + MedialAxis::process_edge_neighbors() as it will connect random pairs of + polylines even when more than two start from the same point. This has no + drawbacks since we optimize later using nearest-neighbor which would do the + same, but should we use a more sophisticated optimization algorithm we should + not connect polylines when more than two meet. */ + if (removed) { + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + if (polyline.endpoints.first && polyline.endpoints.second) continue; // optimization + + // find another polyline starting here + for (size_t j = i+1; j < pp.size(); ++j) { + ThickPolyline& other = pp[j]; + if (polyline.last_point() == other.last_point()) { + other.reverse(); + } else if (polyline.first_point() == other.last_point()) { + polyline.reverse(); + other.reverse(); + } else if (polyline.first_point() == other.first_point()) { + polyline.reverse(); + } else if (polyline.last_point() != other.first_point()) { + continue; + } + + polyline.points.insert(polyline.points.end(), other.points.begin() + 1, other.points.end()); + polyline.width.insert(polyline.width.end(), other.width.begin(), other.width.end()); + polyline.endpoints.second = other.endpoints.second; + assert(polyline.width.size() == polyline.points.size()*2 - 2); + + pp.erase(pp.begin() + j); + j = i; // restart search from i+1 + } + } + } + + polylines->insert(polylines->end(), pp.begin(), pp.end()); +} + +void ExPolygon::medial_axis(double min_width, double max_width, Polylines* polylines) const +{ + ThickPolylines tp; + this->medial_axis(min_width, max_width, &tp); + polylines->reserve(polylines->size() + tp.size()); + for (auto &pl : tp) + polylines->emplace_back(pl.points); +} + +Lines ExPolygon::lines() const +{ + Lines lines = this->contour.lines(); + for (Polygons::const_iterator h = this->holes.begin(); h != this->holes.end(); ++h) { + Lines hole_lines = h->lines(); + lines.insert(lines.end(), hole_lines.begin(), hole_lines.end()); + } + return lines; +} + +// Do expolygons match? If they match, they must have the same topology, +// however their contours may be rotated. +bool expolygons_match(const ExPolygon &l, const ExPolygon &r) +{ + if (l.holes.size() != r.holes.size() || ! polygons_match(l.contour, r.contour)) + return false; + for (size_t hole_idx = 0; hole_idx < l.holes.size(); ++ hole_idx) + if (! polygons_match(l.holes[hole_idx], r.holes[hole_idx])) + return false; + return true; +} + +BoundingBox get_extents(const ExPolygon &expolygon) +{ + return get_extents(expolygon.contour); +} + +BoundingBox get_extents(const ExPolygons &expolygons) +{ + BoundingBox bbox; + if (! expolygons.empty()) { + for (size_t i = 0; i < expolygons.size(); ++ i) + if (! expolygons[i].contour.points.empty()) + bbox.merge(get_extents(expolygons[i])); + } + return bbox; +} + +BoundingBox get_extents_rotated(const ExPolygon &expolygon, double angle) +{ + return get_extents_rotated(expolygon.contour, angle); +} + +BoundingBox get_extents_rotated(const ExPolygons &expolygons, double angle) +{ + BoundingBox bbox; + if (! expolygons.empty()) { + bbox = get_extents_rotated(expolygons.front().contour, angle); + for (size_t i = 1; i < expolygons.size(); ++ i) + bbox.merge(get_extents_rotated(expolygons[i].contour, angle)); + } + return bbox; +} + +extern std::vector get_extents_vector(const ExPolygons &polygons) +{ + std::vector out; + out.reserve(polygons.size()); + for (ExPolygons::const_iterator it = polygons.begin(); it != polygons.end(); ++ it) + out.push_back(get_extents(*it)); + return out; +} + +bool has_duplicate_points(const ExPolygon &expoly) +{ +#if 1 + // Check globally. + size_t cnt = expoly.contour.points.size(); + for (const Polygon &hole : expoly.holes) + cnt += hole.points.size(); + std::vector allpts; + allpts.reserve(cnt); + allpts.insert(allpts.begin(), expoly.contour.points.begin(), expoly.contour.points.end()); + for (const Polygon &hole : expoly.holes) + allpts.insert(allpts.end(), hole.points.begin(), hole.points.end()); + return has_duplicate_points(std::move(allpts)); +#else + // Check per contour. + if (has_duplicate_points(expoly.contour)) + return true; + for (const Polygon &hole : expoly.holes) + if (has_duplicate_points(hole)) + return true; + return false; +#endif +} + +bool has_duplicate_points(const ExPolygons &expolys) +{ +#if 1 + // Check globally. + size_t cnt = 0; + for (const ExPolygon &expoly : expolys) { + cnt += expoly.contour.points.size(); + for (const Polygon &hole : expoly.holes) + cnt += hole.points.size(); + } + std::vector allpts; + allpts.reserve(cnt); + for (const ExPolygon &expoly : expolys) { + allpts.insert(allpts.begin(), expoly.contour.points.begin(), expoly.contour.points.end()); + for (const Polygon &hole : expoly.holes) + allpts.insert(allpts.end(), hole.points.begin(), hole.points.end()); + } + return has_duplicate_points(std::move(allpts)); +#else + // Check per contour. + for (const ExPolygon &expoly : expolys) + if (has_duplicate_points(expoly)) + return true; + return false; +#endif +} + +bool remove_same_neighbor(ExPolygons &expolygons) +{ + if (expolygons.empty()) return false; + bool remove_from_holes = false; + bool remove_from_contour = false; + for (ExPolygon &expoly : expolygons) { + remove_from_contour |= remove_same_neighbor(expoly.contour); + remove_from_holes |= remove_same_neighbor(expoly.holes); + } + // Removing of expolygons without contour + if (remove_from_contour) + expolygons.erase(std::remove_if(expolygons.begin(), expolygons.end(), [](const ExPolygon &p) { return p.contour.points.size() <= 2; }), expolygons.end()); + return remove_from_holes || remove_from_contour; +} + +bool remove_sticks(ExPolygon &poly) +{ + return remove_sticks(poly.contour) || remove_sticks(poly.holes); +} + +bool remove_small_and_small_holes(ExPolygons &expolygons, double min_area) +{ + bool modified = false; + size_t free_idx = 0; + for (size_t expoly_idx = 0; expoly_idx < expolygons.size(); ++expoly_idx) { + if (std::abs(expolygons[expoly_idx].area()) >= min_area) { + // Expolygon is big enough, so also check all its holes + modified |= remove_small(expolygons[expoly_idx].holes, min_area); + if (free_idx < expoly_idx) { + std::swap(expolygons[expoly_idx].contour, expolygons[free_idx].contour); + std::swap(expolygons[expoly_idx].holes, expolygons[free_idx].holes); + } + ++free_idx; + } else + modified = true; + } + if (free_idx < expolygons.size()) + expolygons.erase(expolygons.begin() + free_idx, expolygons.end()); + return modified; +} + +void keep_largest_contour_only(ExPolygons &polygons) +{ + if (polygons.size() > 1) { + double max_area = 0.; + ExPolygon* max_area_polygon = nullptr; + for (ExPolygon& p : polygons) { + double a = p.contour.area(); + if (a > max_area) { + max_area = a; + max_area_polygon = &p; + } + } + assert(max_area_polygon != nullptr); + ExPolygon p(std::move(*max_area_polygon)); + polygons.clear(); + polygons.emplace_back(std::move(p)); + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index d213d991f..4515d8a0d 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -72,7 +72,7 @@ public: void simplify(double tolerance, ExPolygons* expolygons) const; void medial_axis(double min_width, double max_width, ThickPolylines* polylines) const; void medial_axis(double min_width, double max_width, Polylines* polylines) const; - Polylines medial_axis(double min_width, double max_width) const + Polylines medial_axis(double min_width, double max_width) const { Polylines out; this->medial_axis(min_width, max_width, &out); return out; } Lines lines() const; @@ -88,9 +88,9 @@ inline bool operator!=(const ExPolygon &lhs, const ExPolygon &rhs) { return lhs. inline size_t count_points(const ExPolygons &expolys) { size_t n_points = 0; - for (const auto &expoly : expolys) { + for (const auto &expoly : expolys) { n_points += expoly.contour.points.size(); - for (const auto &hole : expoly.holes) + for (const auto &hole : expoly.holes) n_points += hole.points.size(); } return n_points; @@ -99,8 +99,8 @@ inline size_t count_points(const ExPolygons &expolys) inline size_t count_points(const ExPolygon &expoly) { size_t n_points = expoly.contour.points.size(); - for (const auto &hole : expoly.holes) - n_points += hole.points.size(); + for (const auto &hole : expoly.holes) + n_points += hole.points.size(); return n_points; } @@ -114,7 +114,7 @@ inline size_t number_polygons(const ExPolygons &expolys) return n_polygons; } -inline Lines to_lines(const ExPolygon &src) +inline Lines to_lines(const ExPolygon &src) { Lines lines; lines.reserve(count_points(src)); @@ -127,7 +127,7 @@ inline Lines to_lines(const ExPolygon &src) return lines; } -inline Lines to_lines(const ExPolygons &src) +inline Lines to_lines(const ExPolygons &src) { Lines lines; lines.reserve(count_points(src)); @@ -155,7 +155,7 @@ inline Linesf to_linesf(const ExPolygons &src, uint32_t count_lines = 0) assert(pts.size() >= 3); if (pts.size() < 2) return; bool is_first = true; - for (const Point &p : pts) { + for (const Point &p : pts) { Vec2d pd = p.cast(); if (is_first) is_first = false; else lines.emplace_back(prev_pd, pd); @@ -165,7 +165,7 @@ inline Linesf to_linesf(const ExPolygons &src, uint32_t count_lines = 0) }; for (const ExPolygon& expoly: src) { to_lines(expoly.contour.points); - for (const Polygon &hole : expoly.holes) + for (const Polygon &hole : expoly.holes) to_lines(hole.points); } assert(lines.size() == count_lines); @@ -372,15 +372,20 @@ inline Points to_points(const ExPolygon &expoly) return out; } -inline void polygons_append(Polygons &dst, const ExPolygon &src) -{ +inline void translate(ExPolygons &expolys, const Point &p) +{ + for (ExPolygon &expoly : expolys) expoly.translate(p); +} + +inline void polygons_append(Polygons &dst, const ExPolygon &src) +{ dst.reserve(dst.size() + src.holes.size() + 1); dst.push_back(src.contour); dst.insert(dst.end(), src.holes.begin(), src.holes.end()); } -inline void polygons_append(Polygons &dst, const ExPolygons &src) -{ +inline void polygons_append(Polygons &dst, const ExPolygons &src) +{ dst.reserve(dst.size() + number_polygons(src)); for (ExPolygons::const_iterator it = src.begin(); it != src.end(); ++ it) { dst.push_back(it->contour); @@ -389,36 +394,36 @@ inline void polygons_append(Polygons &dst, const ExPolygons &src) } inline void polygons_append(Polygons &dst, ExPolygon &&src) -{ +{ dst.reserve(dst.size() + src.holes.size() + 1); - dst.push_back(std::move(src.contour)); - dst.insert(dst.end(), + dst.push_back(std::move(src.contour)); + dst.insert(dst.end(), std::make_move_iterator(src.holes.begin()), std::make_move_iterator(src.holes.end())); } inline void polygons_append(Polygons &dst, ExPolygons &&src) -{ +{ dst.reserve(dst.size() + number_polygons(src)); for (ExPolygon& expoly: src) { dst.push_back(std::move(expoly.contour)); - dst.insert(dst.end(), + dst.insert(dst.end(), std::make_move_iterator(expoly.holes.begin()), std::make_move_iterator(expoly.holes.end())); } } -inline void expolygons_append(ExPolygons &dst, const ExPolygons &src) -{ +inline void expolygons_append(ExPolygons &dst, const ExPolygons &src) +{ dst.insert(dst.end(), src.begin(), src.end()); } inline void expolygons_append(ExPolygons &dst, ExPolygons &&src) -{ +{ if (dst.empty()) { dst = std::move(src); } else { - dst.insert(dst.end(), + dst.insert(dst.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end())); } @@ -464,6 +469,8 @@ std::vector get_extents_vector(const ExPolygons &polygons); // Test for duplicate points. The points are copied, sorted and checked for duplicates globally. bool has_duplicate_points(const ExPolygon &expoly); bool has_duplicate_points(const ExPolygons &expolys); +// Return True when erase some otherwise False. +bool remove_same_neighbor(ExPolygons &expolys); bool remove_sticks(ExPolygon &poly); void keep_largest_contour_only(ExPolygons &polygons); @@ -517,8 +524,8 @@ namespace boost { namespace polygon { return expolygon; } }; - - + + template <> struct geometry_concept { typedef polygon_with_holes_concept type; }; @@ -545,7 +552,7 @@ namespace boost { namespace polygon { return t; } }; - + //first we register CPolygonSet as a polygon set template <> struct geometry_concept { typedef polygon_set_concept type; }; diff --git a/src/libslic3r/ExPolygonSerialize.hpp b/src/libslic3r/ExPolygonSerialize.hpp new file mode 100644 index 000000000..3cd760bee --- /dev/null +++ b/src/libslic3r/ExPolygonSerialize.hpp @@ -0,0 +1,28 @@ +#ifndef slic3r_ExPolygonSerialize_hpp_ +#define slic3r_ExPolygonSerialize_hpp_ + +#include "ExPolygon.hpp" +#include "Point.hpp" // Cereal serialization of Point +#include +#include + +/// +/// External Cereal serialization of ExPolygons +/// + +// Serialization through the Cereal library +#include +namespace cereal { + +template +void serialize(Archive &archive, Slic3r::Polygon &polygon) { + archive(polygon.points); +} + +template +void serialize(Archive &archive, Slic3r::ExPolygon &expoly) { + archive(expoly.contour, expoly.holes); +} + +} // namespace Slic3r +#endif // slic3r_ExPolygonSerialize_hpp_ diff --git a/src/libslic3r/ExPolygonsIndex.cpp b/src/libslic3r/ExPolygonsIndex.cpp new file mode 100644 index 000000000..f083effa5 --- /dev/null +++ b/src/libslic3r/ExPolygonsIndex.cpp @@ -0,0 +1,82 @@ +#include "ExPolygonsIndex.hpp" +using namespace Slic3r; + +// IMPROVE: use one dimensional vector for polygons offset with searching by std::lower_bound +ExPolygonsIndices::ExPolygonsIndices(const ExPolygons &shapes) +{ + // prepare offsets + m_offsets.reserve(shapes.size()); + uint32_t offset = 0; + for (const ExPolygon &shape : shapes) { + assert(!shape.contour.points.empty()); + std::vector shape_offsets; + shape_offsets.reserve(shape.holes.size() + 1); + shape_offsets.push_back(offset); + offset += shape.contour.points.size(); + for (const Polygon &hole: shape.holes) { + shape_offsets.push_back(offset); + offset += hole.points.size(); + } + m_offsets.push_back(std::move(shape_offsets)); + } + m_count = offset; +} + +uint32_t ExPolygonsIndices::cvt(const ExPolygonsIndex &id) const +{ + assert(id.expolygons_index < m_offsets.size()); + const std::vector &shape_offset = m_offsets[id.expolygons_index]; + assert(id.polygon_index < shape_offset.size()); + uint32_t res = shape_offset[id.polygon_index] + id.point_index; + assert(res < m_count); + return res; +} + +ExPolygonsIndex ExPolygonsIndices::cvt(uint32_t index) const +{ + assert(index < m_count); + ExPolygonsIndex result{0, 0, 0}; + // find expolygon index + auto fn = [](const std::vector &offsets, uint32_t index) { return offsets[0] < index; }; + auto it = std::lower_bound(m_offsets.begin() + 1, m_offsets.end(), index, fn); + result.expolygons_index = it - m_offsets.begin(); + if (it == m_offsets.end() || it->at(0) != index) --result.expolygons_index; + + // find polygon index + const std::vector &shape_offset = m_offsets[result.expolygons_index]; + auto it2 = std::lower_bound(shape_offset.begin() + 1, shape_offset.end(), index); + result.polygon_index = it2 - shape_offset.begin(); + if (it2 == shape_offset.end() || *it2 != index) --result.polygon_index; + + // calculate point index + uint32_t polygon_offset = shape_offset[result.polygon_index]; + assert(index >= polygon_offset); + result.point_index = index - polygon_offset; + return result; +} + +bool ExPolygonsIndices::is_last_point(const ExPolygonsIndex &id) const { + assert(id.expolygons_index < m_offsets.size()); + const std::vector &shape_offset = m_offsets[id.expolygons_index]; + assert(id.polygon_index < shape_offset.size()); + uint32_t index = shape_offset[id.polygon_index] + id.point_index; + assert(index < m_count); + // next index + uint32_t next_point_index = index + 1; + uint32_t next_poly_index = id.polygon_index + 1; + uint32_t next_expoly_index = id.expolygons_index + 1; + // is last expoly? + if (next_expoly_index == m_offsets.size()) { + // is last expoly last poly? + if (next_poly_index == shape_offset.size()) + return next_point_index == m_count; + } else { + // (not last expoly) is expoly last poly? + if (next_poly_index == shape_offset.size()) + return next_point_index == m_offsets[next_expoly_index][0]; + } + // Not last polygon in expolygon + return next_point_index == shape_offset[next_poly_index]; +} + +uint32_t ExPolygonsIndices::get_count() const { return m_count; } diff --git a/src/libslic3r/ExPolygonsIndex.hpp b/src/libslic3r/ExPolygonsIndex.hpp new file mode 100644 index 000000000..66e87b16f --- /dev/null +++ b/src/libslic3r/ExPolygonsIndex.hpp @@ -0,0 +1,74 @@ +#ifndef slic3r_ExPolygonsIndex_hpp_ +#define slic3r_ExPolygonsIndex_hpp_ + +#include "ExPolygon.hpp" +namespace Slic3r { + +/// +/// Index into ExPolygons +/// Identify expolygon, its contour (or hole) and point +/// +struct ExPolygonsIndex +{ + // index of ExPolygons + uint32_t expolygons_index; + + // index of Polygon + // 0 .. contour + // N .. hole[N-1] + uint32_t polygon_index; + + // index of point in polygon + uint32_t point_index; + + bool is_contour() const { return polygon_index == 0; } + bool is_hole() const { return polygon_index != 0; } + uint32_t hole_index() const { return polygon_index - 1; } +}; + +/// +/// Keep conversion from ExPolygonsIndex to Index and vice versa +/// ExPolygonsIndex .. contour(or hole) point from ExPolygons +/// Index .. continous number +/// +/// index is used to address lines and points as result from function +/// Slic3r::to_lines, Slic3r::to_points +/// +class ExPolygonsIndices +{ + std::vector> m_offsets; + // for check range of index + uint32_t m_count; // count of points +public: + ExPolygonsIndices(const ExPolygons &shapes); + + /// + /// Convert to one index number + /// + /// Compose of adress into expolygons + /// Index + uint32_t cvt(const ExPolygonsIndex &id) const; + + /// + /// Separate to multi index + /// + /// adress into expolygons + /// + ExPolygonsIndex cvt(uint32_t index) const; + + /// + /// Check whether id is last point in polygon + /// + /// Identify point in expolygon + /// True when id is last point in polygon otherwise false + bool is_last_point(const ExPolygonsIndex &id) const; + + /// + /// Count of points in expolygons + /// + /// Count of points in expolygons + uint32_t get_count() const; +}; + +} // namespace Slic3r +#endif // slic3r_ExPolygonsIndex_hpp_ diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 26ef25bed..bc04424d9 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -18,6 +18,8 @@ #include #include +#include +#include #include #include #include @@ -46,6 +48,11 @@ namespace pt = boost::property_tree; #include #include "miniz_extension.hpp" #include "nlohmann/json.hpp" + +#include "EmbossShape.hpp" +#include "ExPolygonSerialize.hpp" +#include "NSVGUtils.hpp" + #include // Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, @@ -332,6 +339,40 @@ static constexpr const char* MESH_STAT_FACETS_REMOVED = "facets_removed"; static constexpr const char* MESH_STAT_FACETS_RESERVED = "facets_reversed"; static constexpr const char* MESH_STAT_BACKWARDS_EDGES = "backwards_edges"; +// Store / load of TextConfiguration +static constexpr const char *TEXT_DATA_ATTR = "text"; +// TextConfiguration::EmbossStyle +static constexpr const char *STYLE_NAME_ATTR = "style_name"; +static constexpr const char *FONT_DESCRIPTOR_ATTR = "font_descriptor"; +static constexpr const char *FONT_DESCRIPTOR_TYPE_ATTR = "font_descriptor_type"; + +// TextConfiguration::FontProperty +static constexpr const char *CHAR_GAP_ATTR = "char_gap"; +static constexpr const char *LINE_GAP_ATTR = "line_gap"; +static constexpr const char *LINE_HEIGHT_ATTR = "line_height"; +static constexpr const char *BOLDNESS_ATTR = "boldness"; +static constexpr const char *SKEW_ATTR = "skew"; +static constexpr const char *PER_GLYPH_ATTR = "per_glyph"; +static constexpr const char *HORIZONTAL_ALIGN_ATTR = "horizontal"; +static constexpr const char *VERTICAL_ALIGN_ATTR = "vertical"; +static constexpr const char *COLLECTION_NUMBER_ATTR = "collection"; + +static constexpr const char *FONT_FAMILY_ATTR = "family"; +static constexpr const char *FONT_FACE_NAME_ATTR = "face_name"; +static constexpr const char *FONT_STYLE_ATTR = "style"; +static constexpr const char *FONT_WEIGHT_ATTR = "weight"; + +// Store / load of EmbossShape +static constexpr const char *SHAPE_TAG = "slic3rpe:shape"; +static constexpr const char *SHAPE_SCALE_ATTR = "scale"; +static constexpr const char *UNHEALED_ATTR = "unhealed"; +static constexpr const char *SVG_FILE_PATH_ATTR = "filepath"; +static constexpr const char *SVG_FILE_PATH_IN_3MF_ATTR = "filepath3mf"; + +// EmbossProjection +static constexpr const char *DEPTH_ATTR = "depth"; +static constexpr const char *USE_SURFACE_ATTR = "use_surface"; +// static constexpr const char *FIX_TRANSFORMATION_ATTR = "transform"; const unsigned int BBS_VALID_OBJECT_TYPES_COUNT = 2; const char* BBS_VALID_OBJECT_TYPES[] = @@ -719,7 +760,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) RepairedMeshErrors mesh_stats; ModelVolumeType part_type; TextInfo text_info; - + std::optional shape_configuration; VolumeMetadata(unsigned int first_triangle_id, unsigned int last_triangle_id, ModelVolumeType type = ModelVolumeType::MODEL_PART) : first_triangle_id(first_triangle_id) , last_triangle_id(last_triangle_id) @@ -772,6 +813,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) typedef std::map IdToLayerConfigRangesMap; /*typedef std::map> IdToSlaSupportPointsMap; typedef std::map> IdToSlaDrainHolesMap;*/ + using PathToEmbossShapeFileMap = std::map>; struct ObjectImporter { @@ -966,6 +1008,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) IdToLayerConfigRangesMap m_layer_config_ranges; /*IdToSlaSupportPointsMap m_sla_support_points; IdToSlaDrainHolesMap m_sla_drain_holes;*/ + PathToEmbossShapeFileMap m_path_to_emboss_shape_files; std::string m_curr_metadata_name; std::string m_curr_characters; std::string m_name; @@ -1019,6 +1062,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // add backup & restore logic bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr, BBLProject* project = nullptr, int plate_id = 0); + bool _is_svg_shape_file(const std::string &filename) const; bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function, bool restore = false); bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler); bool _extract_xml_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler); @@ -1039,6 +1083,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) void _extract_auxiliary_file_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); void _extract_file_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat); // handlers to parse the .model file void _handle_start_model_xml_element(const char* name, const char** attributes); @@ -1094,6 +1139,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _handle_start_metadata(const char** attributes, unsigned int num_attributes); bool _handle_end_metadata(); + bool _handle_start_shape_configuration(const char **attributes, unsigned int num_attributes); + bool _create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter); void _apply_transform(ModelInstance& instance, const Transform3d& transform); @@ -1401,6 +1448,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) add_error("Archive does not contain a valid model config"); return false; } + } else if (_is_svg_shape_file(name)) { + _extract_embossed_svg_shape_file(name, archive, stat); } else if (boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) { m_parsing_slice_info = true; @@ -2165,6 +2214,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + bool _BBS_3MF_Importer::_is_svg_shape_file(const std::string &name) const { + return boost::starts_with(name, MODEL_FOLDER) && boost::ends_with(name, ".svg"); + } + bool _BBS_3MF_Importer::_extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function extract, bool restore) { mz_uint num_entries = mz_zip_reader_get_num_files(&archive); @@ -2876,6 +2929,30 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } }*/ + void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat) + { + assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end()); + auto file = std::make_unique(stat.m_uncomp_size, '\0'); + mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void *) file->data(), stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading svg shape for emboss"); + return; + } + + // store for case svg is loaded before volume + m_path_to_emboss_shape_files[filename] = std::move(file); + + // find embossed volume, for case svg is loaded after volume + for (const ModelObject *object : m_model->objects) + for (ModelVolume *volume : object->volumes) { + std::optional &es = volume->emboss_shape; + if (!es.has_value()) continue; + std::optional &svg = es->svg_file; + if (!svg.has_value()) continue; + if (filename.compare(svg->path_in_3mf) == 0) svg->file_data = m_path_to_emboss_shape_files[filename]; + } + } + void _BBS_3MF_Importer::_extract_custom_gcode_per_print_z_from_archive(::mz_zip_archive &archive, const mz_zip_archive_file_stat &stat) { //BBS: add plate tree related logic @@ -3086,7 +3163,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_start_assemble_item(attributes, num_attributes); else if (::strcmp(TEXT_INFO_TAG, name) == 0) res = _handle_start_text_info_item(attributes, num_attributes); - + else if (::strcmp(SHAPE_TAG, name) == 0) + res = _handle_start_shape_configuration(attributes, num_attributes); if (!res) _stop_xml_parser(); } @@ -3639,6 +3717,40 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + // Definition of read/write method for EmbossShape + static void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume &volume, mz_zip_archive &archive); + static std::optional read_emboss_shape(const char **attributes, unsigned int num_attributes); + + bool _BBS_3MF_Importer::_handle_start_shape_configuration(const char **attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Can not assign volume mesh to a valid object"); + return false; + } + auto &volumes = object->second.volumes; + if (volumes.empty()) { + add_error("Can not assign mesh to a valid volume"); + return false; + } + ObjectMetadata::VolumeMetadata &volume = volumes.back(); + volume.shape_configuration = read_emboss_shape(attributes, num_attributes); + if (!volume.shape_configuration.has_value()) return false; + + // Fill svg file content into shape_configuration + std::optional &svg = volume.shape_configuration->svg_file; + if (!svg.has_value()) return true; // do not contain svg file + + const std::string &path = svg->path_in_3mf; + if (path.empty()) return true; // do not contain svg file + + auto it = m_path_to_emboss_shape_files.find(path); + if (it == m_path_to_emboss_shape_files.end()) return true; // svg file is not loaded yet + + svg->file_data = it->second; + return true; + } + bool _BBS_3MF_Importer::_create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter) { static const unsigned int MAX_RECURSIONS = 10; @@ -4203,7 +4315,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } ObjectMetadata::VolumeMetadata &volume = object->second.volumes[m_curr_config.volume_id]; - TextInfo text_info; text_info.m_text = xml_unescape(bbs_get_attribute_value_string(attributes, num_attributes, TEXT_ATTR)); text_info.m_font_name = bbs_get_attribute_value_string(attributes, num_attributes, FONT_NAME_ATTR); @@ -4508,7 +4619,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } volume->set_type(volume_data->part_type); - + if (auto &es = volume_data->shape_configuration; es.has_value()) + volume->emboss_shape = std::move(es); if (!volume_data->text_info.m_text.empty()) volume->set_text_info(volume_data->text_info); @@ -5277,7 +5389,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool save_model_to_file(StoreParams& store_params); // add backup logic bool save_object_mesh(const std::string& temp_path, ModelObject const & object, int obj_id); - + static void add_transformation(std::stringstream &stream, const Transform3d &tr); private: //BBS: add plate data related logic bool _save_model_to_file(const std::string& filename, @@ -6766,6 +6878,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return flush(output_buffer, true); } + void _BBS_3MF_Exporter::add_transformation(std::stringstream &stream, const Transform3d &tr) + { + for (unsigned c = 0; c < 4; ++c) { + for (unsigned r = 0; r < 3; ++r) { + stream << tr(r, c); + if (r != 2 || c != 3) + stream << " "; + } + } + } + bool _BBS_3MF_Exporter::_add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) const { // This happens for empty projects @@ -6786,13 +6909,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (!item.path.empty()) stream << "\" " << PPATH_ATTR << "=\"" << xml_escape(item.path); stream << "\" " << TRANSFORM_ATTR << "=\""; - for (unsigned c = 0; c < 4; ++c) { - for (unsigned r = 0; r < 3; ++r) { - stream << item.transform(r, c); - if (r != 2 || c != 3) - stream << " "; - } - } + add_transformation(stream, item.transform); stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable << "\"/>\n"; } @@ -7186,6 +7303,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " <" << METADATA_TAG << " "<< KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; } + if (const std::optional &es = volume->emboss_shape; es.has_value()) + to_xml(stream, *es, *volume, archive); + const TextInfo &text_info = volume->get_text_info(); if (!text_info.m_text.empty()) _add_text_info_to_archive(stream, text_info); @@ -8283,4 +8403,148 @@ SaveObjectGaurd::~SaveObjectGaurd() _BBS_Backup_Manager::get().pop_object_gaurd(); } +namespace { + +// Conversion with bidirectional map +// F .. first, S .. second +template F bimap_cvt(const boost::bimap &bmap, S s, const F &def_value) +{ + const auto &map = bmap.right; + auto found_item = map.find(s); + + // only for back and forward compatibility + assert(found_item != map.end()); + if (found_item == map.end()) return def_value; + + return found_item->second; +} + +template S bimap_cvt(const boost::bimap &bmap, F f, const S &def_value) +{ + const auto &map = bmap.left; + auto found_item = map.find(f); + + // only for back and forward compatibility + assert(found_item != map.end()); + if (found_item == map.end()) return def_value; + + return found_item->second; +} + +} // namespace + + + +namespace { +Transform3d create_fix(const std::optional &prev, const ModelVolume &volume) +{ + // IMPROVE: check if volume was modified (translated, rotated OR scaled) + // when no change do not calculate transformation only store original fix matrix + + // Create transformation used after load actual stored volume + // Orca: do not bake volume transformation into meshes + // const Transform3d &actual_trmat = volume.get_matrix(); + const Transform3d &actual_trmat = Transform3d::Identity(); + + const auto &vertices = volume.mesh().its.vertices; + Vec3d min = actual_trmat * vertices.front().cast(); + Vec3d max = min; + for (const Vec3f &v : vertices) { + Vec3d vd = actual_trmat * v.cast(); + for (size_t i = 0; i < 3; ++i) { + if (min[i] > vd[i]) min[i] = vd[i]; + if (max[i] < vd[i]) max[i] = vd[i]; + } + } + Vec3d center = (max + min) / 2; + Transform3d post_trmat = Transform3d::Identity(); + post_trmat.translate(center); + + Transform3d fix_trmat = actual_trmat.inverse() * post_trmat; + if (!prev.has_value()) return fix_trmat; + + // check whether fix somehow differ previous + if (fix_trmat.isApprox(Transform3d::Identity(), 1e-5)) return *prev; + + return *prev * fix_trmat; +} + +bool to_xml(std::stringstream &stream, const EmbossShape::SvgFile &svg, const ModelVolume &volume, mz_zip_archive &archive) +{ + if (svg.path_in_3mf.empty()) + return true; // EmbossedText OR unwanted store .svg file into .3mf (protection of copyRight) + + if (!svg.path.empty()) + stream << SVG_FILE_PATH_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(svg.path) << "\" "; + stream << SVG_FILE_PATH_IN_3MF_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(svg.path_in_3mf) << "\" "; + + std::shared_ptr file_data = svg.file_data; + assert(file_data != nullptr); + if (file_data == nullptr && !svg.path.empty()) file_data = read_from_disk(svg.path); + if (file_data == nullptr) { + BOOST_LOG_TRIVIAL(warning) << "Can't write svg file no filedata"; + return false; + } + const std::string &file_data_str = *file_data; + + return mz_zip_writer_add_mem(&archive, svg.path_in_3mf.c_str(), (const void *) file_data_str.c_str(), file_data_str.size(), MZ_DEFAULT_COMPRESSION); +} + +} // namespace + +void to_xml(std::stringstream &stream, const EmbossShape &es, const ModelVolume &volume, mz_zip_archive &archive) +{ + stream << " <" << SHAPE_TAG << " "; + if (es.svg_file.has_value()) + if (!to_xml(stream, *es.svg_file, volume, archive)) BOOST_LOG_TRIVIAL(warning) << "Can't write svg file defiden embossed shape into 3mf"; + + stream << SHAPE_SCALE_ATTR << "=\"" << es.scale << "\" "; + + if (!es.final_shape.is_healed) stream << UNHEALED_ATTR << "=\"" << 1 << "\" "; + + // projection + const EmbossProjection &p = es.projection; + stream << DEPTH_ATTR << "=\"" << p.depth << "\" "; + if (p.use_surface) stream << USE_SURFACE_ATTR << "=\"" << 1 << "\" "; + + // FIX of baked transformation + Transform3d fix = create_fix(es.fix_3mf_tr, volume); + stream << TRANSFORM_ATTR << "=\""; + _BBS_3MF_Exporter::add_transformation(stream, fix); + stream << "\" "; + + stream << "/>\n"; // end SHAPE_TAG +} + +std::optional read_emboss_shape(const char **attributes, unsigned int num_attributes) +{ + double scale = bbs_get_attribute_value_float(attributes, num_attributes, SHAPE_SCALE_ATTR); + int unhealed = bbs_get_attribute_value_int(attributes, num_attributes, UNHEALED_ATTR); + bool is_healed = unhealed != 1; + + EmbossProjection projection; + projection.depth = bbs_get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); + if (is_approx(projection.depth, 0.)) projection.depth = 10.; + + int use_surface = bbs_get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); + if (use_surface == 1) projection.use_surface = true; + + std::optional fix_tr_mat; + std::string fix_tr_mat_str = bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR); + if (!fix_tr_mat_str.empty()) { fix_tr_mat = bbs_get_transform_from_3mf_specs_string(fix_tr_mat_str); } + + std::string file_path = bbs_get_attribute_value_string(attributes, num_attributes, SVG_FILE_PATH_ATTR); + std::string file_path_3mf = bbs_get_attribute_value_string(attributes, num_attributes, SVG_FILE_PATH_IN_3MF_ATTR); + + // MayBe: store also shapes to not store svg + // But be carefull curve will be lost -> scale will not change sampling + // shapes could be loaded from SVG + ExPolygonsWithIds shapes; + // final shape could be calculated from shapes + HealedExPolygons final_shape; + final_shape.is_healed = is_healed; + + EmbossShape::SvgFile svg{file_path, file_path_3mf}; + return EmbossShape{std::move(shapes), std::move(final_shape), scale, std::move(projection), std::move(fix_tr_mat), std::move(svg)}; +} } // namespace Slic3r diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index d18b7ea53..213550436 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -308,6 +308,16 @@ template T angle_to_0_2PI(T angle) return angle; } +template +void to_range_pi_pi(T &angle) +{ + if (angle > T(PI) || angle <= -T(PI)) { + int count = static_cast(std::round(angle / (2 * PI))); + angle -= static_cast(count * 2 * PI); + assert(angle <= T(PI) && angle > -T(PI)); + } +} + void simplify_polygons(const Polygons &polygons, double tolerance, Polygons* retval); double linint(double value, double oldmin, double oldmax, double newmin, double newmax); diff --git a/src/libslic3r/IntersectionPoints.cpp b/src/libslic3r/IntersectionPoints.cpp new file mode 100644 index 000000000..cf5b8706d --- /dev/null +++ b/src/libslic3r/IntersectionPoints.cpp @@ -0,0 +1,45 @@ +#include "IntersectionPoints.hpp" +#include + +//NOTE: using CGAL SweepLines is slower !!! (example in git history) + +namespace { +using namespace Slic3r; +IntersectionsLines compute_intersections(const Lines &lines) +{ + if (lines.size() < 3) + return {}; + + auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + IntersectionsLines result; + for (uint32_t li = 0; li < lines.size()-1; ++li) { + const Line &l = lines[li]; + auto intersections = AABBTreeLines::get_intersections_with_line(lines, tree, l); + for (const auto &[p, node_index] : intersections) { + if (node_index - 1 <= li) + continue; + if (const Line &l_ = lines[node_index]; + l_.a == l.a || + l_.a == l.b || + l_.b == l.a || + l_.b == l.b ) + // it is duplicit point not intersection + continue; + + // NOTE: fix AABBTree to compute intersection with double preccission!! + Vec2d intersection_point = p.cast(); + + result.push_back(IntersectionLines{li, static_cast(node_index), intersection_point}); + } + } + return result; +} +} // namespace + +namespace Slic3r { +IntersectionsLines get_intersections(const Lines &lines) { return compute_intersections(lines); } +IntersectionsLines get_intersections(const Polygon &polygon) { return compute_intersections(to_lines(polygon)); } +IntersectionsLines get_intersections(const Polygons &polygons) { return compute_intersections(to_lines(polygons)); } +IntersectionsLines get_intersections(const ExPolygon &expolygon) { return compute_intersections(to_lines(expolygon)); } +IntersectionsLines get_intersections(const ExPolygons &expolygons) { return compute_intersections(to_lines(expolygons)); } +} diff --git a/src/libslic3r/IntersectionPoints.hpp b/src/libslic3r/IntersectionPoints.hpp new file mode 100644 index 000000000..49694c8a8 --- /dev/null +++ b/src/libslic3r/IntersectionPoints.hpp @@ -0,0 +1,23 @@ +#ifndef slic3r_IntersectionPoints_hpp_ +#define slic3r_IntersectionPoints_hpp_ + +#include "ExPolygon.hpp" + +namespace Slic3r { + +struct IntersectionLines { + uint32_t line_index1; + uint32_t line_index2; + Vec2d intersection; +}; +using IntersectionsLines = std::vector; + +// collect all intersecting points +IntersectionsLines get_intersections(const Lines &lines); +IntersectionsLines get_intersections(const Polygon &polygon); +IntersectionsLines get_intersections(const Polygons &polygons); +IntersectionsLines get_intersections(const ExPolygon &expolygon); +IntersectionsLines get_intersections(const ExPolygons &expolygons); + +} // namespace Slic3r +#endif // slic3r_IntersectionPoints_hpp_ \ No newline at end of file diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 1b718183f..ed986c03a 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1932,6 +1932,21 @@ void ModelObject::clone_for_cut(ModelObject **obj) (*obj)->input_file.clear(); } +bool ModelVolume::is_the_only_one_part() const +{ + if (m_type != ModelVolumeType::MODEL_PART) + return false; + if (object == nullptr) return false; + for (const ModelVolume *v : object->volumes) { + if (v == nullptr) continue; + // is this volume? + if (v->id() == this->id()) continue; + // exist another model part in object? + if (v->type() == ModelVolumeType::MODEL_PART) return false; + } + return true; +} + Transform3d ModelObject::calculate_cut_plane_inverse_matrix(const std::array& plane_points) { Vec3d mid_point = {0.0, 0.0, 0.0}; @@ -2425,6 +2440,7 @@ void ModelObject::split(ModelObjectPtrs* new_objects) if (volume->type() != ModelVolumeType::MODEL_PART) continue; + // splited volume should not be text object if (!is_multi_volume_object) { //BBS: not multi volume object, then split mesh. std::vector volume_meshes = volume->mesh().split(); @@ -3183,7 +3199,7 @@ size_t ModelVolume::split(unsigned int max_extruders) std::vector meshes = this->mesh().split(); if (meshes.size() <= 1) return 1; - + // splited volume should not be text object size_t idx = 0; size_t ivolume = std::find(this->object->volumes.begin(), this->object->volumes.end(), this) - this->object->volumes.begin(); const std::string name = this->name; diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index d9c5f07a4..0a95d5c6e 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -14,7 +14,7 @@ #include "TriangleMesh.hpp" #include "CustomGCode.hpp" #include "enum_bitmask.hpp" - +#include "EmbossShape.hpp" //BBS: add bbs 3mf #include "Format/bbs_3mf.hpp" //BBS: add step @@ -32,7 +32,7 @@ #include #include #include - +#include namespace cereal { class BinaryInputArchive; class BinaryOutputArchive; @@ -901,6 +901,7 @@ public: void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } void reset_mesh() { m_mesh = std::make_shared(); } + const std::shared_ptr &get_mesh_shared_ptr() const { return m_mesh; } // Configuration parameters specific to an object model geometry or a modifier volume, // overriding the global Slic3r settings and the ModelObject settings. ModelConfigObject config; @@ -921,6 +922,10 @@ public: // List of exterior faces FacetsAnnotation exterior_facets; + + // Is set only when volume is Embossed Shape + // Contain 2d information about embossed shape to be editabled + std::optional emboss_shape; // A parent object owning this modifier volume. ModelObject* get_object() const { return this->object; } ModelVolumeType type() const { return m_type; } @@ -931,6 +936,8 @@ public: bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; } bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; } + bool is_svg() const { return emboss_shape.has_value(); } + bool is_the_only_one_part() const; // behave like an object t_model_material_id material_id() const { return m_material_id; } void set_material_id(t_model_material_id material_id); void reset_extra_facets(); @@ -1119,7 +1126,7 @@ private: name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), - m_text_info(other.m_text_info) + m_text_info(other.m_text_info), emboss_shape(other.emboss_shape) { assert(this->id().valid()); assert(this->config.id().valid()); @@ -1139,7 +1146,8 @@ private: } // Providing a new mesh, therefore this volume will get a new unique ID assigned. ModelVolume(ModelObject *object, const ModelVolume &other, const TriangleMesh &&mesh) : - name(other.name), source(other.source), m_mesh(new TriangleMesh(std::move(mesh))), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation) + name(other.name), source(other.source), m_mesh(new TriangleMesh(std::move(mesh))), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), + emboss_shape(other.emboss_shape) { assert(this->id().valid()); assert(this->config.id().valid()); @@ -1186,7 +1194,6 @@ private: auto tr = m_transformation; ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull, m_text_info, cut_info); mesh_changed |= !(tr == m_transformation); - if (mesh_changed) m_transformation.get_matrix(true, true, true, true); // force dirty auto t = supported_facets.timestamp(); cereal::load_by_value(ar, supported_facets); mesh_changed |= t != supported_facets.timestamp(); @@ -1197,6 +1204,7 @@ private: cereal::load_by_value(ar, mmu_segmentation_facets); mesh_changed |= t != mmu_segmentation_facets.timestamp(); cereal::load_by_value(ar, config); + cereal::load(ar, emboss_shape); assert(m_mesh); if (has_convex_hull) { cereal::load_optional(ar, m_convex_hull); @@ -1215,6 +1223,7 @@ private: cereal::save_by_value(ar, seam_facets); cereal::save_by_value(ar, mmu_segmentation_facets); cereal::save_by_value(ar, config); + cereal::save(ar, emboss_shape); if (has_convex_hull) cereal::save_optional(ar, m_convex_hull); } @@ -1306,7 +1315,7 @@ public: // BBS void rotate(Matrix3d rotation_matrix) { // note: must remove scaling from transformation, otherwise auto-orientation with scaled objects will have problem - auto R = get_matrix(true,false,true).matrix().block<3, 3>(0, 0); + auto R = m_transformation.get_rotation_matrix().matrix().block<3, 3>(0, 0); auto R_new = rotation_matrix * R; auto euler_angles = Geometry::extract_euler_angles(R_new); set_rotation(euler_angles); diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp new file mode 100644 index 000000000..14b144dfb --- /dev/null +++ b/src/libslic3r/NSVGUtils.cpp @@ -0,0 +1,543 @@ +#include "NSVGUtils.hpp" +#include +#include // to_chars + +#include +#include +#include "ClipperUtils.hpp" +#include "Emboss.hpp" // heal for shape + +namespace { +using namespace Slic3r; // Polygon +// see function nsvg__lineTo(NSVGparser* p, float x, float y) +bool is_line(const float *p, float precision = 1e-4f); +// convert curve in path to lines +struct LinesPath{ + Polygons polygons; + Polylines polylines; }; +LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams ¶m); +HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m); +HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m); +} // namespace + +namespace Slic3r { + +ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLineParams ¶m) +{ + ExPolygonsWithIds result; + size_t shape_id = 0; + for (NSVGshape *shape_ptr = image.shapes; shape_ptr != NULL; shape_ptr = shape_ptr->next, ++shape_id) { + const NSVGshape &shape = *shape_ptr; + if (!(shape.flags & NSVG_FLAGS_VISIBLE)) + continue; + + bool is_fill_used = shape.fill.type != NSVG_PAINT_NONE; + bool is_stroke_used = + shape.stroke.type != NSVG_PAINT_NONE && + shape.strokeWidth > 1e-5f; + + if (!is_fill_used && !is_stroke_used) + continue; + + const LinesPath lines_path = linearize_path(shape.paths, param); + + if (is_fill_used) { + unsigned unique_id = static_cast(2 * shape_id); + HealedExPolygons expoly = fill_to_expolygons(lines_path, shape, param); + result.push_back({unique_id, expoly.expolygons, expoly.is_healed}); + } + if (is_stroke_used) { + unsigned unique_id = static_cast(2 * shape_id + 1); + HealedExPolygons expoly = stroke_to_expolygons(lines_path, shape, param); + result.push_back({unique_id, expoly.expolygons, expoly.is_healed}); + } + } + + // SVG is used as centered + // Do not disturb user by settings of pivot position + center(result); + return result; +} + +Polygons to_polygons(const NSVGimage &image, const NSVGLineParams ¶m) +{ + Polygons result; + for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) + continue; + if (shape->fill.type == NSVG_PAINT_NONE) + continue; + const LinesPath lines_path = linearize_path(shape->paths, param); + polygons_append(result, lines_path.polygons); + // close polyline to create polygon + polygons_append(result, to_polygons(lines_path.polylines)); + } + return result; +} + +void bounds(const NSVGimage &image, Vec2f& min, Vec2f &max) +{ + for (const NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) + for (const NSVGpath *path = shape->paths; path != NULL; path = path->next) { + if (min.x() > path->bounds[0]) + min.x() = path->bounds[0]; + if (min.y() > path->bounds[1]) + min.y() = path->bounds[1]; + if (max.x() < path->bounds[2]) + max.x() = path->bounds[2]; + if (max.y() < path->bounds[3]) + max.y() = path->bounds[3]; + } +} + +NSVGimage_ptr nsvgParseFromFile(const std::string &filename, const char *units, float dpi) +{ + NSVGimage *image = ::nsvgParseFromFile(filename.c_str(), units, dpi); + return {image, &nsvgDelete}; +} + +std::unique_ptr read_from_disk(const std::string &path) +{ + boost::nowide::ifstream fs{path}; + if (!fs.is_open()) + return nullptr; + std::stringstream ss; + ss << fs.rdbuf(); + return std::make_unique(ss.str()); +} + +NSVGimage_ptr nsvgParse(const std::string& file_data, const char *units, float dpi){ + // NOTE: nsvg parser consume data from input(char *) + size_t size = file_data.size(); + // file data could be big, so it is allocated on heap + std::unique_ptr data_copy(new char[size+1]); + memcpy(data_copy.get(), file_data.c_str(), size); + data_copy[size] = '\0'; // data for nsvg must be null terminated + NSVGimage *image = ::nsvgParse(data_copy.get(), units, dpi); + return {image, &nsvgDelete}; +} + +NSVGimage *init_image(EmbossShape::SvgFile &svg_file){ + // is already initialized? + if (svg_file.image.get() != nullptr) + return svg_file.image.get(); + + if (svg_file.file_data == nullptr) { + // chech if path is known + if (svg_file.path.empty()) + return nullptr; + svg_file.file_data = read_from_disk(svg_file.path); + if (svg_file.file_data == nullptr) + return nullptr; + } + + // init svg image + svg_file.image = nsvgParse(*svg_file.file_data); + if (svg_file.image.get() == NULL) + return nullptr; + + return svg_file.image.get(); +} + +size_t get_shapes_count(const NSVGimage &image) +{ + size_t count = 0; + for (NSVGshape * s = image.shapes; s != NULL; s = s->next) + ++count; + return count; +} + +//void save(const NSVGimage &image, std::ostream &data) +//{ +// data << ""; +// +// // tl .. top left +// Vec2f tl(std::numeric_limits::max(), std::numeric_limits::max()); +// // br .. bottom right +// Vec2f br(std::numeric_limits::min(), std::numeric_limits::min()); +// bounds(image, tl, br); +// +// tl.x() = std::floor(tl.x()); +// tl.y() = std::floor(tl.y()); +// +// br.x() = std::ceil(br.x()); +// br.y() = std::ceil(br.y()); +// Vec2f s = br - tl; +// Point size = s.cast(); +// +// data << "\n"; +// data << "\n"; +// +// std::array buffer; +// auto write_point = [&tl, &buffer](std::string &d, const float *p) { +// float x = p[0] - tl.x(); +// float y = p[1] - tl.y(); +// auto to_string = [&buffer](float f) -> std::string { +// auto [ptr, ec] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), f); +// if (ec != std::errc{}) +// return "0"; +// return std::string(buffer.data(), ptr); +// }; +// d += to_string(x) + "," + to_string(y) + " "; +// }; +// +// for (const NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) { +// enum struct Type { move, line, curve, close }; // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d +// Type type = Type::move; +// std::string d = "M "; // move on start point +// for (const NSVGpath *path = shape->paths; path != NULL; path = path->next) { +// if (path->npts <= 1) +// continue; +// +// if (type == Type::close) { +// type = Type::move; +// // NOTE: After close must be a space +// d += " M "; // move on start point +// } +// write_point(d, path->pts); +// size_t path_size = static_cast(path->npts - 1); +// +// if (path->closed) { +// // Do not use last point in path it is duplicit +// if (path->npts <= 4) +// continue; +// path_size = static_cast(path->npts - 4); +// } +// +// for (size_t i = 0; i < path_size; i += 3) { +// const float *p = &path->pts[i * 2]; +// if (!::is_line(p)) { +// if (type != Type::curve) { +// type = Type::curve; +// d += "C "; // start sequence of triplets defining curves +// } +// write_point(d, &p[2]); +// write_point(d, &p[4]); +// } else { +// +// if (type != Type::line) { +// type = Type::line; +// d += "L "; // start sequence of line points +// } +// } +// write_point(d, &p[6]); +// } +// if (path->closed) { +// type = Type::close; +// d += "Z"; // start sequence of line points +// } +// } +// if (type != Type::close) { +// //type = Type::close; +// d += "Z"; // closed path +// } +// data << "\n"; +// } +// data << "\n"; +//} +// +//bool save(const NSVGimage &image, const std::string &svg_file_path) +//{ +// std::ofstream file{svg_file_path}; +// if (!file.is_open()) +// return false; +// save(image, file); +// return true; +//} +} // namespace Slic3r + +namespace { +using namespace Slic3r; // Polygon + Vec2f + +Point::coord_type to_coor(float val, double scale) { return static_cast(std::round(val * scale)); } + +bool need_flattening(float tessTol, const Vec2f &p1, const Vec2f &p2, const Vec2f &p3, const Vec2f &p4) { + // f .. first + // s .. second + auto det = [](const Vec2f &f, const Vec2f &s) { + return std::fabs(f.x() * s.y() - f.y() * s.x()); + }; + + Vec2f pd = (p4 - p1); + Vec2f pd2 = (p2 - p4); + float d2 = det(pd2, pd); + Vec2f pd3 = (p3 - p4); + float d3 = det(pd3, pd); + float d23 = d2 + d3; + + return (d23 * d23) >= tessTol * pd.squaredNorm(); +} + +// see function nsvg__lineTo(NSVGparser* p, float x, float y) +bool is_line(const float *p, float precision){ + //Vec2f p1(p[0], p[1]); + //Vec2f p2(p[2], p[3]); + //Vec2f p3(p[4], p[5]); + //Vec2f p4(p[6], p[7]); + float dx_3 = (p[6] - p[0]) / 3.f; + float dy_3 = (p[7] - p[1]) / 3.f; + + return + is_approx(p[2], p[0] + dx_3, precision) && + is_approx(p[4], p[6] - dx_3, precision) && + is_approx(p[3], p[1] + dy_3, precision) && + is_approx(p[5], p[7] - dy_3, precision); +} + +/// +/// Convert cubic curve to lines +/// Inspired by nanosvgrast.h function nsvgRasterize -> nsvg__flattenShape -> nsvg__flattenCubicBez +/// https://github.com/memononen/nanosvg/blob/f0a3e1034dd22e2e87e5db22401e44998383124e/src/nanosvgrast.h#L335 +/// +/// Result points +/// Tesselation tolerance +/// Curve point +/// Curve point +/// Curve point +/// Curve point +/// Actual depth of recursion +void flatten_cubic_bez(Points &points, float tessTol, const Vec2f& p1, const Vec2f& p2, const Vec2f& p3, const Vec2f& p4, int level) +{ + if (!need_flattening(tessTol, p1, p2, p3, p4)) { + Point::coord_type x = static_cast(std::round(p4.x())); + Point::coord_type y = static_cast(std::round(p4.y())); + points.emplace_back(x, y); + return; + } + + --level; + if (level == 0) + return; + + Vec2f p12 = (p1 + p2) * 0.5f; + Vec2f p23 = (p2 + p3) * 0.5f; + Vec2f p34 = (p3 + p4) * 0.5f; + Vec2f p123 = (p12 + p23) * 0.5f; + Vec2f p234 = (p23 + p34) * 0.5f; + Vec2f p1234 = (p123 + p234) * 0.5f; + flatten_cubic_bez(points, tessTol, p1, p12, p123, p1234, level); + flatten_cubic_bez(points, tessTol, p1234, p234, p34, p4, level); +} + +LinesPath linearize_path(NSVGpath *first_path, const NSVGLineParams ¶m) +{ + LinesPath result; + Polygons &polygons = result.polygons; + Polylines &polylines = result.polylines; + + // multiple use of allocated memmory for points between paths + Points points; + for (NSVGpath *path = first_path; path != NULL; path = path->next) { + // Flatten path + Point::coord_type x = to_coor(path->pts[0], param.scale); + Point::coord_type y = to_coor(path->pts[1], param.scale); + points.emplace_back(x, y); + size_t path_size = (path->npts > 1) ? static_cast(path->npts - 1) : 0; + for (size_t i = 0; i < path_size; i += 3) { + const float *p = &path->pts[i * 2]; + if (is_line(p)) { + // point p4 + Point::coord_type xx = to_coor(p[6], param.scale); + Point::coord_type yy = to_coor(p[7], param.scale); + points.emplace_back(xx, yy); + continue; + } + Vec2f p1(p[0], p[1]); + Vec2f p2(p[2], p[3]); + Vec2f p3(p[4], p[5]); + Vec2f p4(p[6], p[7]); + flatten_cubic_bez(points, param.tesselation_tolerance, + p1 * param.scale, p2 * param.scale, p3 * param.scale, p4 * param.scale, + param.max_level); + } + assert(!points.empty()); + if (points.empty()) + continue; + + if (param.is_y_negative) + for (Point &p : points) + p.y() = -p.y(); + + if (path->closed) { + polygons.emplace_back(points); + } else { + polylines.emplace_back(points); + } + // prepare for new path - recycle alocated memory + points.clear(); + } + remove_same_neighbor(polygons); + remove_same_neighbor(polylines); + return result; +} + +HealedExPolygons fill_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) +{ + Polygons fill = lines_path.polygons; // copy + + // close polyline to create polygon + polygons_append(fill, to_polygons(lines_path.polylines)); + if (fill.empty()) + return {}; + + // if (shape->fillRule == NSVGfillRule::NSVG_FILLRULE_NONZERO) + bool is_non_zero = true; + if (shape.fillRule == NSVGfillRule::NSVG_FILLRULE_EVENODD) + is_non_zero = false; + + return Emboss::heal_polygons(fill, is_non_zero, param.max_heal_iteration); +} + +struct DashesParam{ + // first dash length + float dash_length = 1.f; // scaled + + // is current dash .. true + // is current space .. false + bool is_line = true; + + // current index to array + unsigned char dash_index = 0; + static constexpr size_t max_dash_array_size = 8; // limitation of nanosvg strokeDashArray + std::array dash_array; // scaled + unsigned char dash_count = 0; // count of values in array + + explicit DashesParam(const NSVGshape &shape, double scale) : + dash_count(shape.strokeDashCount) + { + assert(dash_count > 0); + assert(dash_count <= max_dash_array_size); // limitation of nanosvg strokeDashArray + for (size_t i = 0; i < dash_count; ++i) + dash_array[i] = static_cast(shape.strokeDashArray[i] * scale); + + // Figure out dash offset. + float all_dash_length = 0; + for (unsigned char j = 0; j < dash_count; ++j) + all_dash_length += dash_array[j]; + + if (dash_count%2 == 1) // (shape.strokeDashCount & 1) + all_dash_length *= 2.0f; + + // Find location inside pattern + float dash_offset = fmodf(static_cast(shape.strokeDashOffset * scale), all_dash_length); + if (dash_offset < 0.0f) + dash_offset += all_dash_length; + + while (dash_offset > dash_array[dash_index]) { + dash_offset -= dash_array[dash_index]; + dash_index = (dash_index + 1) % shape.strokeDashCount; + is_line = !is_line; + } + + dash_length = dash_array[dash_index] - dash_offset; + } +}; + +Polylines to_dashes(const Polyline &polyline, const DashesParam& param) +{ + Polylines dashes; + Polyline dash; // cache for one dash in dashed line + Point prev_point; + + bool is_line = param.is_line; + unsigned char dash_index = param.dash_index; + float dash_length = param.dash_length; // current rest of dash distance + for (const Point &point : polyline.points) { + if (&point == &polyline.points.front()) { + // is first point + prev_point = point; // copy + continue; + } + + Point diff = point - prev_point; + float line_segment_length = diff.cast().norm(); + while (dash_length < line_segment_length) { + // Calculate intermediate point + float d = dash_length / line_segment_length; + Point move_point = diff * d; + Point intermediate = prev_point + move_point; + + // add Dash in stroke + if (is_line) { + if (dash.empty()) { + dashes.emplace_back(Points{prev_point, intermediate}); + } else { + dash.append(prev_point); + dash.append(intermediate); + dashes.push_back(dash); + dash.clear(); + } + } + + diff -= move_point; + line_segment_length -= dash_length; + prev_point = intermediate; + + // Advance dash pattern + is_line = !is_line; + dash_index = (dash_index + 1) % param.dash_count; + dash_length = param.dash_array[dash_index]; + } + + if (is_line) + dash.append(prev_point); + dash_length -= line_segment_length; + prev_point = point; // copy + } + + // add last dash + if (is_line){ + assert(!dash.empty()); + dash.append(prev_point); // prev_point == polyline.points.back() + dashes.push_back(dash); + } + return dashes; +} + +HealedExPolygons stroke_to_expolygons(const LinesPath &lines_path, const NSVGshape &shape, const NSVGLineParams ¶m) +{ + // convert stroke to polygon + ClipperLib::JoinType join_type = ClipperLib::JoinType::jtSquare; + switch (static_cast(shape.strokeLineJoin)) { + case NSVGlineJoin::NSVG_JOIN_BEVEL: join_type = ClipperLib::JoinType::jtSquare; break; + case NSVGlineJoin::NSVG_JOIN_MITER: join_type = ClipperLib::JoinType::jtMiter; break; + case NSVGlineJoin::NSVG_JOIN_ROUND: join_type = ClipperLib::JoinType::jtRound; break; + } + + double mitter = shape.miterLimit * param.scale; + if (join_type == ClipperLib::JoinType::jtRound) { + // mitter is used as ArcTolerance + // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/Properties/ArcTolerance.htm + mitter = std::pow(param.tesselation_tolerance, 1/3.); + } + float stroke_width = static_cast(shape.strokeWidth * param.scale); + + ClipperLib::EndType end_type = ClipperLib::EndType::etOpenButt; + switch (static_cast(shape.strokeLineCap)) { + case NSVGlineCap::NSVG_CAP_BUTT: end_type = ClipperLib::EndType::etOpenButt; break; + case NSVGlineCap::NSVG_CAP_ROUND: end_type = ClipperLib::EndType::etOpenRound; break; + case NSVGlineCap::NSVG_CAP_SQUARE: end_type = ClipperLib::EndType::etOpenSquare; break; + } + + Polygons result; + if (shape.strokeDashCount > 0) { + DashesParam params(shape, param.scale); + Polylines dashes; + for (const Polyline &polyline : lines_path.polylines) + polylines_append(dashes, to_dashes(polyline, params)); + for (const Polygon &polygon : lines_path.polygons) + polylines_append(dashes, to_dashes(to_polyline(polygon), params)); + result = offset(dashes, stroke_width / 2, join_type, mitter, end_type); + } else { + result = contour_to_polygons(lines_path.polygons, stroke_width, join_type, mitter); + polygons_append(result, offset(lines_path.polylines, stroke_width / 2, join_type, mitter, end_type)); + } + + bool is_non_zero = true; + return Emboss::heal_polygons(result, is_non_zero, param.max_heal_iteration); +} + +} // namespace diff --git a/src/libslic3r/NSVGUtils.hpp b/src/libslic3r/NSVGUtils.hpp new file mode 100644 index 000000000..8d84815e0 --- /dev/null +++ b/src/libslic3r/NSVGUtils.hpp @@ -0,0 +1,82 @@ +#ifndef slic3r_NSVGUtils_hpp_ +#define slic3r_NSVGUtils_hpp_ + +#include +#include +#include +#include "Polygon.hpp" +#include "ExPolygon.hpp" +#include "EmbossShape.hpp" // ExPolygonsWithIds +#include "nanosvg/nanosvg.h" // load SVG file + +// Helper function to work with nano svg +namespace Slic3r { + +/// +/// Paramreters for conversion curve from SVG to lines in Polygon +/// +struct NSVGLineParams +{ + // Smaller will divide curve to more lines + // NOTE: Value is in image scale + double tesselation_tolerance = 10.f; + + // Maximal depth of recursion for conversion curve to lines + int max_level = 10; + + // Multiplicator of point coors + // NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer --> Slic3r::Point + double scale = 1. / SCALING_FACTOR; + + // Flag wether y is negative, when true than y coor is multiplied by -1 + bool is_y_negative = true; + + // Is used only with rounded Stroke + double arc_tolerance = 1.; + + // Maximal count of heal iteration + unsigned max_heal_iteration = 10; + + explicit NSVGLineParams(double tesselation_tolerance): + tesselation_tolerance(tesselation_tolerance), + arc_tolerance(std::pow(tesselation_tolerance, 1/3.)) + {} +}; + +/// +/// Convert .svg opened by nanoSvg to shapes stored in expolygons with ids +/// +/// Parsed svg file by NanoSvg +/// Smaller will divide curve to more lines +/// NOTE: Value is in image scale +/// Maximal depth for conversion curve to lines +/// Multiplicator of point coors +/// NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer +/// Shapes from svg image - fill + stroke +ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLineParams ¶m); + +// help functions - prepare to be tested +/// Flag is y negative, when true than y coor is multiplied by -1 +Polygons to_polygons(const NSVGimage &image, const NSVGLineParams ¶m); + +void bounds(const NSVGimage &image, Vec2f &min, Vec2f &max); + +// read text data from file +std::unique_ptr read_from_disk(const std::string &path); + +using NSVGimage_ptr = std::unique_ptr; +NSVGimage_ptr nsvgParseFromFile(const std::string &svg_file_path, const char *units = "mm", float dpi = 96.0f); +NSVGimage_ptr nsvgParse(const std::string& file_data, const char *units = "mm", float dpi = 96.0f); +NSVGimage *init_image(EmbossShape::SvgFile &svg_file); + +/// +/// Iterate over shapes and calculate count +/// +/// Contain pointer to first shape +/// Count of shapes +size_t get_shapes_count(const NSVGimage &image); + +//void save(const NSVGimage &image, std::ostream &data); +//bool save(const NSVGimage &image, const std::string &svg_file_path); +} // namespace Slic3r +#endif // slic3r_NSVGUtils_hpp_ diff --git a/src/libslic3r/Point.cpp b/src/libslic3r/Point.cpp index 8799ad078..b2e1d0bdd 100644 --- a/src/libslic3r/Point.cpp +++ b/src/libslic3r/Point.cpp @@ -70,24 +70,24 @@ int Point::nearest_point_index(const PointConstPtrs &points) const { int idx = -1; double distance = -1; // double because long is limited to 2147483647 on some platforms and it's not enough - + for (PointConstPtrs::const_iterator it = points.begin(); it != points.end(); ++it) { /* If the X distance of the candidate is > than the total distance of the best previous candidate, we know we don't want it */ double d = sqr((*this)(0) - (*it)->x()); if (distance != -1 && d > distance) continue; - + /* If the Y distance of the candidate is > than the total distance of the best previous candidate, we know we don't want it */ d += sqr((*this)(1) - (*it)->y()); if (distance != -1 && d > distance) continue; - + idx = it - points.begin(); distance = d; - + if (distance < EPSILON) break; } - + return idx; } @@ -142,7 +142,7 @@ Point Point::projection_onto(const MultiPoint &poly) const { Point running_projection = poly.first_point(); double running_min = (running_projection - *this).cast().norm(); - + Lines lines = poly.lines(); for (Lines::const_iterator line = lines.begin(); line != lines.end(); ++line) { Point point_temp = this->projection_onto(*line); @@ -157,7 +157,7 @@ Point Point::projection_onto(const MultiPoint &poly) const Point Point::projection_onto(const Line &line) const { if (line.a == line.b) return line.a; - + /* (Ported from VisiLibity by Karl J. Obermeyer) The projection of point_temp onto the line determined by @@ -169,12 +169,12 @@ Point Point::projection_onto(const Line &line) const */ double lx = (double)(line.b(0) - line.a(0)); double ly = (double)(line.b(1) - line.a(1)); - double theta = ( (double)(line.b(0) - (*this)(0))*lx + (double)(line.b(1)- (*this)(1))*ly ) + double theta = ( (double)(line.b(0) - (*this)(0))*lx + (double)(line.b(1)- (*this)(1))*ly ) / ( sqr(lx) + sqr(ly) ); - + if (0.0 <= theta && theta <= 1.0) return (theta * line.a.cast() + (1.0-theta) * line.b.cast()).cast(); - + // Else pick closest endpoint. return ((line.a - *this).cast().squaredNorm() < (line.b - *this).cast().squaredNorm()) ? line.a : line.b; } @@ -188,9 +188,26 @@ bool has_duplicate_points(std::vector &&pts) return false; } +Points collect_duplicates(Points pts /* Copy */) +{ + std::sort(pts.begin(), pts.end()); + Points duplicits; + const Point *prev = &pts.front(); + for (size_t i = 1; i < pts.size(); ++i) { + const Point *act = &pts[i]; + if (*prev == *act) { + // duplicit point + if (!duplicits.empty() && duplicits.back() == *act) continue; // only unique duplicits + duplicits.push_back(*act); + } + prev = act; + } + return duplicits; +} + template BoundingBox get_extents(const Points &pts) -{ +{ BoundingBox out; BoundingBox::construct(out, pts.begin(), pts.end()); return out; diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 92ac8f652..3082dd033 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -143,6 +143,29 @@ inline std::string to_string(const Vec3d &pt) { return std::string("[") + floa std::vector transform(const std::vector& points, const Transform3f& t); Pointf3s transform(const Pointf3s& points, const Transform3d& t); +/// +/// Check whether transformation matrix contains odd number of mirroring. +/// NOTE: In code is sometime function named is_left_handed +/// +/// Transformation to check +/// Is positive determinant +inline bool has_reflection(const Transform3d &transform) { return transform.matrix().determinant() < 0; } + +/// +/// Getter on base of transformation matrix +/// +/// column index +/// source transformation +/// Base of transformation matrix +inline const Vec3d get_base(unsigned index, const Transform3d &transform) { return transform.linear().col(index); } +inline const Vec3d get_x_base(const Transform3d &transform) { return get_base(0, transform); } +inline const Vec3d get_y_base(const Transform3d &transform) { return get_base(1, transform); } +inline const Vec3d get_z_base(const Transform3d &transform) { return get_base(2, transform); } +inline const Vec3d get_base(unsigned index, const Transform3d::LinearPart &transform) { return transform.col(index); } +inline const Vec3d get_x_base(const Transform3d::LinearPart &transform) { return get_base(0, transform); } +inline const Vec3d get_y_base(const Transform3d::LinearPart &transform) { return get_base(1, transform); } +inline const Vec3d get_z_base(const Transform3d::LinearPart &transform) { return get_base(2, transform); } + template using Vec = Eigen::Matrix; class Point : public Vec2crd @@ -175,7 +198,7 @@ public: Point& operator-=(const Point& rhs) { this->x() -= rhs.x(); this->y() -= rhs.y(); return *this; } Point& operator*=(const double &rhs) { this->x() = coord_t(this->x() * rhs); this->y() = coord_t(this->y() * rhs); return *this; } Point operator*(const double &rhs) { return Point(this->x() * rhs, this->y() * rhs); } - bool both_comp(const Point &rhs, const std::string& op) { + bool both_comp(const Point &rhs, const std::string& op) { if (op == ">") return this->x() > rhs.x() && this->y() > rhs.y(); else if (op == "<") @@ -215,8 +238,8 @@ public: Point projection_onto(const Line &line) const; }; -inline bool operator<(const Point &l, const Point &r) -{ +inline bool operator<(const Point &l, const Point &r) +{ return l.x() < r.x() || (l.x() == r.x() && l.y() < r.y()); } @@ -311,6 +334,9 @@ inline bool has_duplicate_successive_points_closed(const std::vector &pts return has_duplicate_successive_points(pts) || (pts.size() >= 2 && pts.front() == pts.back()); } +// Collect adjecent(duplicit points) +Points collect_duplicates(Points pts /* Copy */); + inline bool shorter_then(const Point& p0, const coord_t len) { if (p0.x() > len || p0.x() < -len) @@ -343,7 +369,7 @@ struct PointHash { template class ClosestPointInRadiusLookup { public: - ClosestPointInRadiusLookup(coord_t search_radius, PointAccessor point_accessor = PointAccessor()) : + ClosestPointInRadiusLookup(coord_t search_radius, PointAccessor point_accessor = PointAccessor()) : m_search_radius(search_radius), m_point_accessor(point_accessor), m_grid_log2(0) { // Resolution of a grid, twice the search radius + some epsilon. @@ -432,8 +458,8 @@ public: } } } - return (value_min != nullptr && dist_min < coordf_t(m_search_radius) * coordf_t(m_search_radius)) ? - std::make_pair(value_min, dist_min) : + return (value_min != nullptr && dist_min < coordf_t(m_search_radius) * coordf_t(m_search_radius)) ? + std::make_pair(value_min, dist_min) : std::make_pair(nullptr, std::numeric_limits::max()); } @@ -553,13 +579,35 @@ inline coord_t align_to_grid(const coord_t coord, const coord_t spacing) { assert(aligned <= coord); return aligned; } -inline Point align_to_grid(Point coord, Point spacing) +inline Point align_to_grid(Point coord, Point spacing) { return Point(align_to_grid(coord.x(), spacing.x()), align_to_grid(coord.y(), spacing.y())); } -inline coord_t align_to_grid(coord_t coord, coord_t spacing, coord_t base) +inline coord_t align_to_grid(coord_t coord, coord_t spacing, coord_t base) { return base + align_to_grid(coord - base, spacing); } inline Point align_to_grid(Point coord, Point spacing, Point base) { return Point(align_to_grid(coord.x(), spacing.x(), base.x()), align_to_grid(coord.y(), spacing.y(), base.y())); } - + // MinMaxLimits + template struct MinMax + { + T min; + T max; + }; + template static bool apply(std::optional &val, const MinMax &limit) + { + if (!val.has_value()) return false; + return apply(*val, limit); + } + template static bool apply(T &val, const MinMax &limit) + { + if (val > limit.max) { + val = limit.max; + return true; + } + if (val < limit.min) { + val = limit.min; + return true; + } + return false; + } } // namespace Slic3r // start Boost @@ -568,16 +616,16 @@ inline Point align_to_grid(Point coord, Point spacing, Point base) namespace boost { namespace polygon { template <> struct geometry_concept { using type = point_concept; }; - + template <> struct point_traits { using coordinate_type = coord_t; - + static inline coordinate_type get(const Slic3r::Point& point, orientation_2d orient) { return static_cast(point((orient == HORIZONTAL) ? 0 : 1)); } }; - + template <> struct point_mutable_traits { using coordinate_type = coord_t; diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index 30c7d6e6a..a13b6a482 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -110,7 +110,7 @@ Polygons Polygon::simplify(double tolerance) const points.push_back(points.front()); Polygon p(MultiPoint::_douglas_peucker(points, tolerance)); p.points.pop_back(); - + Polygons pp; pp.push_back(p); return simplify_polygons(pp); @@ -125,7 +125,7 @@ void Polygon::triangulate_convex(Polygons* polygons) const p.points.push_back(this->points.front()); p.points.push_back(*(it-1)); p.points.push_back(*it); - + // this should be replaced with a more efficient call to a merge_collinear_segments() method if (p.area() > 0) polygons->push_back(p); } @@ -241,7 +241,7 @@ Points filter_points_by_vectors(const Points &poly, FilterFn filter) v1 = v2; p1 = p2; } - + return out; } @@ -373,8 +373,8 @@ Polygon Polygon::transform(const Transform3d& trafo) const return dstpoly; } -BoundingBox get_extents(const Polygon &poly) -{ +BoundingBox get_extents(const Polygon &poly) +{ return poly.bounding_box(); } @@ -389,8 +389,8 @@ BoundingBox get_extents(const Polygons &polygons) return bb; } -BoundingBox get_extents_rotated(const Polygon &poly, double angle) -{ +BoundingBox get_extents_rotated(const Polygon &poly, double angle) +{ return get_extents_rotated(poly.points, angle); } @@ -454,6 +454,32 @@ bool has_duplicate_points(const Polygons &polys) #endif } +bool remove_same_neighbor(Polygon &polygon) +{ + Points &points = polygon.points; + if (points.empty()) return false; + auto last = std::unique(points.begin(), points.end()); + + // remove first and last neighbor duplication + if (const Point &last_point = *(last - 1); last_point == points.front()) { --last; } + + // no duplicits + if (last == points.end()) return false; + + points.erase(last, points.end()); + return true; +} + +bool remove_same_neighbor(Polygons &polygons) +{ + if (polygons.empty()) return false; + bool exist = false; + for (Polygon &polygon : polygons) exist |= remove_same_neighbor(polygon); + // remove empty polygons + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const Polygon &p) { return p.points.size() <= 2; }), polygons.end()); + return exist; +} + static inline bool is_stick(const Point &p1, const Point &p2, const Point &p3) { Point v1 = p2 - p1; @@ -509,7 +535,7 @@ bool remove_sticks(Polygons &polys) for (size_t i = 0; i < polys.size(); ++ i) { modified |= remove_sticks(polys[i]); if (polys[i].points.size() >= 3) { - if (j < i) + if (j < i) std::swap(polys[i].points, polys[j].points); ++ j; } @@ -525,7 +551,7 @@ bool remove_degenerate(Polygons &polys) size_t j = 0; for (size_t i = 0; i < polys.size(); ++ i) { if (polys[i].points.size() >= 3) { - if (j < i) + if (j < i) std::swap(polys[i].points, polys[j].points); ++ j; } else @@ -542,7 +568,7 @@ bool remove_small(Polygons &polys, double min_area) size_t j = 0; for (size_t i = 0; i < polys.size(); ++ i) { if (std::abs(polys[i].area()) >= min_area) { - if (j < i) + if (j < i) std::swap(polys[i].points, polys[j].points); ++ j; } else @@ -646,7 +672,7 @@ bool overlaps(const Polygons& polys1, const Polygons& polys2) bool contains(const Polygon &polygon, const Point &p, bool border_result) { - if (const int poly_count_inside = ClipperLib::PointInPolygon(p, polygon.points); + if (const int poly_count_inside = ClipperLib::PointInPolygon(p, polygon.points); poly_count_inside == -1) return border_result; else diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index c91f8a2d5..793228c9b 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -27,7 +27,7 @@ public: Polygon(std::initializer_list points) : MultiPoint(points) {} Polygon(const Polygon &other) : MultiPoint(other.points) {} Polygon(Polygon &&other) : MultiPoint(std::move(other.points)) {} - static Polygon new_scale(const std::vector &points) { + static Polygon new_scale(const std::vector &points) { Polygon pgn; pgn.points.reserve(points.size()); for (const Vec2d &pt : points) @@ -51,7 +51,7 @@ public: // Split a closed polygon into an open polyline, with the split point duplicated at both ends. Polyline split_at_first_point() const { return this->split_at_index(0); } Points equally_spaced_points(double distance) const { return this->split_at_first_point().equally_spaced_points(distance); } - + static double area(const Points &pts); double area() const; bool is_counter_clockwise() const; @@ -86,7 +86,7 @@ public: // Projection of a point onto the polygon. Point point_projection(const Point &point) const; std::vector parameter_by_length() const; - + //BBS Polygon transform(const Transform3d& trafo) const; @@ -112,6 +112,10 @@ inline bool has_duplicate_points(Polygon &&poly) { return has_duplicate_poi inline bool has_duplicate_points(const Polygon &poly) { return has_duplicate_points(poly.points); } bool has_duplicate_points(const Polygons &polys); +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polygon &polygon); +bool remove_same_neighbor(Polygons &polygons); + inline double total_length(const Polygons &polylines) { double total = 0; for (Polygons::const_iterator it = polylines.begin(); it != polylines.end(); ++it) @@ -142,7 +146,7 @@ void remove_collinear(Polygons &polys); // Append a vector of polygons at the end of another vector of polygons. inline void polygons_append(Polygons &dst, const Polygons &src) { dst.insert(dst.end(), src.begin(), src.end()); } -inline void polygons_append(Polygons &dst, Polygons &&src) +inline void polygons_append(Polygons &dst, Polygons &&src) { if (dst.empty()) { dst = std::move(src); @@ -179,7 +183,7 @@ inline size_t count_points(const Polygons &polys) { return n_points; } -inline Points to_points(const Polygons &polys) +inline Points to_points(const Polygons &polys) { Points points; points.reserve(count_points(polys)); @@ -188,7 +192,7 @@ inline Points to_points(const Polygons &polys) return points; } -inline Lines to_lines(const Polygon &poly) +inline Lines to_lines(const Polygon &poly) { Lines lines; lines.reserve(poly.points.size()); @@ -200,7 +204,7 @@ inline Lines to_lines(const Polygon &poly) return lines; } -inline Lines to_lines(const Polygons &polys) +inline Lines to_lines(const Polygons &polys) { Lines lines; lines.reserve(count_points(polys)); @@ -245,6 +249,17 @@ inline Polylines to_polylines(Polygons &&polys) return polylines; } +// close polyline to polygon (connect first and last point in polyline) +inline Polygons to_polygons(const Polylines &polylines) +{ + Polygons out; + out.reserve(polylines.size()); + for (const Polyline &polyline : polylines) { + if (polyline.size()) out.emplace_back(polyline.points); + } + return out; +} + inline Polygons to_polygons(const std::vector &paths) { Polygons out; @@ -270,6 +285,21 @@ bool polygons_match(const Polygon &l, const Polygon &r); Polygon make_circle(double radius, double error); Polygon make_circle_num_segments(double radius, size_t num_segments); +/// +/// Define point laying on polygon +/// keep index of polygon line and point coordinate +/// +struct PolygonPoint +{ + // index of line inside of polygon + // 0 .. from point polygon[0] to polygon[1] + size_t index; + + // Point, which lay on line defined by index + Point point; +}; +using PolygonPoints = std::vector; + bool overlaps(const Polygons& polys1, const Polygons& polys2); } // Slic3r @@ -322,7 +352,7 @@ namespace boost { namespace polygon { return polygon; } }; - + template <> struct geometry_concept { typedef polygon_set_concept type; }; diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp index 783f91229..dced6928f 100644 --- a/src/libslic3r/Polyline.cpp +++ b/src/libslic3r/Polyline.cpp @@ -13,7 +13,7 @@ const Point& Polyline::leftmost_point() const { const Point *p = &this->points.front(); for (Points::const_iterator it = this->points.begin() + 1; it != this->points.end(); ++ it) { - if (it->x() < p->x()) + if (it->x() < p->x()) p = &(*it); } return *p; @@ -122,7 +122,7 @@ Points Polyline::equally_spaced_points(double distance) const Points points; points.emplace_back(this->first_point()); double len = 0; - + for (Points::const_iterator it = this->points.begin() + 1; it != this->points.end(); ++it) { Vec2d p1 = (it-1)->cast(); Vec2d v = it->cast() - p1; @@ -172,7 +172,7 @@ Polylines Polyline::equally_spaced_lines(double distance) const if (len == distance) { line.append(*it); lines.emplace_back(line); - + line.clear(); line.append(*it); len = 0; @@ -202,7 +202,7 @@ template void Polyline::simplify_by_visibility(const T &area) { Points &pp = this->points; - + size_t s = 0; bool did_erase = false; for (size_t i = s+2; i < pp.size(); i = s + 2) { @@ -232,7 +232,7 @@ void Polyline::split_at(Point &point, Polyline* p1, Polyline* p2) const point = p1->is_valid()? p1->last_point(): p2->first_point(); return; } - + //1 find the line to split at size_t line_idx = 0; Point p = this->first_point(); @@ -530,6 +530,30 @@ BoundingBox get_extents(const Polylines &polylines) return bb; } +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polyline &polyline) +{ + Points &points = polyline.points; + if (points.empty()) return false; + auto last = std::unique(points.begin(), points.end()); + + // no duplicits + if (last == points.end()) return false; + + points.erase(last, points.end()); + return true; +} + +bool remove_same_neighbor(Polylines &polylines) +{ + if (polylines.empty()) return false; + bool exist = false; + for (Polyline &polyline : polylines) exist |= remove_same_neighbor(polyline); + // remove empty polylines + polylines.erase(std::remove_if(polylines.begin(), polylines.end(), [](const Polyline &p) { return p.points.size() <= 1; }), polylines.end()); + return exist; +} + const Point& leftmost_point(const Polylines &polylines) { if (polylines.empty()) @@ -550,7 +574,7 @@ bool remove_degenerate(Polylines &polylines) size_t j = 0; for (size_t i = 0; i < polylines.size(); ++ i) { if (polylines[i].points.size() >= 2) { - if (j < i) + if (j < i) std::swap(polylines[i].points, polylines[j].points); ++ j; } else diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index 074db1a1c..abf2575cb 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -21,7 +21,7 @@ public: Polyline() {}; Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {} Polyline(Polyline &&other) : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {} - Polyline(std::initializer_list list) : MultiPoint(list) { + Polyline(std::initializer_list list) : MultiPoint(list) { fitting_result.clear(); } explicit Polyline(const Point &p1, const Point &p2) { @@ -55,7 +55,7 @@ public: pl.fitting_result.clear(); return pl; } - + void append(const Point &point) { //BBS: don't need to append same point if (!this->empty() && this->last_point() == point) @@ -128,7 +128,7 @@ public: std::vector fitting_result; //BBS: simplify points by arc fitting void simplify_by_fitting_arc(double tolerance); - //BBS: + //BBS: Polylines equally_spaced_lines(double distance) const; private: @@ -154,6 +154,10 @@ public: extern BoundingBox get_extents(const Polyline &polyline); extern BoundingBox get_extents(const Polylines &polylines); +// Return True when erase some otherwise False. +bool remove_same_neighbor(Polyline &polyline); +bool remove_same_neighbor(Polylines &polylines); + inline double total_length(const Polylines &polylines) { double total = 0; for (const Polyline &pl : polylines) @@ -161,7 +165,7 @@ inline double total_length(const Polylines &polylines) { return total; } -inline Lines to_lines(const Polyline &poly) +inline Lines to_lines(const Polyline &poly) { Lines lines; if (poly.points.size() >= 2) { @@ -172,7 +176,7 @@ inline Lines to_lines(const Polyline &poly) return lines; } -inline Lines to_lines(const Polylines &polys) +inline Lines to_lines(const Polylines &polys) { size_t n_lines = 0; for (size_t i = 0; i < polys.size(); ++ i) @@ -206,12 +210,12 @@ inline Polylines to_polylines(std::vector &&paths) return out; } -inline void polylines_append(Polylines &dst, const Polylines &src) -{ +inline void polylines_append(Polylines &dst, const Polylines &src) +{ dst.insert(dst.end(), src.begin(), src.end()); } -inline void polylines_append(Polylines &dst, Polylines &&src) +inline void polylines_append(Polylines &dst, Polylines &&src) { if (dst.empty()) { dst = std::move(src); diff --git a/src/libslic3r/TextConfiguration.hpp b/src/libslic3r/TextConfiguration.hpp new file mode 100644 index 000000000..d33b67b35 --- /dev/null +++ b/src/libslic3r/TextConfiguration.hpp @@ -0,0 +1,187 @@ +#ifndef slic3r_TextConfiguration_hpp_ +#define slic3r_TextConfiguration_hpp_ + +#include +#include +#include +#include +#include +#include +#include +#include "Point.hpp" // Transform3d + +namespace Slic3r { + +/// +/// User modifiable property of text style +/// NOTE: OnEdit fix serializations: EmbossStylesSerializable, TextConfigurationSerialization +/// +struct FontProp +{ + // define extra space between letters, negative mean closer letter + // When not set value is zero and is not stored + std::optional char_gap; // [in font point] + + // define extra space between lines, negative mean closer lines + // When not set value is zero and is not stored + std::optional line_gap; // [in font point] + + // positive value mean wider character shape + // negative value mean tiner character shape + // When not set value is zero and is not stored + std::optional boldness; // [in mm] + + // positive value mean italic of character (CW) + // negative value mean CCW skew (unItalic) + // When not set value is zero and is not stored + std::optional skew; // [ration x:y] + + // Parameter for True Type Font collections + // Select index of font in collection + std::optional collection_number; + + // Distiguish projection per glyph + bool per_glyph; + + // NOTE: way of serialize to 3mf force that zero must be default value + enum class HorizontalAlign { left = 0, center, right }; + enum class VerticalAlign { top = 0, center, bottom }; + using Align = std::pair; + // change pivot of text + // When not set, center is used and is not stored + Align align = Align(HorizontalAlign::center, VerticalAlign::center); + + ////// + // Duplicit data to wxFontDescriptor + // used for store/load .3mf file + ////// + + // Height of text line (letters) + // duplicit to wxFont::PointSize + float size_in_mm; // [in mm] + + // Additional data about font to be able to find substitution, + // when same font is not installed + std::optional family; + std::optional face_name; + std::optional style; + std::optional weight; + + /// + /// Only constructor with restricted values + /// + /// Y size of text [in mm] + /// Z size of text [in mm] + FontProp(float line_height = 10.f) : size_in_mm(line_height), per_glyph(false) + {} + + bool operator==(const FontProp& other) const { + return + char_gap == other.char_gap && + line_gap == other.line_gap && + per_glyph == other.per_glyph && + align == other.align && + is_approx(size_in_mm, other.size_in_mm) && + is_approx(boldness, other.boldness) && + is_approx(skew, other.skew); + } + + // undo / redo stack recovery + template void save(Archive &ar) const + { + ar(size_in_mm, per_glyph, align.first, align.second); + cereal::save(ar, char_gap); + cereal::save(ar, line_gap); + cereal::save(ar, boldness); + cereal::save(ar, skew); + cereal::save(ar, collection_number); + } + template void load(Archive &ar) + { + ar(size_in_mm, per_glyph, align.first, align.second); + cereal::load(ar, char_gap); + cereal::load(ar, line_gap); + cereal::load(ar, boldness); + cereal::load(ar, skew); + cereal::load(ar, collection_number); + } +}; + +/// +/// Style of embossed text +/// (Path + Type) must define how to open font for using on different OS +/// NOTE: OnEdit fix serializations: EmbossStylesSerializable, TextConfigurationSerialization +/// +struct EmbossStyle +{ + // Human readable name of style it is shown in GUI + std::string name; + + // Define how to open font + // Meaning depend on type + std::string path; + + enum class Type; + // Define what is stored in path + Type type { Type::undefined }; + + // User modification of font style + FontProp prop; + + // when name is empty than Font item was loaded from .3mf file + // and potentionaly it is not reproducable + // define data stored in path + // when wx change way of storing add new descriptor Type + enum class Type { + undefined = 0, + + // wx font descriptors are platform dependent + // path is font descriptor generated by wxWidgets + wx_win_font_descr, // on Windows + wx_lin_font_descr, // on Linux + wx_mac_font_descr, // on Max OS + + // TrueTypeFont file loacation on computer + // for privacy: only filename is stored into .3mf + file_path + }; + + bool operator==(const EmbossStyle &other) const + { + return + type == other.type && + prop == other.prop && + name == other.name && + path == other.path + ; + } + + // undo / redo stack recovery + template void serialize(Archive &ar){ ar(name, path, type, prop); } +}; + +// Emboss style name inside vector is unique +// It is not map beacuse items has own order (view inside of slect) +// It is stored into AppConfig by EmbossStylesSerializable +using EmbossStyles = std::vector; + +/// +/// Define how to create 'Text volume' +/// It is stored into .3mf by TextConfigurationSerialization +/// It is part of ModelVolume optional data +/// +struct TextConfiguration +{ + // Style of embossed text + EmbossStyle style; + + // Embossed text value + std::string text = "None"; + + // undo / redo stack recovery + template void serialize(Archive &ar) { ar(style, text); } +}; + +} // namespace Slic3r + +#endif // slic3r_TextConfiguration_hpp_ diff --git a/src/libslic3r/Timer.cpp b/src/libslic3r/Timer.cpp new file mode 100644 index 000000000..34d3af034 --- /dev/null +++ b/src/libslic3r/Timer.cpp @@ -0,0 +1,21 @@ +#include "Timer.hpp" +#include + +using namespace std::chrono; + +Slic3r::Timer::Timer(const std::string &name) : m_name(name), m_start(steady_clock::now()) {} + +Slic3r::Timer::~Timer() +{ + BOOST_LOG_TRIVIAL(debug) << "Timer '" << m_name << "' spend " << + duration_cast(steady_clock::now() - m_start).count() << "ms"; +} + + +namespace Slic3r::Timing { + +void TimeLimitAlarm::report_time_exceeded() const { + BOOST_LOG_TRIVIAL(error) << "Time limit exceeded for " << m_limit_exceeded_message << ": " << m_timer.elapsed_seconds() << "s"; +} + +} diff --git a/src/libslic3r/Timer.hpp b/src/libslic3r/Timer.hpp new file mode 100644 index 000000000..febe2af38 --- /dev/null +++ b/src/libslic3r/Timer.hpp @@ -0,0 +1,92 @@ +#ifndef libslic3r_Timer_hpp_ +#define libslic3r_Timer_hpp_ + +#include +#include + +namespace Slic3r { + +/// +/// Instance of this class is used for measure time consumtion +/// of block code until instance is alive and write result to debug output +/// +class Timer +{ + std::string m_name; + std::chrono::steady_clock::time_point m_start; +public: + /// + /// name describe timer + /// + /// Describe timer in consol log + Timer(const std::string& name); + + /// + /// name describe timer + /// + ~Timer(); +}; + +namespace Timing { + + // Timing code from Catch2 unit testing library + static inline uint64_t nanoseconds_since_epoch() { + return std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + } + + // Timing code from Catch2 unit testing library + class Timer { + public: + void start() { + m_nanoseconds = nanoseconds_since_epoch(); + } + uint64_t elapsed_nanoseconds() const { + return nanoseconds_since_epoch() - m_nanoseconds; + } + uint64_t elapsed_microseconds() const { + return elapsed_nanoseconds() / 1000; + } + unsigned int elapsed_milliseconds() const { + return static_cast(elapsed_microseconds()/1000); + } + double elapsed_seconds() const { + return elapsed_microseconds() / 1000000.0; + } + private: + uint64_t m_nanoseconds = 0; + }; + + // Emits a Boost::log error if the life time of this timing object exceeds a limit. + class TimeLimitAlarm { + public: + TimeLimitAlarm(uint64_t time_limit_nanoseconds, std::string_view limit_exceeded_message) : + m_time_limit_nanoseconds(time_limit_nanoseconds), m_limit_exceeded_message(limit_exceeded_message) { + m_timer.start(); + } + ~TimeLimitAlarm() { + auto elapsed = m_timer.elapsed_nanoseconds(); + if (elapsed > m_time_limit_nanoseconds) + this->report_time_exceeded(); + } + static TimeLimitAlarm new_nanos(uint64_t time_limit_nanoseconds, std::string_view limit_exceeded_message) { + return TimeLimitAlarm(time_limit_nanoseconds, limit_exceeded_message); + } + static TimeLimitAlarm new_milis(uint64_t time_limit_milis, std::string_view limit_exceeded_message) { + return TimeLimitAlarm(uint64_t(time_limit_milis) * 1000000l, limit_exceeded_message); + } + static TimeLimitAlarm new_seconds(uint64_t time_limit_seconds, std::string_view limit_exceeded_message) { + return TimeLimitAlarm(uint64_t(time_limit_seconds) * 1000000000l, limit_exceeded_message); + } + private: + void report_time_exceeded() const; + + Timer m_timer; + uint64_t m_time_limit_nanoseconds; + std::string_view m_limit_exceeded_message; + }; + +} // namespace Catch + +} // namespace Slic3r + +#endif // libslic3r_Timer_hpp_ diff --git a/src/libslic3r/Triangulation.cpp b/src/libslic3r/Triangulation.cpp new file mode 100644 index 000000000..bb1fba96e --- /dev/null +++ b/src/libslic3r/Triangulation.cpp @@ -0,0 +1,329 @@ +#include "Triangulation.hpp" +#include "IntersectionPoints.hpp" +#include +#include +#include +#include +#include + +using namespace Slic3r; +namespace priv{ +inline void insert_edges(Triangulation::HalfEdges &edges, uint32_t &offset, const Polygon &polygon, const Triangulation::Changes& changes) { + const Points &pts = polygon.points; + uint32_t size = static_cast(pts.size()); + uint32_t last_index = offset + size - 1; + uint32_t prev_index = changes[last_index]; + for (uint32_t i = 0; i < size; ++i) { + uint32_t index = changes[offset + i]; + // when duplicit points are neighbor + if (prev_index == index) continue; + edges.push_back({prev_index, index}); + prev_index = index; + } + offset += size; +} + +inline void insert_edges(Triangulation::HalfEdges &edges, uint32_t &offset, const Polygon &polygon) { + const Points &pts = polygon.points; + uint32_t size = static_cast(pts.size()); + uint32_t prev_index = offset + size - 1; + for (uint32_t i = 0; i < size; ++i) { + uint32_t index = offset + i; + edges.push_back({prev_index, index}); + prev_index = index; + } + offset += size; +} + +inline bool has_bidirectional_constrained( + const Triangulation::HalfEdges &constrained) +{ + for (const auto &c : constrained) { + auto key = std::make_pair(c.second, c.first); + auto it = std::lower_bound(constrained.begin(), constrained.end(), + key); + if (it != constrained.end() && *it == key) return true; + } + return false; +} + +inline bool is_unique(const Points &points) { + Points pts = points; // copy + std::sort(pts.begin(), pts.end()); + auto it = std::adjacent_find(pts.begin(), pts.end()); + return it == pts.end(); +} + +inline bool has_self_intersection( + const Points &points, + const Triangulation::HalfEdges &constrained_half_edges) +{ + Lines lines; + lines.reserve(constrained_half_edges.size()); + for (const auto &he : constrained_half_edges) + lines.emplace_back(points[he.first], points[he.second]); + return !get_intersections(lines).empty(); +} + +} // namespace priv + +//#define VISUALIZE_TRIANGULATION +#ifdef VISUALIZE_TRIANGULATION +#include "admesh/stl.h" // indexed triangle set +static void visualize(const Points &points, + const Triangulation::Indices &indices, + const char *filename) +{ + // visualize + indexed_triangle_set its; + its.vertices.reserve(points.size()); + for (const Point &p : points) its.vertices.emplace_back(p.x(), p.y(), 0.); + its.indices = indices; + its_write_obj(its, filename); +} +#endif // VISUALIZE_TRIANGULATION + +Triangulation::Indices Triangulation::triangulate(const Points &points, + const HalfEdges &constrained_half_edges) +{ + assert(!points.empty()); + assert(!constrained_half_edges.empty()); + // constrained must be sorted + assert(std::is_sorted(constrained_half_edges.begin(), + constrained_half_edges.end())); + // check that there is no duplicit constrained edge + assert(std::adjacent_find(constrained_half_edges.begin(), constrained_half_edges.end()) == constrained_half_edges.end()); + // edges can NOT contain bidirectional constrained + assert(!priv::has_bidirectional_constrained(constrained_half_edges)); + // check that there is only unique poistion of points + assert(priv::is_unique(points)); + assert(!priv::has_self_intersection(points, constrained_half_edges)); + // use cgal triangulation + using K = CGAL::Exact_predicates_inexact_constructions_kernel; + using Vb = CGAL::Triangulation_vertex_base_with_info_2; + using Fb = CGAL::Constrained_triangulation_face_base_2; + using Tds = CGAL::Triangulation_data_structure_2; + using CDT = CGAL::Constrained_Delaunay_triangulation_2; + + // construct a constrained triangulation + CDT cdt; + { + std::vector vertices_handle(points.size()); // for constriants + using Point_with_ord = std::pair; + using SearchTrait = CGAL::Spatial_sort_traits_adapter_2 + >; + + std::vector cdt_points; + cdt_points.reserve(points.size()); + size_t ord = 0; + for (const auto &p : points) + cdt_points.emplace_back(std::make_pair(CDT::Point{p.x(), p.y()}, ord++)); + + SearchTrait st; + CGAL::spatial_sort(cdt_points.begin(), cdt_points.end(), st); + CDT::Face_handle f; + for (const auto& p : cdt_points) { + auto handle = cdt.insert(p.first, f); + handle->info() = p.second; + vertices_handle[p.second] = handle; + f = handle->face(); + } + + // Constrain the triangulation. + for (const HalfEdge &edge : constrained_half_edges) + cdt.insert_constraint(vertices_handle[edge.first], vertices_handle[edge.second]); + } + + auto faces = cdt.finite_face_handles(); + + // Unmark constrained edges of outside faces. + size_t num_faces = 0; + for (CDT::Face_handle fh : faces) { + for (int i = 0; i < 3; ++i) { + if (!fh->is_constrained(i)) continue; + auto key = std::make_pair(fh->vertex((i + 2) % 3)->info(), fh->vertex((i + 1) % 3)->info()); + auto it = std::lower_bound(constrained_half_edges.begin(), constrained_half_edges.end(), key); + if (it == constrained_half_edges.end() || *it != key) continue; + // This face contains a constrained edge and it is outside. + for (int j = 0; j < 3; ++ j) + fh->set_constraint(j, false); + --num_faces; + break; + } + ++num_faces; + } + + auto inside = [](CDT::Face_handle &fh) { + return fh->neighbor(0) != fh && + (fh->is_constrained(0) || + fh->is_constrained(1) || + fh->is_constrained(2)); + }; + +#ifdef VISUALIZE_TRIANGULATION + std::vector indices2; + indices2.reserve(num_faces); + for (CDT::Face_handle fh : faces) + if (inside(fh)) indices2.emplace_back(fh->vertex(0)->info(), fh->vertex(1)->info(), fh->vertex(2)->info()); + visualize(points, indices2, "C:/data/temp/triangulation_without_floodfill.obj"); +#endif // VISUALIZE_TRIANGULATION + + // Propagate inside the constrained regions. + std::vector queue; + queue.reserve(num_faces); + for (CDT::Face_handle seed : faces){ + if (!inside(seed)) continue; + // Seed fill to neighbor faces. + queue.emplace_back(seed); + while (! queue.empty()) { + CDT::Face_handle fh = queue.back(); + queue.pop_back(); + for (int i = 0; i < 3; ++i) { + if (fh->is_constrained(i)) continue; + // Propagate along this edge. + fh->set_constraint(i, true); + CDT::Face_handle nh = fh->neighbor(i); + bool was_inside = inside(nh); + // Mark the other side of this edge. + nh->set_constraint(nh->index(fh), true); + if (! was_inside) + queue.push_back(nh); + } + } + } + + std::vector indices; + indices.reserve(num_faces); + for (CDT::Face_handle fh : faces) + if (inside(fh)) + indices.emplace_back(fh->vertex(0)->info(), fh->vertex(1)->info(), fh->vertex(2)->info()); + +#ifdef VISUALIZE_TRIANGULATION + visualize(points, indices, "C:/data/temp/triangulation.obj"); +#endif // VISUALIZE_TRIANGULATION + + return indices; +} + +Triangulation::Indices Triangulation::triangulate(const Polygon &polygon) +{ + const Points &pts = polygon.points; + HalfEdges edges; + edges.reserve(pts.size()); + uint32_t offset = 0; + priv::insert_edges(edges, offset, polygon); + std::sort(edges.begin(), edges.end()); + return triangulate(pts, edges); +} + +Triangulation::Indices Triangulation::triangulate(const Polygons &polygons) +{ + size_t count = count_points(polygons); + Points points; + points.reserve(count); + + HalfEdges edges; + edges.reserve(count); + uint32_t offset = 0; + + for (const Polygon &polygon : polygons) { + Slic3r::append(points, polygon.points); + priv::insert_edges(edges, offset, polygon); + } + + std::sort(edges.begin(), edges.end()); + return triangulate(points, edges); +} + +Triangulation::Indices Triangulation::triangulate(const ExPolygon &expolygon){ + ExPolygons expolys({expolygon}); + return triangulate(expolys); +} + +Triangulation::Indices Triangulation::triangulate(const ExPolygons &expolygons){ + Points pts = to_points(expolygons); + Points d_pts = collect_duplicates(pts); + if (d_pts.empty()) return triangulate(expolygons, pts); + + Changes changes = create_changes(pts, d_pts); + Indices indices = triangulate(expolygons, pts, changes); + // reverse map for changes + Changes changes2(changes.size(), std::numeric_limits::max()); + for (size_t i = 0; i < changes.size(); ++i) + changes2[changes[i]] = i; + + // convert indices into expolygons indicies + for (Vec3i32 &t : indices) + for (size_t ti = 0; ti < 3; ti++) t[ti] = changes2[t[ti]]; + + return indices; +} + +Triangulation::Indices Triangulation::triangulate(const ExPolygons &expolygons, const Points &points) +{ + assert(count_points(expolygons) == points.size()); + // when contain duplicit coordinate in points will not work properly + assert(collect_duplicates(points).empty()); + + HalfEdges edges; + edges.reserve(points.size()); + uint32_t offset = 0; + for (const ExPolygon &expolygon : expolygons) { + priv::insert_edges(edges, offset, expolygon.contour); + for (const Polygon &hole : expolygon.holes) + priv::insert_edges(edges, offset, hole); + } + std::sort(edges.begin(), edges.end()); + return triangulate(points, edges); +} + +Triangulation::Indices Triangulation::triangulate(const ExPolygons &expolygons, const Points& points, const Changes& changes) +{ + assert(!points.empty()); + assert(count_points(expolygons) == points.size()); + assert(changes.size() == points.size()); + // IMPROVE: search from end and somehow distiquish that value is not a change + uint32_t count_points = *std::max_element(changes.begin(), changes.end())+1; + Points pts(count_points); + for (size_t i = 0; i < changes.size(); i++) + pts[changes[i]] = points[i]; + + HalfEdges edges; + edges.reserve(points.size()); + uint32_t offset = 0; + for (const ExPolygon &expolygon : expolygons) { + priv::insert_edges(edges, offset, expolygon.contour, changes); + for (const Polygon &hole : expolygon.holes) + priv::insert_edges(edges, offset, hole, changes); + } + + std::sort(edges.begin(), edges.end()); + return triangulate(pts, edges); +} + +Triangulation::Changes Triangulation::create_changes(const Points &points, const Points &duplicits) +{ + assert(!duplicits.empty()); + assert(duplicits.size() < points.size()/2); + std::vector duplicit_indices(duplicits.size(), std::numeric_limits::max()); + Changes changes; + changes.reserve(points.size()); + uint32_t index = 0; + for (const Point &p: points) { + auto it = std::lower_bound(duplicits.begin(), duplicits.end(), p); + if (it == duplicits.end() || *it != p) { + changes.push_back(index); + ++index; + continue; + } + uint32_t &d_index = duplicit_indices[it - duplicits.begin()]; + if (d_index == std::numeric_limits::max()) { + d_index = index; + changes.push_back(index); + ++index; + } else { + changes.push_back(d_index); + } + } + return changes; +} diff --git a/src/libslic3r/Triangulation.hpp b/src/libslic3r/Triangulation.hpp new file mode 100644 index 000000000..00c46cdef --- /dev/null +++ b/src/libslic3r/Triangulation.hpp @@ -0,0 +1,72 @@ +#ifndef libslic3r_Triangulation_hpp_ +#define libslic3r_Triangulation_hpp_ + +#include +#include +#include +#include +#include + +namespace Slic3r { + +class Triangulation +{ +public: + Triangulation() = delete; + + // define oriented connection of 2 vertices(defined by its index) + using HalfEdge = std::pair; + using HalfEdges = std::vector; + using Indices = std::vector; + + /// + /// Connect points by triangulation to create filled surface by triangles + /// Input points have to be unique + /// Inspiration for make unique points is Emboss::dilate_to_unique_points + /// + /// Points to connect + /// Constraint for edges, pair is from point(first) to + /// point(second), sorted lexicographically + /// Triangles + static Indices triangulate(const Points &points, + const HalfEdges &half_edges); + static Indices triangulate(const Polygon &polygon); + static Indices triangulate(const Polygons &polygons); + static Indices triangulate(const ExPolygon &expolygon); + static Indices triangulate(const ExPolygons &expolygons); + + // Map for convert original index to set without duplication + // from_index + using Changes = std::vector; + + /// + /// Create conversion map from original index into new + /// with respect of duplicit point + /// + /// input set of points + /// duplicit points collected from points + /// Conversion map for point index + static Changes create_changes(const Points &points, const Points &duplicits); + + /// + /// Triangulation for expolygons, speed up when points are already collected + /// NOTE: Not working properly for ExPolygons with multiple point on same coordinate + /// You should check it by "collect_changes" + /// + /// Input shape to triangulation - define edges + /// Points from expolygons + /// Triangle indices + static Indices triangulate(const ExPolygons &expolygons, const Points& points); + + /// + /// Triangulation for expolygons containing multiple points with same coordinate + /// + /// Input shape to triangulation - define edge + /// Points from expolygons + /// Changes swap for indicies into points + /// Triangle indices + static Indices triangulate(const ExPolygons &expolygons, const Points& points, const Changes& changes); +}; + +} // namespace Slic3r +#endif // libslic3r_Triangulation_hpp_ \ No newline at end of file diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 753bf7446..084f44f52 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -354,6 +354,7 @@ inline typename CONTAINER_TYPE::value_type& next_value_modulo(typename CONTAINER } extern std::string xml_escape(std::string text, bool is_marked = false); +extern std::string xml_escape_double_quotes_attribute_value(std::string text); extern std::string xml_unescape(std::string text); diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 2fd34b643..226b01e7b 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -1210,6 +1210,34 @@ std::string xml_escape(std::string text, bool is_marked/* = false*/) return text; } +// Definition of escape symbols https://www.w3.org/TR/REC-xml/#AVNormalize +// During the read of xml attribute normalization of white spaces is applied +// Soo for not lose white space character it is escaped before store +std::string xml_escape_double_quotes_attribute_value(std::string text) +{ + std::string::size_type pos = 0; + for (;;) { + pos = text.find_first_of("\"&<\r\n\t", pos); + if (pos == std::string::npos) break; + + std::string replacement; + switch (text[pos]) { + case '\"': replacement = """; break; + case '&': replacement = "&"; break; + case '<': replacement = "<"; break; + case '\r': replacement = " "; break; + case '\n': replacement = " "; break; + case '\t': replacement = " "; break; + default: break; + } + + text.replace(pos, 1, replacement); + pos += replacement.size(); + } + + return text; +} + std::string xml_unescape(std::string s) { std::string ret; diff --git a/src/nanosvg/nanosvg.h b/src/nanosvg/nanosvg.h index 57bcb7c2c..559502708 100644 --- a/src/nanosvg/nanosvg.h +++ b/src/nanosvg/nanosvg.h @@ -72,6 +72,7 @@ extern "C" { */ enum NSVGpaintType { + NSVG_PAINT_UNDEF = -1, NSVG_PAINT_NONE = 0, NSVG_PAINT_COLOR = 1, NSVG_PAINT_LINEAR_GRADIENT = 2, @@ -119,7 +120,7 @@ typedef struct NSVGgradient { } NSVGgradient; typedef struct NSVGpaint { - char type; + signed char type; union { unsigned int color; NSVGgradient* gradient; @@ -143,14 +144,17 @@ typedef struct NSVGshape float opacity; // Opacity of the shape. float strokeWidth; // Stroke width (scaled). float strokeDashOffset; // Stroke dash offset (scaled). - float strokeDashArray[8]; // Stroke dash array (scaled). - char strokeDashCount; // Number of dash values in dash array. + float strokeDashArray[8]; // Stroke dash array (scaled). + char strokeDashCount; // Number of dash values in dash array. char strokeLineJoin; // Stroke join type. char strokeLineCap; // Stroke cap type. float miterLimit; // Miter limit char fillRule; // Fill rule, see NSVGfillRule. unsigned char flags; // Logical or of NSVG_FLAGS_* flags float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. + char fillGradient[64]; // Optional 'id' of fill gradient + char strokeGradient[64]; // Optional 'id' of stroke gradient + float xform[6]; // Root transformation for fill/stroke gradient NSVGpath* paths; // Linked list of paths in the image. struct NSVGshape* next; // Pointer to next shape, or NULL if last element. } NSVGshape; @@ -181,16 +185,13 @@ void nsvgDelete(NSVGimage* image); #endif #endif -#endif // NANOSVG_H - #ifdef NANOSVG_IMPLEMENTATION #include #include +#include #include -#include - #define NSVG_PI (3.14159265358979323846264338327f) #define NSVG_KAPPA90 (0.5522847493f) // Length proportional to radius of a cubic bezier handle for 90deg arcs. @@ -227,11 +228,6 @@ static int nsvg__isdigit(char c) return c >= '0' && c <= '9'; } -static int nsvg__isnum(char c) -{ - return strchr("0123456789+-.eE", c) != 0; -} - static NSVG_INLINE float nsvg__minf(float a, float b) { return a < b ? a : b; } static NSVG_INLINE float nsvg__maxf(float a, float b) { return a > b ? a : b; } @@ -402,7 +398,7 @@ typedef struct NSVGgradientData { char id[64]; char ref[64]; - char type; + signed char type; union { NSVGlinearData linear; NSVGradialData radial; @@ -618,7 +614,7 @@ static void nsvg__curveBounds(float* bounds, float* curve) } } -static NSVGparser* nsvg__createParser() +static NSVGparser* nsvg__createParser(void) { NSVGparser* p; p = (NSVGparser*)malloc(sizeof(NSVGparser)); @@ -738,9 +734,11 @@ static void nsvg__lineTo(NSVGparser* p, float x, float y) static void nsvg__cubicBezTo(NSVGparser* p, float cpx1, float cpy1, float cpx2, float cpy2, float x, float y) { - nsvg__addPoint(p, cpx1, cpy1); - nsvg__addPoint(p, cpx2, cpy2); - nsvg__addPoint(p, x, y); + if (p->npts > 0) { + nsvg__addPoint(p, cpx1, cpy1); + nsvg__addPoint(p, cpx2, cpy2); + nsvg__addPoint(p, x, y); + } } static NSVGattrib* nsvg__getAttr(NSVGparser* p) @@ -810,7 +808,9 @@ static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c, float orig, static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) { NSVGgradientData* grad = p->gradients; - while (grad) { + if (id == NULL || *id == '\0') + return NULL; + while (grad != NULL) { if (strcmp(grad->id, id) == 0) return grad; grad = grad->next; @@ -818,28 +818,34 @@ static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) return NULL; } -static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, char* paintType) +static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, float *xform, signed char* paintType) { - NSVGattrib* attr = nsvg__getAttr(p); NSVGgradientData* data = NULL; NSVGgradientData* ref = NULL; NSVGgradientStop* stops = NULL; NSVGgradient* grad; float ox, oy, sw, sh, sl; int nstops = 0; + int refIter; data = nsvg__findGradientData(p, id); if (data == NULL) return NULL; // TODO: use ref to fill in all unset values too. ref = data; + refIter = 0; while (ref != NULL) { + NSVGgradientData* nextRef = NULL; if (stops == NULL && ref->stops != NULL) { stops = ref->stops; nstops = ref->nstops; break; } - ref = nsvg__findGradientData(p, ref->ref); + nextRef = nsvg__findGradientData(p, ref->ref); + if (nextRef == ref) break; // prevent infite loops on malformed data + ref = nextRef; + refIter++; + if (refIter > 32) break; // prevent infite loops on malformed data } if (stops == NULL) return NULL; @@ -888,7 +894,7 @@ static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const f } nsvg__xformMultiply(grad->xform, data->xform); - nsvg__xformMultiply(grad->xform, attr->xform); + nsvg__xformMultiply(grad->xform, xform); grad->spread = data->spread; memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop)); @@ -952,6 +958,9 @@ static void nsvg__addShape(NSVGparser* p) memset(shape, 0, sizeof(NSVGshape)); memcpy(shape->id, attr->id, sizeof shape->id); + memcpy(shape->fillGradient, attr->fillGradient, sizeof shape->fillGradient); + memcpy(shape->strokeGradient, attr->strokeGradient, sizeof shape->strokeGradient); + memcpy(shape->xform, attr->xform, sizeof shape->xform); scale = nsvg__getAverageScale(attr->xform); shape->strokeWidth = attr->strokeWidth * scale; shape->strokeDashOffset = attr->strokeDashOffset * scale; @@ -987,13 +996,7 @@ static void nsvg__addShape(NSVGparser* p) shape->fill.color = attr->fillColor; shape->fill.color |= (unsigned int)(attr->fillOpacity*255) << 24; } else if (attr->hasFill == 2) { - float inv[6], localBounds[4]; - nsvg__xformInverse(inv, attr->xform); - nsvg__getLocalBounds(localBounds, shape, inv); - shape->fill.gradient = nsvg__createGradient(p, attr->fillGradient, localBounds, &shape->fill.type); - if (shape->fill.gradient == NULL) { - shape->fill.type = NSVG_PAINT_NONE; - } + shape->fill.type = NSVG_PAINT_UNDEF; } // Set stroke @@ -1004,12 +1007,7 @@ static void nsvg__addShape(NSVGparser* p) shape->stroke.color = attr->strokeColor; shape->stroke.color |= (unsigned int)(attr->strokeOpacity*255) << 24; } else if (attr->hasStroke == 2) { - float inv[6], localBounds[4]; - nsvg__xformInverse(inv, attr->xform); - nsvg__getLocalBounds(localBounds, shape, inv); - shape->stroke.gradient = nsvg__createGradient(p, attr->strokeGradient, localBounds, &shape->stroke.type); - if (shape->stroke.gradient == NULL) - shape->stroke.type = NSVG_PAINT_NONE; + shape->stroke.type = NSVG_PAINT_UNDEF; } // Set flags @@ -1042,6 +1040,10 @@ static void nsvg__addPath(NSVGparser* p, char closed) if (closed) nsvg__lineTo(p, p->pts[0], p->pts[1]); + // Expect 1 + N*3 points (N = number of cubic bezier segments). + if ((p->npts % 3) != 1) + return; + path = (NSVGpath*)malloc(sizeof(NSVGpath)); if (path == NULL) goto error; memset(path, 0, sizeof(NSVGpath)); @@ -1090,7 +1092,7 @@ static double nsvg__atof(const char* s) char* cur = (char*)s; char* end = NULL; double res = 0.0, sign = 1.0; - long long intPart = 0, fracPart = 0; + double intPart = 0.0, fracPart = 0.0; char hasIntPart = 0, hasFracPart = 0; // Parse optional sign @@ -1104,9 +1106,13 @@ static double nsvg__atof(const char* s) // Parse integer part if (nsvg__isdigit(*cur)) { // Parse digit sequence +#ifdef _MSC_VER + intPart = (double)_strtoi64(cur, &end, 10); +#else intPart = (double)strtoll(cur, &end, 10); +#endif if (cur != end) { - res = (double)intPart; + res = intPart; hasIntPart = 1; cur = end; } @@ -1117,9 +1123,13 @@ static double nsvg__atof(const char* s) cur++; // Skip '.' if (nsvg__isdigit(*cur)) { // Parse digit sequence - fracPart = strtoll(cur, &end, 10); +#ifdef _MSC_VER + fracPart = (double)_strtoi64(cur, &end, 10); +#else + fracPart = (double)strtoll(cur, &end, 10); +#endif if (cur != end) { - res += (double)fracPart / pow(10.0, (double)(end - cur)); + res += fracPart / pow(10.0, (double)(end - cur)); hasFracPart = 1; cur = end; } @@ -1132,11 +1142,11 @@ static double nsvg__atof(const char* s) // Parse optional exponent if (*cur == 'e' || *cur == 'E') { - int expPart = 0; + double expPart = 0.0; cur++; // skip 'E' - expPart = strtol(cur, &end, 10); // Parse digit sequence with sign + expPart = (double)strtol(cur, &end, 10); // Parse digit sequence with sign if (cur != end) { - res *= pow(10.0, (double)expPart); + res *= pow(10.0, expPart); } } @@ -1170,7 +1180,7 @@ static const char* nsvg__parseNumber(const char* s, char* it, const int size) } } // exponent - if (*s == 'e' || *s == 'E') { + if ((*s == 'e' || *s == 'E') && (s[1] != 'm' && s[1] != 'x')) { if (i < last) it[i++] = *s; s++; if (*s == '-' || *s == '+') { @@ -1187,6 +1197,19 @@ static const char* nsvg__parseNumber(const char* s, char* it, const int size) return s; } +static const char* nsvg__getNextPathItemWhenArcFlag(const char* s, char* it) +{ + it[0] = '\0'; + while (*s && (nsvg__isspace(*s) || *s == ',')) s++; + if (!*s) return s; + if (*s == '0' || *s == '1') { + it[0] = *s++; + it[1] = '\0'; + return s; + } + return s; +} + static const char* nsvg__getNextPathItem(const char* s, char* it) { it[0] = '\0'; @@ -1207,35 +1230,66 @@ static const char* nsvg__getNextPathItem(const char* s, char* it) static unsigned int nsvg__parseColorHex(const char* str) { - unsigned int c = 0, r = 0, g = 0, b = 0; - int n = 0; - str++; // skip # - // Calculate number of characters. - while(str[n] && !nsvg__isspace(str[n])) - n++; - if (n == 6) { - sscanf(str, "%x", &c); - } else if (n == 3) { - sscanf(str, "%x", &c); - c = (c&0xf) | ((c&0xf0) << 4) | ((c&0xf00) << 8); - c |= c<<4; - } - r = (c >> 16) & 0xff; - g = (c >> 8) & 0xff; - b = c & 0xff; - return NSVG_RGB(r,g,b); + unsigned int r=0, g=0, b=0; + if (sscanf(str, "#%2x%2x%2x", &r, &g, &b) == 3 ) // 2 digit hex + return NSVG_RGB(r, g, b); + if (sscanf(str, "#%1x%1x%1x", &r, &g, &b) == 3 ) // 1 digit hex, e.g. #abc -> 0xccbbaa + return NSVG_RGB(r*17, g*17, b*17); // same effect as (r<<4|r), (g<<4|g), .. + return NSVG_RGB(128, 128, 128); } +// Parse rgb color. The pointer 'str' must point at "rgb(" (4+ characters). +// This function returns gray (rgb(128, 128, 128) == '#808080') on parse errors +// for backwards compatibility. Note: other image viewers return black instead. + static unsigned int nsvg__parseColorRGB(const char* str) { - int r = -1, g = -1, b = -1; - char s1[32]="", s2[32]=""; - sscanf(str + 4, "%d%[%%, \t]%d%[%%, \t]%d", &r, s1, &g, s2, &b); - if (strchr(s1, '%')) { - return NSVG_RGB((r*255)/100,(g*255)/100,(b*255)/100); - } else { - return NSVG_RGB(r,g,b); + int i; + unsigned int rgbi[3]; + float rgbf[3]; + // try decimal integers first + if (sscanf(str, "rgb(%u, %u, %u)", &rgbi[0], &rgbi[1], &rgbi[2]) != 3) { + // integers failed, try percent values (float, locale independent) + const char delimiter[3] = {',', ',', ')'}; + str += 4; // skip "rgb(" + for (i = 0; i < 3; i++) { + while (*str && (nsvg__isspace(*str))) str++; // skip leading spaces + if (*str == '+') str++; // skip '+' (don't allow '-') + if (!*str) break; + rgbf[i] = nsvg__atof(str); + + // Note 1: it would be great if nsvg__atof() returned how many + // bytes it consumed but it doesn't. We need to skip the number, + // the '%' character, spaces, and the delimiter ',' or ')'. + + // Note 2: The following code does not allow values like "33.%", + // i.e. a decimal point w/o fractional part, but this is consistent + // with other image viewers, e.g. firefox, chrome, eog, gimp. + + while (*str && nsvg__isdigit(*str)) str++; // skip integer part + if (*str == '.') { + str++; + if (!nsvg__isdigit(*str)) break; // error: no digit after '.' + while (*str && nsvg__isdigit(*str)) str++; // skip fractional part + } + if (*str == '%') str++; else break; + while (nsvg__isspace(*str)) str++; + if (*str == delimiter[i]) str++; + else break; + } + if (i == 3) { + rgbi[0] = roundf(rgbf[0] * 2.55f); + rgbi[1] = roundf(rgbf[1] * 2.55f); + rgbi[2] = roundf(rgbf[2] * 2.55f); + } else { + rgbi[0] = rgbi[1] = rgbi[2] = 128; + } } + // clip values as the CSS spec requires + for (i = 0; i < 3; i++) { + if (rgbi[i] > 255) rgbi[i] = 255; + } + return NSVG_RGB(rgbi[0], rgbi[1], rgbi[2]); } typedef struct NSVGNamedColor { @@ -1460,6 +1514,15 @@ static int nsvg__parseUnits(const char* units) return NSVG_UNITS_USER; } +static int nsvg__isCoordinate(const char* s) +{ + // optional sign + if (*s == '-' || *s == '+') + s++; + // must have at least one digit, or start by a dot + return (nsvg__isdigit(*s) || *s == '.'); +} + static NSVGcoordinate nsvg__parseCoordinateRaw(const char* str) { NSVGcoordinate coord = {0, NSVG_UNITS_USER}; @@ -1599,25 +1662,32 @@ static int nsvg__parseRotate(float* xform, const char* str) static void nsvg__parseTransform(float* xform, const char* str) { float t[6]; + int len; nsvg__xformIdentity(xform); while (*str) { if (strncmp(str, "matrix", 6) == 0) - str += nsvg__parseMatrix(t, str); + len = nsvg__parseMatrix(t, str); else if (strncmp(str, "translate", 9) == 0) - str += nsvg__parseTranslate(t, str); + len = nsvg__parseTranslate(t, str); else if (strncmp(str, "scale", 5) == 0) - str += nsvg__parseScale(t, str); + len = nsvg__parseScale(t, str); else if (strncmp(str, "rotate", 6) == 0) - str += nsvg__parseRotate(t, str); + len = nsvg__parseRotate(t, str); else if (strncmp(str, "skewX", 5) == 0) - str += nsvg__parseSkewX(t, str); + len = nsvg__parseSkewX(t, str); else if (strncmp(str, "skewY", 5) == 0) - str += nsvg__parseSkewY(t, str); + len = nsvg__parseSkewY(t, str); else{ ++str; continue; } + if (len != 0) { + str += len; + } else { + ++str; + continue; + } nsvg__xformPremultiply(xform, t); } @@ -1627,9 +1697,9 @@ static void nsvg__parseUrl(char* id, const char* str) { int i = 0; str += 4; // "url("; - if (*str == '#') + if (*str && *str == '#') str++; - while (i < 63 && *str != ')') { + while (i < 63 && *str && *str != ')') { id[i] = *str++; i++; } @@ -1878,8 +1948,11 @@ static int nsvg__getArgsPerElement(char cmd) case 'a': case 'A': return 7; + case 'z': + case 'Z': + return 0; } - return 0; + return -1; } static void nsvg__pathMoveTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) @@ -2160,7 +2233,12 @@ static void nsvg__pathArcTo(NSVGparser* p, float* cpx, float* cpy, float* args, // The loop assumes an iteration per end point (including start and end), this +1. ndivs = (int)(fabsf(da) / (NSVG_PI*0.5f) + 1.0f); hda = (da / (float)ndivs) / 2.0f; - kappa = fabsf(4.0f / 3.0f * (1.0f - cosf(hda)) / sinf(hda)); + // Fix for ticket #179: division by 0: avoid cotangens around 0 (infinite) + if ((hda < 1e-3f) && (hda > -1e-3f)) + hda *= 0.5f; + else + hda = (1.0f - cosf(hda)) / sinf(hda); + kappa = fabsf(4.0f / 3.0f * hda); if (da < 0.0f) kappa = -kappa; @@ -2189,6 +2267,7 @@ static void nsvg__parsePath(NSVGparser* p, const char** attr) float args[10]; int nargs; int rargs = 0; + char initPoint; float cpx, cpy, cpx2, cpy2; const char* tmp[4]; char closedFlag; @@ -2211,13 +2290,18 @@ static void nsvg__parsePath(NSVGparser* p, const char** attr) nsvg__resetPath(p); cpx = 0; cpy = 0; cpx2 = 0; cpy2 = 0; + initPoint = 0; closedFlag = 0; nargs = 0; while (*s) { - s = nsvg__getNextPathItem(s, item); + item[0] = '\0'; + if ((cmd == 'A' || cmd == 'a') && (nargs == 3 || nargs == 4)) + s = nsvg__getNextPathItemWhenArcFlag(s, item); + if (!*item) + s = nsvg__getNextPathItem(s, item); if (!*item) break; - if (nsvg__isnum(item[0])) { + if (cmd != '\0' && nsvg__isCoordinate(item)) { if (nargs < 10) args[nargs++] = (float)nsvg__atof(item); if (nargs >= rargs) { @@ -2230,6 +2314,7 @@ static void nsvg__parsePath(NSVGparser* p, const char** attr) cmd = (cmd == 'm') ? 'l' : 'L'; rargs = nsvg__getArgsPerElement(cmd); cpx2 = cpx; cpy2 = cpy; + initPoint = 1; break; case 'l': case 'L': @@ -2279,7 +2364,6 @@ static void nsvg__parsePath(NSVGparser* p, const char** attr) } } else { cmd = item[0]; - rargs = nsvg__getArgsPerElement(cmd); if (cmd == 'M' || cmd == 'm') { // Commit path. if (p->npts > 0) @@ -2288,7 +2372,11 @@ static void nsvg__parsePath(NSVGparser* p, const char** attr) nsvg__resetPath(p); closedFlag = 0; nargs = 0; - } else if (cmd == 'Z' || cmd == 'z') { + } else if (initPoint == 0) { + // Do not allow other commands until initial point has been set (moveTo called once). + cmd = '\0'; + } + if (cmd == 'Z' || cmd == 'z') { closedFlag = 1; // Commit path. if (p->npts > 0) { @@ -2304,6 +2392,12 @@ static void nsvg__parsePath(NSVGparser* p, const char** attr) closedFlag = 0; nargs = 0; } + rargs = nsvg__getArgsPerElement(cmd); + if (rargs == -1) { + // Command not recognized + cmd = '\0'; + rargs = 0; + } } } // Commit path. @@ -2550,7 +2644,7 @@ static void nsvg__parseSVG(NSVGparser* p, const char** attr) } } -static void nsvg__parseGradient(NSVGparser* p, const char** attr, char type) +static void nsvg__parseGradient(NSVGparser* p, const char** attr, signed char type) { int i; NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); @@ -2875,6 +2969,36 @@ static void nsvg__scaleToViewbox(NSVGparser* p, const char* units) } } +static void nsvg__createGradients(NSVGparser* p) +{ + NSVGshape* shape; + + for (shape = p->image->shapes; shape != NULL; shape = shape->next) { + if (shape->fill.type == NSVG_PAINT_UNDEF) { + if (shape->fillGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->fill.gradient = nsvg__createGradient(p, shape->fillGradient, localBounds, shape->xform, &shape->fill.type); + } + if (shape->fill.type == NSVG_PAINT_UNDEF) { + shape->fill.type = NSVG_PAINT_NONE; + } + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + if (shape->strokeGradient[0] != '\0') { + float inv[6], localBounds[4]; + nsvg__xformInverse(inv, shape->xform); + nsvg__getLocalBounds(localBounds, shape, inv); + shape->stroke.gradient = nsvg__createGradient(p, shape->strokeGradient, localBounds, shape->xform, &shape->stroke.type); + } + if (shape->stroke.type == NSVG_PAINT_UNDEF) { + shape->stroke.type = NSVG_PAINT_NONE; + } + } + } +} + NSVGimage* nsvgParse(char* input, const char* units, float dpi) { NSVGparser* p; @@ -2888,6 +3012,9 @@ NSVGimage* nsvgParse(char* input, const char* units, float dpi) nsvg__parseXML(input, nsvg__startElement, nsvg__endElement, nsvg__content, p); + // Create gradients after all definitions have been parsed + nsvg__createGradients(p); + // Scale to viewBox nsvg__scaleToViewbox(p, units); @@ -2899,7 +3026,10 @@ NSVGimage* nsvgParse(char* input, const char* units, float dpi) return ret; } -#include +#if WIN32 +#define WIN32_LEAN_AND_MEAN +#include "windows.h" +#endif NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) { @@ -2908,8 +3038,17 @@ NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) char* data = NULL; NSVGimage* image = NULL; - fp = boost::nowide::fopen(filename, "rb"); - if (!fp) goto error; +#if WIN32 + int name_len = MultiByteToWideChar(CP_UTF8, NULL, filename, strlen(filename), NULL, 0); + wchar_t w_fname[512]; + memset(w_fname, 0, sizeof(w_fname)); + MultiByteToWideChar(CP_UTF8, NULL, filename, strlen(filename), w_fname, name_len); + w_fname[name_len] = '\0'; + fp = _wfopen(w_fname, L"rb"); +#else + fp = fopen(filename, "rb"); +#endif + if (!fp) goto error; fseek(fp, 0, SEEK_END); size = ftell(fp); fseek(fp, 0, SEEK_SET); @@ -2918,9 +3057,9 @@ NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) if (fread(data, 1, size, fp) != size) goto error; data[size] = '\0'; // Must be null terminated. fclose(fp); - image = nsvgParse(data, units, dpi); free(data); + return image; error: @@ -2976,4 +3115,6 @@ void nsvgDelete(NSVGimage* image) free(image); } -#endif +#endif // NANOSVG_IMPLEMENTATION + +#endif // NANOSVG_H diff --git a/src/nanosvg/nanosvgrast.h b/src/nanosvg/nanosvgrast.h index b740c316c..a4b866beb 100644 --- a/src/nanosvg/nanosvgrast.h +++ b/src/nanosvg/nanosvgrast.h @@ -22,9 +22,17 @@ * */ +/* Modified by FLTK to support non-square X,Y axes scaling. + * + * Added: nsvgRasterizeXY() +*/ + + #ifndef NANOSVGRAST_H #define NANOSVGRAST_H +#include "nanosvg.h" + #ifndef NANOSVGRAST_CPLUSPLUS #ifdef __cplusplus extern "C" { @@ -44,16 +52,19 @@ typedef struct NSVGrasterizer NSVGrasterizer; unsigned char* img = malloc(w*h*4); // Rasterize nsvgRasterize(rast, image, 0,0,1, img, w, h, w*4); + + // For non-square X,Y scaling, use + nsvgRasterizeXY(rast, image, 0,0,1,1, img, w, h, w*4); */ // Allocated rasterizer context. -NSVGrasterizer* nsvgCreateRasterizer(); +NSVGrasterizer* nsvgCreateRasterizer(void); // Rasterizes SVG image, returns RGBA image (non-premultiplied alpha) // r - pointer to rasterizer context // image - pointer to image to rasterize // tx,ty - image offset (applied after scaling) -// scale - image scale +// scale - image scale (assumes square aspect ratio) // dst - pointer to destination image data, 4 bytes per pixel (RGBA) // w - width of the image to render // h - height of the image to render @@ -62,6 +73,12 @@ void nsvgRasterize(NSVGrasterizer* r, NSVGimage* image, float tx, float ty, float scale, unsigned char* dst, int w, int h, int stride); +// As above, but allow X and Y axes to scale independently for non-square aspects +void nsvgRasterizeXY(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, + float sx, float sy, + unsigned char* dst, int w, int h, int stride); + // Deletes rasterizer context. void nsvgDeleteRasterizer(NSVGrasterizer*); @@ -72,11 +89,11 @@ void nsvgDeleteRasterizer(NSVGrasterizer*); #endif #endif -#endif // NANOSVGRAST_H - #ifdef NANOSVGRAST_IMPLEMENTATION #include +#include +#include #define NSVG__SUBSAMPLES 5 #define NSVG__FIXSHIFT 10 @@ -112,7 +129,7 @@ typedef struct NSVGmemPage { } NSVGmemPage; typedef struct NSVGcachedPaint { - char type; + signed char type; char spread; float xform[6]; unsigned int colors[256]; @@ -148,7 +165,7 @@ struct NSVGrasterizer int width, height, stride; }; -NSVGrasterizer* nsvgCreateRasterizer() +NSVGrasterizer* nsvgCreateRasterizer(void) { NSVGrasterizer* r = (NSVGrasterizer*)malloc(sizeof(NSVGrasterizer)); if (r == NULL) goto error; @@ -329,6 +346,7 @@ static float nsvg__normalize(float *x, float* y) } static float nsvg__absf(float x) { return x < 0 ? -x : x; } +static float nsvg__roundf(float x) { return (x >= 0) ? floorf(x + 0.5) : ceilf(x - 0.5); } static void nsvg__flattenCubicBez(NSVGrasterizer* r, float x1, float y1, float x2, float y2, @@ -368,7 +386,7 @@ static void nsvg__flattenCubicBez(NSVGrasterizer* r, nsvg__flattenCubicBez(r, x1234,y1234, x234,y234, x34,y34, x4,y4, level+1, type); } -static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float scale) +static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float sx, float sy) { int i, j; NSVGpath* path; @@ -376,13 +394,13 @@ static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float scale) for (path = shape->paths; path != NULL; path = path->next) { r->npoints = 0; // Flatten path - nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); + nsvg__addPathPoint(r, path->pts[0]*sx, path->pts[1]*sy, 0); for (i = 0; i < path->npts-1; i += 3) { float* p = &path->pts[i*2]; - nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, 0); + nsvg__flattenCubicBez(r, p[0]*sx,p[1]*sy, p[2]*sx,p[3]*sy, p[4]*sx,p[5]*sy, p[6]*sx,p[7]*sy, 0, 0); } // Close path - nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); + nsvg__addPathPoint(r, path->pts[0]*sx, path->pts[1]*sy, 0); // Build edges for (i = 0, j = r->npoints-1; i < r->npoints; j = i++) nsvg__addEdge(r, r->points[j].x, r->points[j].y, r->points[i].x, r->points[i].y); @@ -732,7 +750,7 @@ static void nsvg__prepareStroke(NSVGrasterizer* r, float miterLimit, int lineJoi } } -static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float scale) +static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float sx, float sy) { int i, j, closed; NSVGpath* path; @@ -740,15 +758,16 @@ static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float float miterLimit = shape->miterLimit; int lineJoin = shape->strokeLineJoin; int lineCap = shape->strokeLineCap; - float lineWidth = shape->strokeWidth * scale; + const float sw = (sx + sy) / 2; // average scaling factor + const float lineWidth = shape->strokeWidth * sw; // FIXME (?) for (path = shape->paths; path != NULL; path = path->next) { // Flatten path r->npoints = 0; - nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, NSVG_PT_CORNER); + nsvg__addPathPoint(r, path->pts[0]*sx, path->pts[1]*sy, NSVG_PT_CORNER); for (i = 0; i < path->npts-1; i += 3) { float* p = &path->pts[i*2]; - nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, NSVG_PT_CORNER); + nsvg__flattenCubicBez(r, p[0]*sx,p[1]*sy, p[2]*sx,p[3]*sy, p[4]*sx,p[5]*sy, p[6]*sx,p[7]*sy, 0, NSVG_PT_CORNER); } if (r->npoints < 2) continue; @@ -794,7 +813,7 @@ static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float dashOffset -= shape->strokeDashArray[idash]; idash = (idash + 1) % shape->strokeDashCount; } - dashLen = (shape->strokeDashArray[idash] - dashOffset) * scale; + dashLen = (shape->strokeDashArray[idash] - dashOffset) * sw; for (j = 1; j < r->npoints2; ) { float dx = r->points2[j].x - cur.x; @@ -816,7 +835,7 @@ static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float // Advance dash pattern dashState = !dashState; idash = (idash+1) % shape->strokeDashCount; - dashLen = shape->strokeDashArray[idash] * scale; + dashLen = shape->strokeDashArray[idash] * sw; // Restart cur.x = x; cur.y = y; @@ -870,10 +889,10 @@ static NSVGactiveEdge* nsvg__addActive(NSVGrasterizer* r, NSVGedge* e, float sta // STBTT_assert(e->y0 <= start_point); // round dx down to avoid going too far if (dxdy < 0) - z->dx = (int)(-floorf(NSVG__FIX * -dxdy)); + z->dx = (int)(-nsvg__roundf(NSVG__FIX * -dxdy)); else - z->dx = (int)floorf(NSVG__FIX * dxdy); - z->x = (int)floorf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); + z->dx = (int)nsvg__roundf(NSVG__FIX * dxdy); + z->x = (int)nsvg__roundf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); // z->x -= off_x * FIX; z->ey = e->y1; z->next = 0; @@ -956,7 +975,7 @@ static float nsvg__clampf(float a, float mn, float mx) { return a < mn ? mn : (a static unsigned int nsvg__RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { - return (r) | (g << 8) | (b << 16) | (a << 24); + return ((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16) | ((unsigned int)a << 24); } static unsigned int nsvg__lerpRGBA(unsigned int c0, unsigned int c1, float u) @@ -985,7 +1004,7 @@ static inline int nsvg__div255(int x) } static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* cover, int x, int y, - float tx, float ty, float scale, NSVGcachedPaint* cache) + float tx, float ty, float sx, float sy, NSVGcachedPaint* cache) { if (cache->type == NSVG_PAINT_COLOR) { @@ -1026,9 +1045,9 @@ static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* co int i, cr, cg, cb, ca; unsigned int c; - fx = ((float)x - tx) / scale; - fy = ((float)y - ty) / scale; - dx = 1.0f / scale; + fx = ((float)x - tx) / sx; + fy = ((float)y - ty) / sy; + dx = 1.0f / sx; for (i = 0; i < count; i++) { int r,g,b,a,ia; @@ -1071,9 +1090,9 @@ static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* co int i, cr, cg, cb, ca; unsigned int c; - fx = ((float)x - tx) / scale; - fy = ((float)y - ty) / scale; - dx = 1.0f / scale; + fx = ((float)x - tx) / sx; + fy = ((float)y - ty) / sy; + dx = 1.0f / sx; for (i = 0; i < count; i++) { int r,g,b,a,ia; @@ -1112,7 +1131,7 @@ static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* co } } -static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, float scale, NSVGcachedPaint* cache, char fillRule) +static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, float sx, float sy, NSVGcachedPaint* cache, char fillRule) { NSVGactiveEdge *active = NULL; int y, s; @@ -1194,7 +1213,7 @@ static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, fl if (xmin < 0) xmin = 0; if (xmax > r->width-1) xmax = r->width-1; if (xmin <= xmax) { - nsvg__scanlineSolid(&r->bitmap[y * r->stride] + xmin*4, xmax-xmin+1, &r->scanline[xmin], xmin, y, tx,ty, scale, cache); + nsvg__scanlineSolid(&r->bitmap[y * r->stride] + xmin*4, xmax-xmin+1, &r->scanline[xmin], xmin, y, tx,ty, sx, sy, cache); } } @@ -1280,7 +1299,7 @@ static void nsvg__initPaint(NSVGcachedPaint* cache, NSVGpaint* paint, float opac if (grad->nstops == 0) { for (i = 0; i < 256; i++) cache->colors[i] = 0; - } if (grad->nstops == 1) { + } else if (grad->nstops == 1) { for (i = 0; i < 256; i++) cache->colors[i] = nsvg__applyOpacity(grad->stops[i].color, opacity); } else { @@ -1362,8 +1381,9 @@ static void dumpEdges(NSVGrasterizer* r, const char* name) } */ -void nsvgRasterize(NSVGrasterizer* r, - NSVGimage* image, float tx, float ty, float scale, +void nsvgRasterizeXY(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, + float sx, float sy, unsigned char* dst, int w, int h, int stride) { NSVGshape *shape = NULL; @@ -1394,7 +1414,7 @@ void nsvgRasterize(NSVGrasterizer* r, r->freelist = NULL; r->nedges = 0; - nsvg__flattenShape(r, shape, scale); + nsvg__flattenShape(r, shape, sx, sy); // Scale and translate edges for (i = 0; i < r->nedges; i++) { @@ -1406,19 +1426,20 @@ void nsvgRasterize(NSVGrasterizer* r, } // Rasterize edges - qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule nsvg__initPaint(&cache, &shape->fill, shape->opacity); - nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, shape->fillRule); + nsvg__rasterizeSortedEdges(r, tx,ty, sx, sy, &cache, shape->fillRule); } - if (shape->stroke.type != NSVG_PAINT_NONE && (shape->strokeWidth * scale) > 0.01f) { + if (shape->stroke.type != NSVG_PAINT_NONE && (shape->strokeWidth * sx) > 0.01f) { nsvg__resetPool(r); r->freelist = NULL; r->nedges = 0; - nsvg__flattenShapeStroke(r, shape, scale); + nsvg__flattenShapeStroke(r, shape, sx, sy); // dumpEdges(r, "edge.svg"); @@ -1432,12 +1453,13 @@ void nsvgRasterize(NSVGrasterizer* r, } // Rasterize edges - qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); + if (r->nedges != 0) + qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule nsvg__initPaint(&cache, &shape->stroke, shape->opacity); - nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, NSVG_FILLRULE_NONZERO); + nsvg__rasterizeSortedEdges(r, tx,ty,sx, sy, &cache, NSVG_FILLRULE_NONZERO); } } @@ -1449,4 +1471,13 @@ void nsvgRasterize(NSVGrasterizer* r, r->stride = 0; } -#endif +void nsvgRasterize(NSVGrasterizer* r, + NSVGimage* image, float tx, float ty, float scale, + unsigned char* dst, int w, int h, int stride) +{ + nsvgRasterizeXY(r,image, tx, ty, scale, scale, dst, w, h, stride); +} + +#endif // NANOSVGRAST_IMPLEMENTATION + +#endif // NANOSVGRAST_H diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 79f9a974d..9b57201c8 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -147,6 +147,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoSeam.hpp GUI/Gizmos/GLGizmoText.cpp GUI/Gizmos/GLGizmoText.hpp + GUI/Gizmos/GLGizmoSVG.cpp + GUI/Gizmos/GLGizmoSVG.hpp GUI/Gizmos/GLGizmoMeshBoolean.cpp GUI/Gizmos/GLGizmoMeshBoolean.hpp GUI/GLSelectionRectangle.cpp @@ -187,6 +189,8 @@ set(SLIC3R_GUI_SOURCES GUI/GUI_Utils.hpp GUI/I18N.cpp GUI/I18N.hpp + GUI/IconManager.cpp + GUI/IconManager.hpp GUI/MainFrame.cpp GUI/MainFrame.hpp GUI/BBLTopbar.cpp @@ -299,6 +303,8 @@ set(SLIC3R_GUI_SOURCES GUI/SendSystemInfoDialog.hpp GUI/StepMeshDialog.cpp GUI/StepMeshDialog.hpp + GUI/SurfaceDrag.cpp + GUI/SurfaceDrag.hpp GUI/PlateSettingsDialog.cpp GUI/PlateSettingsDialog.hpp GUI/ImGuiWrapper.hpp @@ -328,6 +334,15 @@ set(SLIC3R_GUI_SOURCES GUI/UpdateDialogs.hpp GUI/Jobs/Job.hpp GUI/Jobs/Job.cpp + GUI/Jobs/JobNew.hpp + GUI/Jobs/Worker.hpp + GUI/Jobs/BoostThreadWorker.hpp + GUI/Jobs/BoostThreadWorker.cpp + GUI/Jobs/BusyCursorJob.hpp + GUI/Jobs/ThreadSafeQueue.hpp + GUI/Jobs/PlaterWorker.hpp + GUI/Jobs/EmbossJob.cpp + GUI/Jobs/EmbossJob.hpp GUI/Jobs/PlaterJob.hpp GUI/Jobs/PlaterJob.cpp GUI/Jobs/UpgradeNetworkJob.hpp @@ -472,6 +487,8 @@ set(SLIC3R_GUI_SOURCES Utils/PresetUpdater.hpp Utils/Process.cpp Utils/Process.hpp + Utils/RaycastManager.cpp + Utils/RaycastManager.hpp Utils/Profile.hpp Utils/UndoRedo.cpp Utils/UndoRedo.hpp diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 7c524ce32..aef3cb4dc 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4542,6 +4542,38 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) else evt.Skip(); + // Detection of doubleclick on text to open emboss edit window + auto type = m_gizmos.get_current_type(); + if (evt.LeftDClick() && !m_hover_volume_idxs.empty() && + (type == GLGizmosManager::EType::Undefined //||type == GLGizmosManager::EType::Text || + //type == GLGizmosManager::EType::Svg + )) { + for (int hover_volume_id : m_hover_volume_idxs) { + const GLVolume &hover_gl_volume = *m_volumes.volumes[hover_volume_id]; + int object_idx = hover_gl_volume.object_idx(); + if (object_idx < 0 || static_cast(object_idx) >= m_model->objects.size()) + continue; + const ModelObject *hover_object = m_model->objects[object_idx]; + int hover_volume_idx = hover_gl_volume.volume_idx(); + if (hover_volume_idx < 0 || static_cast(hover_volume_idx) >= hover_object->volumes.size()) + continue; + const ModelVolume *hover_volume = hover_object->volumes[hover_volume_idx]; + + /* if (hover_volume->text_configuration.has_value()) { + m_selection.add_volumes(Selection::EMode::Volume, {(unsigned) hover_volume_id}); + if (type != GLGizmosManager::EType::Emboss) m_gizmos.open_gizmo(GLGizmosManager::EType::Emboss); + wxGetApp().obj_list()->update_selections(); + return; + } else*/ if (hover_volume->emboss_shape.has_value()) { + m_selection.add_volumes(Selection::EMode::Volume, {(unsigned) hover_volume_id}); + if (type != GLGizmosManager::EType::Svg) + m_gizmos.open_gizmo(GLGizmosManager::EType::Svg); + wxGetApp().obj_list()->update_selections(); + return; + } + } + } + if (m_moving) show_sinking_contours(); @@ -4658,7 +4690,7 @@ void GLCanvas3D::do_move(const std::string &snapshot_type) else if (selection_mode == Selection::Volume) { auto cur_mv = model_object->volumes[volume_idx]; if (cur_mv->get_offset() != v->get_volume_offset()) { - cur_mv->set_offset(v->get_volume_offset()); + cur_mv->set_transformation(v->get_volume_transformation()); // BBS: backup Slic3r::save_object_mesh(*model_object); } @@ -9836,5 +9868,57 @@ ModelVolume *get_model_volume(const GLVolume &v, const ModelObjectPtrs &objects) return get_model_volume(v, *objects[objext_idx]); } +GLVolume *get_first_hovered_gl_volume(const GLCanvas3D &canvas) +{ + int hovered_id_signed = canvas.get_first_hover_volume_idx(); + if (hovered_id_signed < 0) + return nullptr; + + size_t hovered_id = static_cast(hovered_id_signed); + const GLVolumePtrs &volumes = canvas.get_volumes().volumes; + if (hovered_id >= volumes.size()) + return nullptr; + + return volumes[hovered_id]; +} + +GLVolume *get_selected_gl_volume(const GLCanvas3D &canvas) +{ + const GLVolume *gl_volume = get_selected_gl_volume(canvas.get_selection()); + if (gl_volume == nullptr) return nullptr; + + const GLVolumePtrs &gl_volumes = canvas.get_volumes().volumes; + for (GLVolume *v : gl_volumes) + if (v->composite_id == gl_volume->composite_id) return v; + return nullptr; +} + +ModelObject *get_model_object(const GLVolume &gl_volume, const Model &model) { return get_model_object(gl_volume, model.objects); } + +ModelObject *get_model_object(const GLVolume &gl_volume, const ModelObjectPtrs &objects) +{ + if (gl_volume.object_idx() < 0) return nullptr; + size_t objext_idx = static_cast(gl_volume.object_idx()); + if (objext_idx >= objects.size()) return nullptr; + return objects[objext_idx]; +} + +ModelInstance *get_model_instance(const GLVolume &gl_volume, const Model &model) { return get_model_instance(gl_volume, model.objects); } + +ModelInstance *get_model_instance(const GLVolume &gl_volume, const ModelObjectPtrs &objects) +{ + if (gl_volume.instance_idx() < 0) return nullptr; + ModelObject *object = get_model_object(gl_volume, objects); + return get_model_instance(gl_volume, *object); +} + +ModelInstance *get_model_instance(const GLVolume &gl_volume, const ModelObject &object) +{ + if (gl_volume.instance_idx() < 0) return nullptr; + size_t instance_idx = static_cast(gl_volume.instance_idx()); + if (instance_idx >= object.instances.size()) return nullptr; + return object.instances[instance_idx]; +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 93ba2424f..2c5fab31d 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -1228,6 +1228,16 @@ const ModelVolume *get_model_volume(const GLVolume &v, const Model &model); ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects); ModelVolume *get_model_volume(const GLVolume &v, const ModelObjectPtrs &objects); ModelVolume *get_model_volume(const GLVolume &v, const ModelObject &object); + +GLVolume *get_first_hovered_gl_volume(const GLCanvas3D &canvas); +GLVolume *get_selected_gl_volume(const GLCanvas3D &canvas); + +ModelObject *get_model_object(const GLVolume &gl_volume, const Model &model); +ModelObject *get_model_object(const GLVolume &gl_volume, const ModelObjectPtrs &objects); + +ModelInstance *get_model_instance(const GLVolume &gl_volume, const Model &model); +ModelInstance *get_model_instance(const GLVolume &gl_volume, const ModelObjectPtrs &objects); +ModelInstance *get_model_instance(const GLVolume &gl_volume, const ModelObject &object); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 9db80b5d8..0c3161a29 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -277,24 +277,28 @@ wxBitmap SettingsFactory::get_category_bitmap(const std::string& category_name, //------------------------------------- // Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important -#ifdef __WINDOWS__ -const std::vector> MenuFactory::ADD_VOLUME_MENU_ITEMS = { + +static const std::vector> ADD_VOLUME_MENU_ITEMS = { {L("Add part"), "menu_add_part" }, // ~ModelVolumeType::MODEL_PART {L("Add negative part"), "menu_add_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME {L("Add modifier"), "menu_add_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER {L("Add support blocker"), "menu_support_blocker"}, // ~ModelVolumeType::SUPPORT_BLOCKER {L("Add support enforcer"), "menu_support_enforcer"} // ~ModelVolumeType::SUPPORT_ENFORCER }; -#else -const std::vector> MenuFactory::ADD_VOLUME_MENU_ITEMS = { - {L("Add part"), "menu_add_part" }, // ~ModelVolumeType::MODEL_PART - {L("Add negative part"), "menu_add_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME - {L("Add modifier"), "menu_add_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER - {L("Add support blocker"), "menu_support_blocker"}, // ~ModelVolumeType::SUPPORT_BLOCKER - {L("Add support enforcer"), "menu_support_enforcer"} // ~ModelVolumeType::SUPPORT_ENFORCER -}; -#endif +// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important +static const constexpr std::array, 3> TEXT_VOLUME_ICONS{{ + // menu_item Name menu_item bitmap name + {L("Add text"), "add_text_part"}, // ~ModelVolumeType::MODEL_PART + {L("Add negative text"), "add_text_negative"}, // ~ModelVolumeType::NEGATIVE_VOLUME + {L("Add text modifier"), "add_text_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER +}}; +// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important +static const constexpr std::array, 3> SVG_VOLUME_ICONS{{ + {L("Add SVG part"), "svg_part"}, // ~ModelVolumeType::MODEL_PART + {L("Add negative SVG"), "svg_negative"}, // ~ModelVolumeType::NEGATIVE_VOLUME + {L("Add SVG modifier"), "svg_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER +}}; static Plater* plater() { @@ -440,6 +444,22 @@ std::vector MenuFactory::get_volume_bitmaps() return volume_bmps; } +std::vector MenuFactory::get_text_volume_bitmaps() +{ + std::vector volume_bmps; + volume_bmps.reserve(TEXT_VOLUME_ICONS.size()); + for (const auto &item : TEXT_VOLUME_ICONS) volume_bmps.push_back(create_scaled_bitmap(item.second)); + return volume_bmps; +} + +std::vector MenuFactory::get_svg_volume_bitmaps() +{ + std::vector volume_bmps; + volume_bmps.reserve(SVG_VOLUME_ICONS.size()); + for (const auto &item : SVG_VOLUME_ICONS) volume_bmps.push_back(create_scaled_bitmap(item.second)); + return volume_bmps; +} + void MenuFactory::append_menu_item_set_visible(wxMenu* menu) { bool has_one_shown = false; @@ -479,6 +499,36 @@ void MenuFactory::append_menu_item_edit_text(wxMenu *menu) #endif } +void MenuFactory::append_menu_item_edit_svg(wxMenu *menu) +{ + wxString name = _L("Edit SVG"); + auto can_edit_svg = []() { + if (plater() == nullptr) return false; + const Selection &selection = plater()->get_selection(); + if (selection.volumes_count() != 1) return false; + const GLVolume *gl_volume = selection.get_first_volume(); + if (gl_volume == nullptr) return false; + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume == nullptr) return false; + return volume->is_svg(); + }; + + if (menu != &m_svg_part_menu) { + const int menu_item_id = menu->FindItem(name); + if (menu_item_id != wxNOT_FOUND) menu->Destroy(menu_item_id); + if (!can_edit_svg()) return; + } + + wxString description = _L("Change SVG source file, projection, size, ..."); + std::string icon = "svg_part"; + auto open_svg = [](const wxCommandEvent &) { + GLGizmosManager &mng = plater()->get_view3D_canvas3D()->get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Svg) mng.open_gizmo(GLGizmosManager::Svg); // close() and reopen - move to be visible + mng.open_gizmo(GLGizmosManager::Svg); + }; + append_menu_item(menu, wxID_ANY, name, description, open_svg, icon, nullptr, can_edit_svg, m_parent); +} + wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType type) { auto sub_menu = new wxMenu; @@ -1169,6 +1219,34 @@ void MenuFactory::create_part_menu() append_menu_item_per_object_settings(&m_part_menu); } +void MenuFactory::create_text_part_menu() +{ + wxMenu *menu = &m_text_part_menu; + + append_menu_item_edit_text(menu); + append_menu_item_delete(menu); + append_menu_item_fix_through_netfabb(menu); + append_menu_item_simplify(menu); + append_menu_items_mirror(menu); + menu->AppendSeparator(); + append_menu_item_per_object_settings(menu); + append_menu_item_change_type(menu); +} + +void MenuFactory::create_svg_part_menu() +{ + wxMenu *menu = &m_svg_part_menu; + + append_menu_item_edit_svg(menu); + append_menu_item_delete(menu); + append_menu_item_fix_through_netfabb(menu); + append_menu_item_simplify(menu); + append_menu_items_mirror(menu); + menu->AppendSeparator(); + append_menu_item_per_object_settings(menu); + append_menu_item_change_type(menu); +} + void MenuFactory::create_bbl_part_menu() { wxMenu* menu = &m_part_menu; @@ -1302,7 +1380,7 @@ void MenuFactory::init(wxWindow* parent) //create_object_menu(); create_sla_object_menu(); //create_part_menu(); - + create_svg_part_menu(); create_bbl_object_menu(); create_bbl_part_menu(); create_bbl_assemble_object_menu(); @@ -1335,6 +1413,7 @@ wxMenu* MenuFactory::object_menu() append_menu_items_convert_unit(&m_object_menu); append_menu_items_flush_options(&m_object_menu); append_menu_item_invalidate_cut_info(&m_object_menu); + append_menu_item_edit_svg(&m_object_menu); append_menu_item_change_filament(&m_object_menu); { NetworkAgent* agent = GUI::wxGetApp().getAgent(); @@ -1347,6 +1426,8 @@ wxMenu* MenuFactory::sla_object_menu() { append_menu_items_convert_unit(&m_sla_object_menu); append_menu_item_settings(&m_sla_object_menu); + //append_menu_item_edit_text(&m_sla_object_menu); + append_menu_item_edit_svg(&m_object_menu); //update_menu_items_instance_manipulation(mtObjectSLA); return &m_sla_object_menu; } @@ -1363,6 +1444,22 @@ wxMenu* MenuFactory::part_menu() return &m_part_menu; } +wxMenu *MenuFactory::text_part_menu() +{ + append_menu_item_change_filament(&m_text_part_menu); + append_menu_item_per_object_settings(&m_text_part_menu); + + return &m_text_part_menu; +} + +wxMenu *MenuFactory::svg_part_menu() +{ + append_menu_item_change_filament(&m_svg_part_menu); + append_menu_item_per_object_settings(&m_svg_part_menu); + + return &m_svg_part_menu; +} + wxMenu* MenuFactory::instance_menu() { return &m_instance_menu; diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index c6c9b025e..5a82f6f15 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -47,8 +47,9 @@ struct SettingsFactory class MenuFactory { public: - static const std::vector> ADD_VOLUME_MENU_ITEMS; static std::vector get_volume_bitmaps(); + static std::vector get_text_volume_bitmaps(); + static std::vector get_svg_volume_bitmaps(); MenuFactory(); ~MenuFactory() = default; @@ -66,6 +67,8 @@ public: wxMenu* object_menu(); wxMenu* sla_object_menu(); wxMenu* part_menu(); + wxMenu *text_part_menu(); + wxMenu *svg_part_menu(); wxMenu* instance_menu(); wxMenu* layer_menu(); wxMenu* multi_selection_menu(); @@ -86,6 +89,8 @@ private: MenuWithSeparators m_object_menu; MenuWithSeparators m_part_menu; + MenuWithSeparators m_text_part_menu; + MenuWithSeparators m_svg_part_menu; MenuWithSeparators m_sla_object_menu; MenuWithSeparators m_default_menu; MenuWithSeparators m_instance_menu; @@ -112,6 +117,8 @@ private: void create_object_menu(); void create_sla_object_menu(); void create_part_menu(); + void create_text_part_menu(); + void create_svg_part_menu(); //BBS: add part plate related logic void create_plate_menu(); //BBS: add bbl object menu @@ -136,7 +143,6 @@ private: void append_menu_item_change_extruder(wxMenu* menu); void append_menu_item_set_visible(wxMenu* menu); void append_menu_item_delete(wxMenu* menu); - void append_menu_item_edit_text(wxMenu *menu); void append_menu_item_scale_selection_to_fit_print_volume(wxMenu* menu); void append_menu_items_convert_unit(wxMenu* menu); // Add "Conver/Revert..." menu items (from/to inches/meters) after "Reload From Disk" void append_menu_items_flush_options(wxMenu* menu); @@ -145,6 +151,8 @@ private: void append_menu_item_merge_parts_to_single_part(wxMenu *menu); void append_menu_items_mirror(wxMenu *menu); void append_menu_item_invalidate_cut_info(wxMenu *menu); + void append_menu_item_edit_text(wxMenu *menu); + void append_menu_item_edit_svg(wxMenu *menu); //void append_menu_items_instance_manipulation(wxMenu *menu); //void update_menu_items_instance_manipulation(MenuType type); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 9ed79d6cc..1d64e6303 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1350,20 +1350,27 @@ void ObjectList::show_context_menu(const bool evt_context_menu) if (volume_type != ModelVolumeType::MODEL_PART) return; } - menu = plater->assemble_multi_selection_menu(); } else { - menu = type & itPlate ? plater->plate_menu() : - type & itInstance ? plater->instance_menu() : - type & itVolume ? plater->part_menu() : - printer_technology() == ptFFF ? plater->object_menu() : - plater->sla_object_menu(); - plater->SetPlateIndexByRightMenuInLeftUI(-1); - if (type & itPlate) { - int plate_idx = -1; - const ItemType type0 = m_objects_model->GetItemType(item, plate_idx); - if (plate_idx >= 0) { - plater->SetPlateIndexByRightMenuInLeftUI(plate_idx); + if (type & itVolume) { + int obj_idx, vol_idx; + get_selected_item_indexes(obj_idx, vol_idx, item); + if (obj_idx < 0 || vol_idx < 0) return; + const ModelVolume *volume = object(obj_idx)->volumes[vol_idx]; + + menu = volume->is_svg() ? plater->svg_part_menu() : // ORCA fixes missing "Edit SVG" item for Add/Negative/Modifier SVG objects in object list + plater->part_menu(); + } + else { + menu = type & itPlate ? plater->plate_menu() : + type & itInstance ? plater->instance_menu() : + printer_technology() == ptFFF ? plater->object_menu() : + plater->sla_object_menu(); + plater->SetPlateIndexByRightMenuInLeftUI(-1); + if (type & itPlate) { + int plate_idx = -1; + const ItemType type0 = m_objects_model->GetItemType(item, plate_idx); + if (plate_idx >= 0) { plater->SetPlateIndexByRightMenuInLeftUI(plate_idx); } } } } @@ -2701,6 +2708,7 @@ void ObjectList::split() for (const ModelVolume* volume : model_object->volumes) { const wxDataViewItem& vol_item = m_objects_model->AddVolumeChild(parent, from_u8(volume->name), volume->type(),// is_modifier() ? ModelVolumeType::PARAMETER_MODIFIER : ModelVolumeType::MODEL_PART, + volume->is_svg(), get_warning_icon_name(volume->mesh().stats()), volume->config.has("extruder") ? volume->config.extruder() : 0, false); @@ -3899,6 +3907,7 @@ wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, st object_item, from_u8(volume->name), volume->type(), + volume->is_svg(), get_warning_icon_name(volume->mesh().stats()), volume->config.has("extruder") ? volume->config.extruder() : 0, false); @@ -5204,8 +5213,17 @@ void ObjectList::change_part_type() } } - const wxString names[] = { _L("Part"), _L("Negative Part"), _L("Modifier"), _L("Support Blocker"), _L("Support Enforcer") }; - SingleChoiceDialog dlg(_L("Type:"), _L("Choose part type"), wxArrayString(5, names), int(type)); + // ORCA: Fix crash when changing type of svg / text modifier + wxArrayString names; + names.Add(_L("Part")); + names.Add(_L("Negative Part")); + names.Add(_L("Modifier")); + if (!volume->is_svg()) { + names.Add(_L("Support Blocker")); + names.Add(_L("Support Enforcer")); + } + + SingleChoiceDialog dlg(_L("Type:"), _L("Choose part type"), names, int(type)); auto new_type = ModelVolumeType(dlg.GetSingleChoiceIndex()); if (new_type == type || new_type == ModelVolumeType::INVALID) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index fd1cf30af..47a3ed541 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -481,6 +481,83 @@ void GLGizmoBase::set_dirty() { m_dirty = true; } +bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) +{ + bool is_dragging_finished = false; + if (mouse_event.Moving()) { + // it should not happen but for sure + assert(!m_dragging); + if (m_dragging) + is_dragging_finished = true; + else + return false; + } + + if (mouse_event.LeftDown()) { + Selection &selection = m_parent.get_selection(); + if (!selection.is_empty() && m_hover_id != -1 /* && + (m_grabbers.empty() || m_hover_id < static_cast(m_grabbers.size()))*/) { + selection.setup_cache(); + + m_dragging = true; + for (auto &grabber : m_grabbers) grabber.dragging = false; + // if (!m_grabbers.empty() && m_hover_id < int(m_grabbers.size())) + // m_grabbers[m_hover_id].dragging = true; + + on_start_dragging(); + + // Let the plater know that the dragging started + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED)); + m_parent.set_as_dirty(); + return true; + } + } else if (m_dragging) { + // when mouse cursor leave window than finish actual dragging operation + bool is_leaving = mouse_event.Leaving(); + if (mouse_event.Dragging()) { + Point mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + auto ray = m_parent.mouse_ray(mouse_coord); + UpdateData data(ray, mouse_coord); + + update(data); + + wxGetApp().obj_manipul()->set_dirty(); + m_parent.set_as_dirty(); + return true; + } else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { + do_stop_dragging(is_leaving); + return true; + } + } + return false; +} + +void GLGizmoBase::do_stop_dragging(bool perform_mouse_cleanup) +{ + for (auto &grabber : m_grabbers) grabber.dragging = false; + m_dragging = false; + + // NOTE: This should be part of GLCanvas3D + // Reset hover_id when leave window + if (perform_mouse_cleanup) m_parent.mouse_up_cleanup(); + + on_stop_dragging(); + + // There is prediction that after draggign, data are changed + // Data are updated twice also by canvas3D::reload_scene. + // Should be fixed. + m_parent.get_gizmos_manager().update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + + // Let the plater know that the dragging finished, so a delayed + // refresh of the scene with the background processing data should + // be performed. + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + m_parent.refresh_camera_scene_box(); +} + void GLGizmoBase::render_input_window(float x, float y, float bottom_limit) { on_render_input_window(x, y, bottom_limit); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index d412eb9f4..64dd35259 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -25,8 +25,8 @@ class Linef3; class ModelObject; namespace GUI { - - +#define MAX_NUM 9999.99 +#define MAX_SIZE "9999.99" class ImGuiWrapper; class GLCanvas3D; @@ -275,6 +275,22 @@ protected: // Mark gizmo as dirty to Re-Render when idle() void set_dirty(); + + /// + /// function which + /// Set up m_dragging and call functions + /// on_start_dragging / on_dragging / on_stop_dragging + /// + /// Keep information about mouse click + /// same as on_mouse + bool use_grabbers(const wxMouseEvent &mouse_event); + void do_stop_dragging(bool perform_mouse_cleanup); + template void limit_value(T &value, T _min, T _max) + { + if (value >= _max) { value = _max;} + if (value <= _min) { value = _min; } + } + private: // Flag for dirty visible state of Gizmo // When True then need new rendering diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 3fe13f865..e8aa86612 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -360,7 +360,7 @@ void GLGizmoRotate::transform_to_local(const Selection &selection) const { glsafe(::glTranslated(m_center(0), m_center(1), m_center(2))); - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) { + if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes() || m_force_local_coordinate) { glsafe(::glMultMatrixd(Geometry::Transformation(m_orient_matrix).get_matrix_no_offset().data())); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp new file mode 100644 index 000000000..f6d6bf200 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp @@ -0,0 +1,2168 @@ +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoSVG.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" + +#include "slic3r/GUI/Plater.hpp" +#include "libslic3r/AppConfig.hpp" + +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp" +#include "slic3r/GUI/MainFrame.hpp" // to update title when add text +#include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/CameraUtils.hpp" + +#include "slic3r/Utils/UndoRedo.hpp" + +#include "libslic3r/Point.hpp" +#include "libslic3r/SVG.hpp" // debug store +#include "libslic3r/Geometry.hpp" // covex hull 2d +#include "libslic3r/Timer.hpp" // covex hull 2d + +#include "libslic3r/NSVGUtils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/ClipperUtils.hpp" // union_ex + +#include "imgui/imgui_stdlib.h" // using std::string for inputs +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include +#include "nanosvg/nanosvg.h" // load SVG file + +#include // detection of change DPI +#include + + +#include + +#include +#include +#include // measure enumeration of fonts +#include // save for svg +#include +#include + +#include "slic3r/GUI/BitmapCache.hpp" +#include "libslic3r/AABBTreeLines.hpp" // aabb lines for draw filled expolygon +#include +#include // use surface cuts +#include "slic3r/GUI/Jobs/Worker.hpp" +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; + +namespace Slic3r { +namespace GUI { +const int c_move_cube_id = 1; +const std::array SVG_Move_GrabberColor = {1.0, 1.0, 0.0, 1.0}; +const std::array SVG_Move_GrabberHoverColor = {0.7, 0.7, 0.0, 1.0}; + +// TRN - Title in Undo/Redo stack after rotate with SVG around emboss axe +const std::string rotation_snapshot_name = "SVG rotate"; +// NOTE: Translation is made in "m_parent.do_rotate()" + +// TRN - Title in Undo/Redo stack after move with SVG along emboss axe - From surface +const std::string move_snapshot_name = "SVG move"; +// NOTE: Translation is made in "m_parent.do_translate()" + +// Variable keep limits for variables +const struct Limits +{ + MinMax depth{0.01, 1e4}; // in mm + MinMax size{0.01f, 1e4f}; // in mm (width + height) + MinMax ui_size{5.f, 100.f}; // in mm (width + height) - only slider values + MinMax ui_size_in{.1f, 4.f}; // in inches (width + height) - only slider values + MinMax relative_scale_ratio{1e-5, 1e4}; // change size + // distance text object from surface + MinMax angle{-180.f, 180.f}; // in degrees +} limits; + +wxString last_used_directory = wxEmptyString; +std::string choose_svg_file() +{ + wxWindow * parent = nullptr; + wxString message = _L("Choose SVG file for emboss:"); + wxString selected_file = wxEmptyString; + wxString wildcard = file_wildcards(FT_SVG); + long style = wxFD_OPEN | wxFD_FILE_MUST_EXIST; + wxFileDialog dialog(parent, message, last_used_directory, selected_file, wildcard, style); + if (dialog.ShowModal() != wxID_OK) { + BOOST_LOG_TRIVIAL(warning) << "SVG file for emboss was NOT selected."; + return {}; + } + + wxArrayString input_files; + dialog.GetPaths(input_files); + if (input_files.IsEmpty()) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result is empty."; + return {}; + } + + if (input_files.size() != 1) BOOST_LOG_TRIVIAL(warning) << "SVG file dialog result contain multiple files but only first is used."; + + std::string path = into_u8(input_files.front()); + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return invalid path."; + return {}; + } + + if (!boost::algorithm::iends_with(path, ".svg")) { + BOOST_LOG_TRIVIAL(warning) << "SVG file dialog return path without '.svg' tail"; + return {}; + } + + last_used_directory = dialog.GetDirectory(); + return path; +} + + + + +double get_tesselation_tolerance(double scale) +{ + double tesselation_tolerance_in_mm = .1; // 8e-2; + double tesselation_tolerance_scaled = (tesselation_tolerance_in_mm * tesselation_tolerance_in_mm) / SCALING_FACTOR / SCALING_FACTOR; + return tesselation_tolerance_scaled / scale / scale; +} + +EmbossShape select_shape(std::string_view filepath, double tesselation_tolerance = get_tesselation_tolerance(1.)) +{ + EmbossShape shape; + shape.projection.depth = 10.; + shape.projection.use_surface = false; + + EmbossShape::SvgFile svg; + if (filepath.empty()) { + // When empty open file dialog + svg.path = choose_svg_file(); + if (svg.path.empty()) return {}; // file was not selected + } else { + svg.path = filepath; // copy + } + + boost::filesystem::path path(svg.path); + if (!boost::filesystem::exists(path)) { + show_error(nullptr, GUI::format(_u8L("File does NOT exist (%1%)."), svg.path)); + return {}; + } + + if (!boost::algorithm::iends_with(svg.path, ".svg")) { + show_error(nullptr, GUI::format(_u8L("Filename has to end with \".svg\" but you selected %1%"), svg.path)); + return {}; + } + + if (init_image(svg) == nullptr) { + show_error(nullptr, GUI::format(_u8L("Nano SVG parser can't load from file (%1%)."), svg.path)); + return {}; + } + + // Set default and unchanging scale + NSVGLineParams params{tesselation_tolerance}; + shape.shapes_with_ids = create_shape_with_ids(*svg.image, params); + + // Must contain some shapes !!! + if (shape.shapes_with_ids.empty()) { + show_error(nullptr, GUI::format(_u8L("SVG file does NOT contain a single path to be embossed (%1%)."), svg.path)); + return {}; + } + shape.svg_file = std::move(svg); + return shape; +} + +std::string get_file_name(const std::string &file_path) +{ + if (file_path.empty()) return file_path; + + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + if (pos_last_delimiter == std::string::npos) { + // should not happend that in path is not delimiter + assert(false); + pos_last_delimiter = 0; + } + + size_t pos_point = file_path.find_last_of('.'); + if (pos_point == std::string::npos || pos_point < pos_last_delimiter // last point is inside of directory path + ) { + // there is no extension + assert(false); + pos_point = file_path.size(); + } + + size_t offset = pos_last_delimiter + 1; // result should not contain last delimiter ( +1 ) + size_t count = pos_point - pos_last_delimiter - 1; // result should not contain extension point ( -1 ) + return file_path.substr(offset, count); +} + +std::string volume_name(const EmbossShape &shape) +{ + std::string file_name = get_file_name(shape.svg_file->path); + if (!file_name.empty()) + return file_name; + return "SVG shape"; +} + + + + + +// BBS: GUI refactor: add obj manipulation +GLGizmoSVG::GLGizmoSVG(GLCanvas3D& parent, unsigned int sprite_id) + : GLGizmoBase(parent, "toolbar_cut.svg", sprite_id) //"toolbar_cut.svg" no use + , m_gui_cfg(nullptr) + , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) +{ + m_rotate_gizmo.set_group_id(0); + m_rotate_gizmo.set_force_local_coordinate(true); +} + +std::string GLGizmoSVG::get_tooltip() const +{ + return ""; +} + +void GLGizmoSVG::data_changed(bool is_serializing) +{ + set_volume_by_selection(); + if (!is_serializing && m_volume == nullptr) + close(); +} + +void GLGizmoSVG::on_enable_grabber(unsigned int id) { + m_rotate_gizmo.enable_grabber(0); +} + +void GLGizmoSVG::on_disable_grabber(unsigned int id) { + m_rotate_gizmo.disable_grabber(0); +} + +Emboss::DataBasePtr GLGizmoSVG::create_emboss_data_base(std::shared_ptr> &cancel, ModelVolumeType volume_type, std::string_view filepath) +{ + EmbossShape shape = select_shape(filepath); + + if (shape.shapes_with_ids.empty()) + // canceled selection of SVG file + return nullptr; + + // Cancel previous Job, when it is in process + // worker.cancel(); --> Use less in this case I want cancel only previous EmbossJob no other jobs + // Cancel only EmbossUpdateJob no others + if (cancel != nullptr) + cancel->store(true); + // create new shared ptr to cancel new job + cancel = std::make_shared>(false); + + std::string name = volume_name(shape); + + auto result = std::make_unique(name, cancel, std::move(shape)); + result->is_outside = volume_type == ModelVolumeType::MODEL_PART; + return result; +} + +Emboss::CreateVolumeParams GLGizmoSVG::create_input(GLCanvas3D &canvas, ModelVolumeType volume_type) +{ + auto gizmo = static_cast(GLGizmosManager::Svg); + const GLVolume *gl_volume = canvas.get_selection().get_first_volume();//modify by bbs //get_first_hovered_gl_volume(canvas); + Plater * plater = wxGetApp().plater(); + auto register_mesh_pick = [this]() { register_single_mesh_pick(); }; +#ifndef EXECUTE_UPDATE_ON_MAIN_THREAD + return Emboss::CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), plater->get_ui_job_worker(), + register_mesh_pick, m_raycast_manager, m_raycast_condition,volume_type,gizmo,gl_volume}; +#else + return Emboss::CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), volume_type, gizmo, gl_volume}; + #endif +} + +enum class IconType : unsigned { + reset_value, + refresh, + change_file, + bake, + save, + exclamation, + lock, + unlock, + reflection_x, + reflection_y, + // automatic calc of icon's count + _count +}; +// This configs holds GUI layout size given by translated texts. +// etc. When language changes, GUI is recreated and this class constructed again, +// so the change takes effect. (info by GLGizmoFdmSupports.hpp) +struct GuiCfg +{ + // Detect invalid config values when change monitor DPI + double screen_scale = -1.; + bool dark_mode = false; + + // Define bigger size(width or height) + unsigned texture_max_size_px = 256; + + float input_width = 0.f; + float input_offset = 0.f; + + float icon_width = 0.f; + + float max_tooltip_width = 0.f; + + // offset for checbox for lock up vector + float lock_offset = 0.f; + // Only translations needed for calc GUI size + struct Translations + { + std::string depth; + std::string size; + std::string use_surface; + std::string rotation; + std::string distance; // from surface + std::string mirror; + }; + Translations translations; +}; + +GuiCfg create_gui_configuration() +{ + GuiCfg cfg; // initialize by default values; + + float line_height = ImGui::GetTextLineHeight(); + float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); + + float space = line_height_with_spacing - line_height; + + cfg.icon_width = std::max(std::round(line_height / 8) * 8, 8.f); + + GuiCfg::Translations &tr = cfg.translations; + + float lock_width = cfg.icon_width + 3 * space; + // TRN - Input label. Be short as possible + tr.depth = _u8L("Depth"); + // TRN - Input label. Be short as possible + tr.size = _u8L("Size"); + // TRN - Input label. Be short as possible + tr.use_surface = _u8L("Use surface"); + // TRN - Input label. Be short as possible + tr.distance = _u8L("From surface"); + // TRN - Input label. Be short as possible + tr.rotation = _u8L("Rotation"); + // TRN - Input label. Be short as possible + tr.mirror = _u8L("Mirror"); + float max_tr_width = std::max({ + ImGui::CalcTextSize(tr.depth.c_str()).x, + ImGui::CalcTextSize(tr.size.c_str()).x + lock_width, + ImGui::CalcTextSize(tr.use_surface.c_str()).x, + ImGui::CalcTextSize(tr.distance.c_str()).x + space, + ImGui::CalcTextSize(tr.rotation.c_str()).x + lock_width, + ImGui::CalcTextSize(tr.mirror.c_str()).x, + }); + + const ImGuiStyle &style = ImGui::GetStyle(); + cfg.input_offset = style.WindowPadding.x + max_tr_width + space * 2+ cfg.icon_width; + cfg.lock_offset = cfg.input_offset - (cfg.icon_width + 2 * space); + + ImVec2 letter_m_size = ImGui::CalcTextSize("M"); + const float count_letter_M_in_input = 12.f; + cfg.input_width = letter_m_size.x * count_letter_M_in_input; + cfg.texture_max_size_px = std::round((cfg.input_width + cfg.input_offset + cfg.icon_width + space) / 8) * 8; + + cfg.max_tooltip_width = ImGui::GetFontSize() * 20.0f; + + return cfg; +} + +// use private definition +struct GLGizmoSVG::GuiCfg : public ::GuiCfg +{}; +// Define rendered version of icon +enum class IconState : unsigned { activable = 0, hovered /*1*/, disabled /*2*/ }; +// selector for icon by enum +const IconManager::Icon &get_icon(const IconManager::VIcons &icons, IconType type, IconState state) { return *icons[(unsigned) type][(unsigned) state]; } + +IconManager::VIcons init_icons(IconManager &mng, const GuiCfg &cfg) +{ + mng.release(); + + ImVec2 size(cfg.icon_width, cfg.icon_width); + // icon order has to match the enum IconType + std::vector filenames{ + "text_undo.svg", // reset_value + "text_refresh.svg", // refresh + "text_open.svg", // changhe_file + "text_bake.svg", // bake + "text_save.svg", // save + "text_obj_warning.svg", // exclamation // ORCA: use obj_warning instead exclamation. exclamation is not compatible with low res + "text_lock_closed.svg", // lock + "text_lock_open.svg", // unlock + "text_reflection_x.svg", // reflection_x + "text_reflection_y.svg", // reflection_y + }; + + assert(filenames.size() == static_cast(IconType::_count)); + std::string path = resources_dir() + "/images/"; + for (std::string &filename : filenames) + filename = path + filename; + + auto type = IconManager::RasterType::color_wite_gray; + return mng.init(filenames, size, type); +} + +bool draw_clickable(const IconManager::VIcons &icons, IconType type) { + return clickable(get_icon(icons, type, IconState::activable), get_icon(icons, type, IconState::hovered)); +} + +bool reset_button(const IconManager::VIcons &icons) +{ + float reset_offset = ImGui::GetStyle().WindowPadding.x; + ImGui::SameLine(reset_offset); + // from GLGizmoCut + // std::string label_id = "neco"; + // std::string btn_label; + // btn_label += ImGui::RevertButton; + // return ImGui::Button((btn_label + "##" + label_id).c_str()); + return draw_clickable(icons, IconType::reset_value); +} + +bool GLGizmoSVG::create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type) +{//drag file//drag svg_file + wxBusyCursor wait; + Emboss::CreateVolumeParams input = create_input(m_parent, volume_type); + Emboss::DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type, svg_file); + if (!base) + return false; // Uninterpretable svg + return start_create_volume(input, std::move(base), mouse_pos); +} + +bool GLGizmoSVG::is_svg_object(const ModelVolume &volume) +{ + if (!volume.emboss_shape.has_value()) return false; + if (volume.type() != ModelVolumeType::MODEL_PART) return false; + for (const ModelVolume *v : volume.get_object()->volumes) { + if (v->id() == volume.id()) continue; + if (v->type() == ModelVolumeType::MODEL_PART) return false; + } + return true; +} + +bool GLGizmoSVG::is_svg(const ModelVolume &volume) { + return volume.emboss_shape.has_value() && volume.emboss_shape->svg_file.has_value(); +} + +bool GLGizmoSVG::on_init() +{ + m_rotate_gizmo.init(); + ColorRGBA gray_color(.6f, .6f, .6f, .3f); + m_rotate_gizmo.set_highlight_color(gray_color.get_data()); + // Set rotation gizmo upwardrotate + m_rotate_gizmo.set_angle(PI / 2); + return true; +} + +std::string GLGizmoSVG::on_get_name() const +{ + return "SVG"; +} + +bool GLGizmoSVG::on_is_activable() const +{ + const Selection &selection = m_parent.get_selection(); + if (selection.is_empty()) { + return true; + } + return !selection.is_any_connector();//maybe is negitive volume or modifier volume +} + +void GLGizmoSVG::on_set_state() { + // enable / disable bed from picking + // Rotation gizmo must work through bed + // m_parent.set_raycaster_gizmos_on_top(GLGizmoBase::m_state == GLGizmoBase::On); + m_rotate_gizmo.set_state(GLGizmoBase::m_state); + + // Closing gizmo. e.g. selecting another one + if (GLGizmoBase::m_state == GLGizmoBase::Off) { + m_parent.enable_moving(true); // modify by bbs + reset_volume(); + } else if (GLGizmoBase::m_state == GLGizmoBase::On) { + if (on_is_activable()) { + // Try(when exist) set text configuration by volume + set_volume_by_selection(); + if (m_volume) { + register_single_mesh_pick(); + } + } + } +} + +void GLGizmoSVG::on_start_dragging() +{ + if (m_hover_id < 0) { return;} + if (m_hover_id == c_move_cube_id) { + } + else { + m_rotate_gizmo.start_dragging(); + } +} + +void GLGizmoSVG::on_stop_dragging() +{ + if (m_hover_id == c_move_cube_id) { + } else { + m_rotate_gizmo.stop_dragging(); + // TODO: when start second rotatiton previous rotation rotate draggers + // This is fast fix for second try to rotate + // When fixing, move grabber above text (not on side) + m_rotate_gizmo.set_angle(PI / 2); + + // apply rotation + // TRN This is an item label in the undo-redo stack. + m_parent.do_rotate(rotation_snapshot_name); + m_rotate_start_angle.reset(); + volume_transformation_changed(); + // recalculate for surface cut + if (m_volume != nullptr && m_volume->emboss_shape.has_value() && m_volume->emboss_shape->projection.use_surface) + process_job(); + } +} + +bool GLGizmoSVG::on_mouse(const wxMouseEvent &mouse_event) +{ // not selected volume + if (m_volume == nullptr || get_model_volume(m_volume_id, m_parent.get_selection().get_model()->objects) == nullptr || !m_volume->emboss_shape.has_value()) + return false; + if (m_hover_id != c_move_cube_id) { + if (on_mouse_for_rotation(mouse_event)) + return true; + } + if (m_hover_id == c_move_cube_id) { + if (mouse_event.Moving()) + return false; + bool used = use_grabbers(mouse_event); + if (on_mouse_for_translate(mouse_event)) + return true; + } + return false; +} + +void GLGizmoSVG::on_update(const UpdateData &data) { + if (m_hover_id == c_move_cube_id) { + } + else { + m_rotate_gizmo.update(data); + } +} + +void GLGizmoSVG::on_render() +{ + if (const Selection &selection = m_parent.get_selection(); selection.volumes_count() != 1 || // only one selected volume + m_volume == nullptr || // already selected volume in gizmo + get_model_volume(m_volume_id, selection.get_model()->objects) == nullptr) // still exist model + return; + if (!m_svg_volume) { + return; + } + bool is_surface_dragging = m_surface_drag.has_value(); + bool is_parent_dragging = m_parent.is_mouse_dragging(); + // Do NOT render rotation grabbers when dragging object + bool is_rotate_by_grabbers = m_dragging; + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + Geometry::Transformation tran(m_svg_volume->world_matrix()); + if (!m_volume->is_the_only_one_part()) { + m_parent.enable_moving(false); // modify by bbs + bool hover = (m_hover_id == c_move_cube_id); + std::array render_color; + if (hover) { + render_color = SVG_Move_GrabberHoverColor; + } else + render_color = SVG_Move_GrabberColor; + float fullsize = 8.0f; + if (GLGizmoBase::INV_ZOOM > 0) { fullsize = m_move_grabber.FixedGrabberSize * GLGizmoBase::INV_ZOOM; } + m_move_grabber.center = tran.get_offset(); + Transform3d rotate_matrix = tran.get_rotation_matrix(); + Transform3d cube_mat = Geometry::translation_transform(m_move_grabber.center) * rotate_matrix * Geometry::scale_transform(fullsize); + render_glmodel(m_move_grabber.get_cube(), render_color, cube_mat); + } +#ifdef DEBUG_SVG + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixd(tran.get_matrix().data())); + render_cross_mark(Vec3f::Zero(), true); + glsafe(::glPopMatrix()); +#endif + if (is_rotate_by_grabbers || (!is_surface_dragging && !is_parent_dragging)) { + if (m_hover_id == c_move_cube_id && m_dragging) { + } + else { + m_rotate_gizmo.render(); + } + } +} + +void GLGizmoSVG::on_render_for_picking() +{ + glsafe(::glDisable(GL_DEPTH_TEST)); + m_rotate_gizmo.render_for_picking(); + + auto color = picking_color_component(c_move_cube_id); + m_move_grabber.color[0] = color[0]; + m_move_grabber.color[1] = color[1]; + m_move_grabber.color[2] = color[2]; + m_move_grabber.color[3] = color[3]; + float mean_size = (float) (GLGizmoBase::Grabber::FixedGrabberSize); + m_move_grabber.render_for_picking(mean_size); +} + +//BBS: add input window for move +void GLGizmoSVG::on_render_input_window(float x, float y, float bottom_limit) +{ + set_volume_by_selection(); + double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); + // Configuration creation + if (m_gui_cfg == nullptr || // Exist configuration - first run + m_gui_cfg->screen_scale != screen_scale || // change of DPI + m_gui_cfg->dark_mode != m_is_dark_mode // change of dark mode + ) { + // Create cache for gui offsets + ::GuiCfg cfg = create_gui_configuration(); + cfg.screen_scale = screen_scale; + cfg.dark_mode = m_is_dark_mode; + + GuiCfg gui_cfg{std::move(cfg)}; + m_gui_cfg = std::make_unique(std::move(gui_cfg)); + + m_icons = init_icons(m_icon_manager, *m_gui_cfg); // need regeneration when change resolution(move between monitors) + } + + static float last_y = 0.0f; + static float last_h = 0.0f; + const float win_h = ImGui::GetWindowHeight(); + y = std::min(y, bottom_limit - win_h); + GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f); + if (last_h != win_h || last_y != y) { + // ask canvas for another frame to render the window in the correct position + m_imgui->set_requires_extra_frame(); + if (last_h != win_h) last_h = win_h; + if (last_y != y) last_y = y; + } + ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); + GizmoImguiBegin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); + + draw_window(); + + GizmoImguiEnd(); + ImGuiWrapper::pop_toolbar_style(); +} + +void GLGizmoSVG::set_volume_by_selection() +{ + const Selection &selection = m_parent.get_selection(); + const GLVolume * gl_volume = get_selected_gl_volume(selection); + + if (gl_volume == nullptr) + return reset_volume(); + + const ModelObjectPtrs &objects = selection.get_model()->objects; + ModelVolume * volume = get_model_volume(*gl_volume, objects); + if (volume == nullptr) + return reset_volume(); + + // is same volume as actual selected? + if (volume->id() == m_volume_id) + return; + + // Do not use focused input value when switch volume(it must swith value) + if (m_volume != nullptr && m_volume != volume) // when update volume it changed id BUT not pointer + ImGuiWrapper::left_inputs(); + + // is valid svg volume? + if (!is_svg(*volume)) + return reset_volume(); + + // cancel previous job + if (m_job_cancel != nullptr) { + m_job_cancel->store(true); + m_job_cancel = nullptr; + } + + // calculate scale for height and depth inside of scaled object instance + calculate_scale(); // must be before calculation of tesselation + + // checking that exist is inside of function "is_svg" + EmbossShape & es = *volume->emboss_shape; + EmbossShape::SvgFile &svg_file = *es.svg_file; + if (svg_file.image == nullptr) { + if (init_image(svg_file) == nullptr) + return reset_volume(); + } + + assert(svg_file.image != nullptr); + assert(svg_file.image.get() != nullptr); + const NSVGimage & image = *svg_file.image; + ExPolygonsWithIds &shape_ids = es.shapes_with_ids; + if (shape_ids.empty()) { + NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; + shape_ids = create_shape_with_ids(image, params); + } + reset_volume(); // clear cached data + + m_svg_volume = gl_volume; + m_volume = volume; + m_volume_id = volume->id(); + m_volume_shape = es; // copy + m_shape_warnings = create_shape_warnings(es, get_scale_for_tolerance()); + + // Calculate current angle of up vector + m_angle = calc_angle(selection); + m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + m_shape_bb = get_extents(m_volume_shape.shapes_with_ids); + m_origin_shape_bb = m_shape_bb; +} + +void GLGizmoSVG::delete_texture(Emboss::Texture &texture) +{ + if (texture.id != 0) { + glsafe(::glDeleteTextures(1, &texture.id)); + texture.id = 0; + } +} + +void GLGizmoSVG::reset_volume() { + if (m_volume == nullptr) + return; // already reseted + m_svg_volume = nullptr; + m_volume = nullptr; + m_volume_id.id = 0; + m_volume_shape.shapes_with_ids.clear(); + m_filename_preview.clear(); + m_shape_warnings.clear(); + // delete texture after finish imgui draw + wxGetApp().plater()->CallAfter([this]() { + delete_texture(m_texture); + }); +} + +void GLGizmoSVG::calculate_scale() { + // be carefull m_volume is not set yet + const Selection &selection = m_parent.get_selection(); + const GLVolume * gl_volume = selection.get_first_volume(); + if (gl_volume == nullptr) return; + + Transform3d to_world = gl_volume->world_matrix(); + + const ModelVolume *volume_ptr = get_model_volume(*gl_volume, selection.get_model()->objects); + assert(volume_ptr != nullptr); + assert(volume_ptr->emboss_shape.has_value()); + // Fix for volume loaded from 3mf + if (volume_ptr != nullptr && volume_ptr->emboss_shape.has_value()) { + const std::optional &fix_tr = volume_ptr->emboss_shape->fix_3mf_tr; + if (fix_tr.has_value()) to_world = to_world * (fix_tr->inverse()); + } + + auto to_world_linear = to_world.linear(); + auto calc = [&to_world_linear](const Vec3d &axe, std::optional &scale) { + Vec3d axe_world = to_world_linear * axe; + double norm_sq = axe_world.squaredNorm(); + if (is_approx(norm_sq, 1.)) { + if (!scale.has_value()) return; + scale.reset(); + } else { + scale = sqrt(norm_sq); + } + }; + + calc(Vec3d::UnitX(), m_scale_width); + calc(Vec3d::UnitY(), m_scale_height); + calc(Vec3d::UnitZ(), m_scale_depth); +} + +// inspired by Xiaolin Wu's line algorithm - https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm +// Draw inner part of polygon CCW line as full brightness(edge of expolygon) +void wu_draw_line_side(Linef line, const std::function &plot) +{ + auto ipart = [](float x) -> int { return static_cast(std::floor(x)); }; + auto round = [](float x) -> float { return std::round(x); }; + auto fpart = [](float x) -> float { return x - std::floor(x); }; + auto rfpart = [=](float x) -> float { return 1 - fpart(x); }; + + Vec2d d = line.b - line.a; + const bool steep = abs(d.y()) > abs(d.x()); + bool is_full; // identify full brightness pixel + if (steep) { + is_full = d.y() >= 0; + std::swap(line.a.x(), line.a.y()); + std::swap(line.b.x(), line.b.y()); + std::swap(d.x(), d.y()); + } else + is_full = d.x() < 0; // opposit direction of y + + if (line.a.x() > line.b.x()) { + std::swap(line.a.x(), line.b.x()); + std::swap(line.a.y(), line.b.y()); + d *= -1; + } + const float gradient = (d.x() == 0) ? 1. : d.y() / d.x(); + + int xpx11; + float intery; + { + const float xend = round(line.a.x()); + const float yend = line.a.y() + gradient * (xend - line.a.x()); + const float xgap = rfpart(line.a.x() + 0.5f); + xpx11 = int(xend); + const int ypx11 = ipart(yend); + if (steep) { + plot(ypx11, xpx11, is_full ? 1.f : (rfpart(yend) * xgap)); + plot(ypx11 + 1, xpx11, !is_full ? 1.f : (fpart(yend) * xgap)); + } else { + plot(xpx11, ypx11, is_full ? 1.f : (rfpart(yend) * xgap)); + plot(xpx11, ypx11 + 1, !is_full ? 1.f : (fpart(yend) * xgap)); + } + intery = yend + gradient; + } + + int xpx12; + { + const float xend = round(line.b.x()); + const float yend = line.b.y() + gradient * (xend - line.b.x()); + const float xgap = rfpart(line.b.x() + 0.5f); + xpx12 = int(xend); + const int ypx12 = ipart(yend); + if (steep) { + plot(ypx12, xpx12, is_full ? 1.f : (rfpart(yend) * xgap)); + plot(ypx12 + 1, xpx12, !is_full ? 1.f : (fpart(yend) * xgap)); + } else { + plot(xpx12, ypx12, is_full ? 1.f : (rfpart(yend) * xgap)); + plot(xpx12, ypx12 + 1, !is_full ? 1.f : (fpart(yend) * xgap)); + } + } + + if (steep) { + if (is_full) { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, 1.f); + plot(ipart(intery) + 1, x, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, rfpart(intery)); + plot(ipart(intery) + 1, x, 1.f); + intery += gradient; + } + } + } else { + if (is_full) { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), 1.f); + plot(x, ipart(intery) + 1, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), rfpart(intery)); + plot(x, ipart(intery) + 1, 1.f); + intery += gradient; + } + } + } +} + +template // N .. count of channels per pixel +void draw_side_outline(const ExPolygons &shape, const std::array &color, std::vector &data, size_t data_width, double scale) +{ + int count_lines = data.size() / (N * data_width); + size_t data_line = N * data_width; + auto get_offset = [count_lines, data_line](int x, int y) { + // NOTE: y has opposit direction in texture + return (count_lines - y - 1) * data_line + x * N; + }; + + // overlap color + auto draw = [&data, data_width, count_lines, get_offset, &color](int x, int y, float brightess) { + if (x < 0 || y < 0 || static_cast(x) >= data_width || y >= count_lines) return; // out of image + size_t offset = get_offset(x, y); + bool change_color = false; + for (size_t i = 0; i < N - 1; ++i) { + if (data[offset + i] != color[i]) { + data[offset + i] = color[i]; + change_color = true; + } + } + + unsigned char &alpha = data[offset + N - 1]; + if (alpha == 0 || change_color) { + alpha = static_cast(std::round(brightess * 255)); + } else if (alpha != 255) { + alpha = static_cast(std::min(255, int(alpha) + static_cast(std::round(brightess * 255)))); + } + }; + + BoundingBox bb_unscaled = get_extents(shape); + Linesf lines = to_linesf(shape); + BoundingBoxf bb(bb_unscaled.min.cast(), bb_unscaled.max.cast()); + + // scale lines to pixels + if (!is_approx(scale, 1.)) { + for (Linef &line : lines) { + line.a *= scale; + line.b *= scale; + } + bb.min *= scale; + bb.max *= scale; + } + + for (const Linef &line : lines) + wu_draw_line_side(line, draw); +} + +/// +/// Draw filled ExPolygon into data +/// line by line inspired by: http://alienryderflex.com/polygon_fill/ +/// +/// Count channels for one pixel(RGBA = 4) +/// Shape to draw +/// Color of shape contain count of channels(N) +/// Image(2d) stored in 1d array +/// Count of pixel on one line(size in data = N x data_width) +/// Shape scale for conversion to pixels +template // N .. count of channels per pixel +void draw_filled(const ExPolygons &shape, const std::array &color, std::vector &data, size_t data_width, double scale) +{ + assert(data.size() % N == 0); + assert(data.size() % data_width == 0); + assert((data.size() % (N * data_width)) == 0); + + BoundingBox bb_unscaled = get_extents(shape); + + Linesf lines = to_linesf(shape); + BoundingBoxf bb(bb_unscaled.min.cast(), bb_unscaled.max.cast()); + + // scale lines to pixels + if (!is_approx(scale, 1.)) { + for (Linef &line : lines) { + line.a *= scale; + line.b *= scale; + } + bb.min *= scale; + bb.max *= scale; + } + + int count_lines = data.size() / (N * data_width); + size_t data_line = N * data_width; + auto get_offset = [count_lines, data_line](int x, int y) { + // NOTE: y has opposit direction in texture + return (count_lines - y - 1) * data_line + x * N; + }; + auto set_color = [&data, &color, get_offset](int x, int y) { + size_t offset = get_offset(x, y); + if (data[offset + N - 1] != 0) return; // already setted by line + for (unsigned i = 0; i < N; ++i) data[offset + i] = color[i]; + }; + + // anti aliased drawing of lines + auto draw = [&data, width = static_cast(data_width), count_lines, get_offset, &color](int x, int y, float brightess) { + if (x < 0 || y < 0 || x >= width || y >= count_lines) return; // out of image + size_t offset = get_offset(x, y); + unsigned char &alpha = data[offset + N - 1]; + if (alpha == 0) { + alpha = static_cast(std::round(brightess * 255)); + for (size_t i = 0; i < N - 1; ++i) data[offset + i] = color[i]; + } else if (alpha != 255) { + alpha = static_cast(std::min(255, int(alpha) + static_cast(std::round(brightess * 255)))); + } + }; + + for (const Linef &line : lines) wu_draw_line_side(line, draw); + + auto tree = Slic3r::AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + + // range for intersection line + double x1 = bb.min.x() - 1.f; + double x2 = bb.max.x() + 1.f; + + int max_y = std::min(count_lines, static_cast(std::round(bb.max.y()))); + for (int y = std::max(0, static_cast(std::round(bb.min.y()))); y < max_y; ++y) { + double y_f = y + .5; // 0.5 ... intersection in center of pixel of pixel + Linef line(Vec2d(x1, y_f), Vec2d(x2, y_f)); + using Intersection = std::pair; + using Intersections = std::vector; + // sorted .. false + // + Intersections intersections = Slic3r::AABBTreeLines::get_intersections_with_line(lines, tree, line); + if (intersections.empty()) continue; + + assert((intersections.size() % 2) == 0); + + // sort intersections by x + std::sort(intersections.begin(), intersections.end(), [](const Intersection &i1, const Intersection &i2) { return i1.first.x() < i2.first.x(); }); + + // draw lines + for (size_t i = 0; i < intersections.size(); i += 2) { + const Vec2d &p2 = intersections[i + 1].first; + if (p2.x() < 0) continue; // out of data + + const Vec2d &p1 = intersections[i].first; + if (p1.x() > data_width) break; // out of data + + // clamp to data + int max_x = std::min(static_cast(data_width - 1), static_cast(std::round(p2.x()))); + for (int x = std::max(0, static_cast(std::round(p1.x()))); x <= max_x; ++x) set_color(x, y); + } + } +} + +/// Union shape defined by glyphs +ExPolygons union_ex(const ExPolygonsWithIds &shapes) +{ + // unify to one expolygon + ExPolygons result; + for (const ExPolygonsWithId &shape : shapes) { + if (shape.expoly.empty()) continue; + expolygons_append(result, shape.expoly); + } + return union_ex(result); +} + +// init texture by draw expolygons into texture +bool GLGizmoSVG::init_texture(Emboss::Texture &texture, const ExPolygonsWithIds &shapes_with_ids, unsigned max_size_px, const std::vector &shape_warnings) +{ + BoundingBox bb = get_extents(shapes_with_ids); + Point bb_size = bb.size(); + double bb_width = bb_size.x(); // [in mm] + double bb_height = bb_size.y(); // [in mm] + + bool is_widder = bb_size.x() > bb_size.y(); + double scale = 0.f; + if (is_widder) { + scale = max_size_px / bb_width; + texture.width = max_size_px; + texture.height = static_cast(std::ceil(bb_height * scale)); + } else { + scale = max_size_px / bb_height; + texture.width = static_cast(std::ceil(bb_width * scale)); + texture.height = max_size_px; + } + const int n_pixels = texture.width * texture.height; + if (n_pixels <= 0) return false; + + constexpr int channels_count = 4; + std::vector data(n_pixels * channels_count, {0}); + + // Union All shapes + ExPolygons shape = union_ex(shapes_with_ids); + + // align to texture + translate(shape, -bb.min); + size_t texture_width = static_cast(texture.width); + unsigned char alpha = 255; // without transparency + std::array color_shape{201, 201, 201, alpha}; // from degin by @JosefZachar + std::array color_error{237, 28, 36, alpha}; // from icon: resources/icons/flag_red.svg + std::array color_warning{237, 107, 33, alpha}; // icons orange + // draw unhealedable shape + for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids) + if (!shapes_with_id.is_healed) { + ExPolygons bad_shape = shapes_with_id.expoly; // copy + translate(bad_shape, -bb.min); // align to texture + draw_side_outline<4>(bad_shape, color_error, data, texture_width, scale); + } + // Draw shape with warning + if (!shape_warnings.empty()) { + for (const ExPolygonsWithId &shapes_with_id : shapes_with_ids) { + assert(shapes_with_id.id < shape_warnings.size()); + if (shapes_with_id.id >= shape_warnings.size()) continue; + if (shape_warnings[shapes_with_id.id].empty()) continue; // no warnings for shape + ExPolygons warn_shape = shapes_with_id.expoly; // copy + translate(warn_shape, -bb.min); // align to texture + draw_side_outline<4>(warn_shape, color_warning, data, texture_width, scale); + } + } + + // Draw rest of shape + draw_filled<4>(shape, color_shape, data, texture_width, scale); + + // sends data to gpu + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + if (texture.id != 0) + glsafe(::glDeleteTextures(1, &texture.id)); + glsafe(::glGenTextures(1, &texture.id)); + glsafe(::glBindTexture(GL_TEXTURE_2D, texture.id)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei) texture.width, (GLsizei) texture.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void *) data.data())); + + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + + GLuint NO_TEXTURE_ID = 0; + glsafe(::glBindTexture(GL_TEXTURE_2D, NO_TEXTURE_ID)); + return true; +} + +bool is_closed(NSVGpath *path) +{ + for (; path != NULL; path = path->next) + if (path->next == NULL && path->closed) return true; + return false; +} + +void add_comma_separated(std::string &result, const std::string &add) +{ + if (!result.empty()) result += ", "; + result += add; +} + +const float warning_preccission = 1e-4f; +std::string create_fill_warning(const NSVGshape &shape) +{ + if (!(shape.flags & NSVG_FLAGS_VISIBLE) || shape.fill.type == NSVG_PAINT_NONE) return {}; // not visible + + std::string warning; + if ((shape.opacity - 1.f + warning_preccission) <= 0.f) + add_comma_separated(warning, GUI::format(_L("Opacity (%1%)"), shape.opacity)); + + // if(shape->flags != NSVG_FLAGS_VISIBLE) add_warning(_u8L("Visibility flag")); + bool is_fill_gradient = shape.fillGradient[0] != '\0'; + if (is_fill_gradient) add_comma_separated(warning, GUI::format(_L("Color gradient (%1%)"), shape.fillGradient)); + + switch (shape.fill.type) { + case NSVG_PAINT_UNDEF: add_comma_separated(warning, _u8L("Undefined fill type")); break; + case NSVG_PAINT_LINEAR_GRADIENT: + if (!is_fill_gradient) add_comma_separated(warning, _u8L("Linear gradient")); + break; + case NSVG_PAINT_RADIAL_GRADIENT: + if (!is_fill_gradient) add_comma_separated(warning, _u8L("Radial gradient")); + break; + // case NSVG_PAINT_NONE: + // case NSVG_PAINT_COLOR: + // default: break; + } + + // Unfilled is only line which could be opened + if (shape.fill.type != NSVG_PAINT_NONE && !is_closed(shape.paths)) add_comma_separated(warning, _u8L("Open filled path")); + return warning; +} + +std::string create_stroke_warning(const NSVGshape &shape) +{ + std::string warning; + if (!(shape.flags & NSVG_FLAGS_VISIBLE) || shape.stroke.type == NSVG_PAINT_NONE || shape.strokeWidth <= 1e-5f) return {}; // not visible + + if ((shape.opacity - 1.f + warning_preccission) <= 0.f) add_comma_separated(warning, GUI::format(_L("Opacity (%1%)"), shape.opacity)); + + bool is_stroke_gradient = shape.strokeGradient[0] != '\0'; + if (is_stroke_gradient) add_comma_separated(warning, GUI::format(_L("Color gradient (%1%)"), shape.strokeGradient)); + + switch (shape.stroke.type) { + case NSVG_PAINT_UNDEF: add_comma_separated(warning, _u8L("Undefined stroke type")); break; + case NSVG_PAINT_LINEAR_GRADIENT: + if (!is_stroke_gradient) add_comma_separated(warning, _u8L("Linear gradient")); + break; + case NSVG_PAINT_RADIAL_GRADIENT: + if (!is_stroke_gradient) add_comma_separated(warning, _u8L("Radial gradient")); + break; + // case NSVG_PAINT_COLOR: + // case NSVG_PAINT_NONE: + // default: break; + } + + return warning; +} + +float GLGizmoSVG::get_scale_for_tolerance() +{ + return std::max(m_scale_width.value_or(1.f), m_scale_height.value_or(1.f)); } + +/// +/// Create warnings about shape +/// +/// Input svg loaded to shapes +/// Vector of warnings with same size as EmbossShape::shapes_with_ids +/// or Empty when no warnings -> for fast checking that every thing is all right(more common case) +std::vector GLGizmoSVG::create_shape_warnings(const EmbossShape &shape, float scale) +{ + const std::shared_ptr &image_ptr = shape.svg_file->image; + assert(image_ptr != nullptr); + if (image_ptr == nullptr) return {std::string{"Uninitialized SVG image"}}; + + const NSVGimage & image = *image_ptr; + std::vector result; + auto add_warning = [&result, &image](size_t index, const std::string &message) { + if (result.empty()) result = std::vector(get_shapes_count(image) * 2); + std::string &res = result[index]; + if (res.empty()) + res = message; + else + res += '\n' + message; + }; + + if (!shape.final_shape.is_healed) { + for (const ExPolygonsWithId &i : shape.shapes_with_ids) + if (!i.is_healed) add_warning(i.id, _u8L("Path can't be healed from selfintersection and multiple points.")); + + // This waning is not connected to NSVGshape. It is about union of paths, but Zero index is shown first + size_t index = 0; + add_warning(index, _u8L("Final shape constains selfintersection or multiple points with same coordinate.")); + } + + size_t shape_index = 0; + for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next, ++shape_index) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) { + add_warning(shape_index * 2, GUI::format(_L("Shape is marked as invisible (%1%)."), shape->id)); + continue; + } + + std::string fill_warning = create_fill_warning(*shape); + if (!fill_warning.empty()) { + // TRN: The first placeholder is shape identifier, the second one is text describing the problem. + add_warning(shape_index * 2, GUI::format(_L("Fill of shape (%1%) contains unsupported: %2%."), shape->id, fill_warning)); + } + + float minimal_width_in_mm = 1e-3f; + if (shape->strokeWidth <= minimal_width_in_mm * scale) { + add_warning(shape_index * 2, GUI::format(_L("Stroke of shape (%1%) is too thin (minimal width is %2% mm)."), shape->id, minimal_width_in_mm)); + continue; + } + std::string stroke_warning = create_stroke_warning(*shape); + if (!stroke_warning.empty()) add_warning(shape_index * 2 + 1, GUI::format(_L("Stroke of shape (%1%) contains unsupported: %2%."), shape->id, stroke_warning)); + } + return result; +} + +bool GLGizmoSVG::process_job(bool make_snapshot) +{ // no volume is selected -> selection from right panel + assert(m_volume != nullptr); + if (m_volume == nullptr) return false; + + assert(m_volume->emboss_shape.has_value()); + if (!m_volume->emboss_shape.has_value()) return false; + + // Cancel previous Job, when it is in process + // worker.cancel(); --> Use less in this case I want cancel only previous EmbossJob no other jobs + // Cancel only EmbossUpdateJob no others + if (m_job_cancel != nullptr) + m_job_cancel->store(true); + // create new shared ptr to cancel new job + m_job_cancel = std::make_shared>(false); + + EmbossShape shape = m_volume_shape; // copy + auto base = std::make_unique(m_volume->name, m_job_cancel, std::move(shape)); + base->is_outside = m_volume->type() == ModelVolumeType::MODEL_PART; + Emboss::DataUpdate data{std::move(base), m_volume_id, make_snapshot}; + return start_update_volume(std::move(data), *m_volume, m_parent.get_selection(), m_raycast_manager); +} + +void GLGizmoSVG::close() +{ // close gizmo == open it again + auto &mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Svg) + mng.open_gizmo(GLGizmosManager::Svg); + reset_volume(); +} + +void GLGizmoSVG::draw_window() +{ + assert(m_volume != nullptr); + assert(m_volume_id.valid()); + if (m_volume == nullptr || m_volume_id.invalid()) { + ImGui::Text("Not valid state please report reproduction steps on github"); + return; + } + + assert(m_volume->emboss_shape.has_value()); + if (!m_volume->emboss_shape.has_value()) { + ImGui::Text("No embossed file"); + return; + } + + assert(m_volume->emboss_shape->svg_file.has_value()); + if (!m_volume->emboss_shape->svg_file.has_value()) { + ImGui::Text("Missing svg file in embossed shape"); + return; + } + + assert(m_volume->emboss_shape->svg_file->file_data != nullptr); + if (m_volume->emboss_shape->svg_file->file_data == nullptr) { + ImGui::Text("Missing data of svg file"); + return; + } + draw_preview(); + + draw_filename(); + // Is SVG baked? + if (m_volume == nullptr) + return; + ImGui::Separator(); + + ImGui::Indent(m_gui_cfg->icon_width); + draw_depth(); + draw_size(); + + m_can_use_surface = (m_volume->emboss_shape->projection.use_surface) ? true : // already used surface must have option to uncheck + !m_volume->is_the_only_one_part(); + if (m_can_use_surface) { + draw_use_surface(); + draw_distance(); + } + draw_rotation(); + draw_mirroring(); + //draw_face_the_camera(); + + ImGui::Unindent(m_gui_cfg->icon_width); + + if (!m_volume->is_the_only_one_part()) { + ImGui::Separator(); + draw_model_type(); + } + if (!m_can_use_surface) { + ImGui::Text("Tip:If you want to place svg file on another part surface,\nyou should select part first, and then drag svg file to the part surface."); + } +} + +void GLGizmoSVG::draw_preview() +{// init texture when not initialized yet. + // drag&drop is out of rendering scope so texture must be created on this place + if (m_texture.id == 0) { + const ExPolygonsWithIds &shapes = m_volume->emboss_shape->shapes_with_ids; + init_texture(m_texture, shapes, m_gui_cfg->texture_max_size_px, m_shape_warnings); + } + + //::draw(m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px); + + if (m_texture.id != 0) { + ImTextureID id = (void *) static_cast(m_texture.id); + ImVec2 s(m_texture.width, m_texture.height); + + std::optional spacing; + // is texture over full height? + if (m_texture.height != m_gui_cfg->texture_max_size_px) { + spacing = (m_gui_cfg->texture_max_size_px - m_texture.height) / 2.f; + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + *spacing); + } + // is texture over full width? + unsigned window_width = static_cast(ImGui::GetWindowSize().x - 2 * ImGui::GetStyle().WindowPadding.x); + if (window_width > m_texture.width) { + float space = (window_width - m_texture.width) / 2.f; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + space); + } + + ImGui::Image(id, s); + // if(ImGui::IsItemHovered()){ + // const EmbossShape &es = *m_volume->emboss_shape; + // size_t count_of_shapes = get_shapes_count(*es.svg_file.image); + // size_t count_of_expolygons = 0; + // size_t count_of_points = 0; + // for (const auto &shape : es.shapes_with_ids) { + // for (const ExPolygon &expoly : shape.expoly){ + // ++count_of_expolygons; + // count_of_points += count_points(expoly); + // } + // } + // // Do not translate it is only for debug + // std::string tooltip = GUI::format("%1% shapes, which create %2% polygons with %3% line segments", + // count_of_shapes, count_of_expolygons, count_of_points); + // ImGui::SetTooltip("%s", tooltip.c_str()); + //} + + if (spacing.has_value()) ImGui::SetCursorPosY(ImGui::GetCursorPosY() + *spacing); + } +} + +void GLGizmoSVG::draw_filename() +{ + const EmbossShape & es = *m_volume->emboss_shape; + const EmbossShape::SvgFile &svg = *es.svg_file; + if (m_filename_preview.empty()) { + // create filename preview + if (!svg.path.empty()) { + m_filename_preview = get_file_name(svg.path); + } else if (!svg.path_in_3mf.empty()) { + m_filename_preview = get_file_name(svg.path_in_3mf); + } + + if (m_filename_preview.empty()) + // TRN - Preview of filename after clear local filepath. + m_filename_preview = _u8L("Unknown filename"); + + m_filename_preview = ImGuiWrapper::trunc(m_filename_preview, m_gui_cfg->input_width); + } + + if (!m_shape_warnings.empty()) { + draw(get_icon(m_icons, IconType::exclamation, IconState::hovered)); + if (ImGui::IsItemHovered()) { + std::string tooltip; + for (const std::string &w : m_shape_warnings) { + if (w.empty()) continue; + if (!tooltip.empty()) tooltip += "\n"; + tooltip += w; + } + m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); + } + ImGui::SameLine(); + } + + // Remove space between filename and gray suffix ".svg" + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", m_filename_preview.c_str()); + bool is_hovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_GREY_LIGHT, ".svg"); + ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing + + is_hovered |= ImGui::IsItemHovered(); + if (is_hovered) { + std::string tooltip = GUI::format(_L("SVG file path is \"%1%\""), svg.path); + m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); + } + + bool file_changed = false; + + // Re-Load button + bool can_reload = !m_volume_shape.svg_file->path.empty(); + if (can_reload) { + ImGui::SameLine(); + if (draw_clickable(m_icons, IconType::refresh)) { + if (!boost::filesystem::exists(m_volume_shape.svg_file->path)) { + m_volume_shape.svg_file->path.clear(); + } else { + file_changed = true; + } + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Reload SVG file from disk."), m_gui_cfg->max_tooltip_width); + } + + std::string tooltip = ""; + ImGuiComboFlags flags = ImGuiComboFlags_PopupAlignLeft | ImGuiComboFlags_NoPreview; + ImGui::SameLine(); + ImGuiWrapper::push_combo_style(m_parent.get_scale()); + if (ImGui::BeginCombo("##file_options", nullptr, flags)) { + ScopeGuard combo_sg([]() { ImGui::EndCombo(); }); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {ImGui::GetStyle().FramePadding.x, 0}); + draw(get_icon(m_icons, IconType::change_file, IconState::hovered)); + ImGui::SameLine(); + if (ImGui::Selectable((_L("Change file") + dots).ToUTF8().data())) { + std::string new_path = choose_svg_file(); + if (!new_path.empty()) { + file_changed = true; + EmbossShape::SvgFile svg_file_new; + svg_file_new.path = new_path; + m_volume_shape.svg_file = svg_file_new; // clear data + } + } else if (ImGui::IsItemHovered()) { + tooltip = _u8L("Change to another .svg file"); + } + + //std::string forget_path = _u8L("Forget the file path"); + //if (m_volume->emboss_shape->svg_file->path.empty()) { + // draw(get_icon(m_icons, IconType::bake, IconState::disabled)); + // ImGui::SameLine(); + // m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, forget_path.c_str()); + //} else { + // draw(get_icon(m_icons, IconType::bake, IconState::hovered)); + // ImGui::SameLine(); + // if (ImGui::Selectable(forget_path.c_str())) { + // // set .svg_file.path_in_3mf to remember file name + // m_volume->emboss_shape->svg_file->path.clear(); + // m_volume_shape.svg_file->path.clear(); + // m_filename_preview.clear(); + // } else if (ImGui::IsItemHovered()) { + // tooltip = _u8L("Do NOT save local path to 3MF file.\n" + // "Also disables 'reload from disk' option."); + // } + //} + + draw(get_icon(m_icons, IconType::bake, IconState::hovered)); + ImGui::SameLine(); + // TRN: An menu option to convert the SVG into an unmodifiable model part. + if (ImGui::Selectable(_u8L("Bake to model").c_str())) { + m_volume->emboss_shape.reset(); + close(); + } else if (ImGui::IsItemHovered()) { + // TRN: Tooltip for the menu item. + tooltip = _u8L("Bake into model as uneditable part"); + } + + draw(get_icon(m_icons, IconType::save, IconState::activable)); + ImGui::SameLine(); + if (ImGui::Selectable((_L("Save as") + dots).ToUTF8().data())) { + wxWindow * parent = nullptr; + GUI::FileType file_type = FT_SVG; + wxString wildcard = file_wildcards(file_type); + wxString dlg_title = _L("Save SVG file"); + const EmbossShape::SvgFile &svg = *m_volume_shape.svg_file; + wxString dlg_file = from_u8(get_file_name(((!svg.path.empty()) ? svg.path : svg.path_in_3mf))) + ".svg"; + wxFileDialog dlg(parent, dlg_title, last_used_directory, dlg_file, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + if (dlg.ShowModal() == wxID_OK) { + last_used_directory = dlg.GetDirectory(); + wxString out_path = dlg.GetPath(); + std::string path{out_path.c_str()}; + // Slic3r::save(*m_volume_shape.svg_file.image, path); + + std::ofstream stream(path); + if (stream.is_open()) { + stream << *svg.file_data; + + // change source file + m_filename_preview.clear(); + m_volume_shape.svg_file->path = path; + m_volume_shape.svg_file->path_in_3mf.clear(); // possible change name + m_volume->emboss_shape->svg_file = m_volume_shape.svg_file; // copy - write changes into volume + } else { + BOOST_LOG_TRIVIAL(error) << "Opening file: \"" << path << "\" Failed"; + } + } + } else if (ImGui::IsItemHovered()) { + tooltip = _u8L("Save as '.svg' file"); + } + + // draw(get_icon(m_icons, IconType::save)); + // ImGui::SameLine(); + // if (ImGui::Selectable((_L("Save used as") + dots).ToUTF8().data())) { + // GUI::FileType file_type = FT_SVG; + // wxString wildcard = file_wildcards(file_type); + // wxString dlg_title = _L("Export SVG file:"); + // wxString dlg_dir = from_u8(wxGetApp().app_config->get_last_dir()); + // const EmbossShape::SvgFile& svg = m_volume_shape.svg_file; + // wxString dlg_file = from_u8(get_file_name(((!svg.path.empty()) ? svg.path : svg.path_in_3mf))) + ".svg"; + // wxFileDialog dlg(nullptr, dlg_title, dlg_dir, dlg_file, wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + // if (dlg.ShowModal() == wxID_OK ){ + // wxString out_path = dlg.GetPath(); + // std::string path{out_path.c_str()}; + // Slic3r::save(*m_volume_shape.svg_file.image, path); + // } + //} else if (ImGui::IsItemHovered()) { + // ImGui::SetTooltip("%s", _u8L("Save only used path as '.svg' file").c_str()); + //} + ImGui::PopStyleVar(1); + } + ImGuiWrapper::pop_combo_style(); + if (!tooltip.empty()) m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); + + if (file_changed) { + float scale = get_scale_for_tolerance(); + double tes_tol = get_tesselation_tolerance(scale); + EmbossShape es_ = select_shape(m_volume_shape.svg_file->path, tes_tol); + m_volume_shape.svg_file = std::move(es_.svg_file); + m_volume_shape.shapes_with_ids = std::move(es_.shapes_with_ids); + m_volume_shape.final_shape = {}; // clear cache + m_shape_warnings = create_shape_warnings(m_volume_shape, scale); + init_texture(m_texture, m_volume_shape.shapes_with_ids, m_gui_cfg->texture_max_size_px, m_shape_warnings); + process_job(); + } +} + +void GLGizmoSVG::draw_depth() +{ + ImGui::AlignTextToFramePadding(); + ImGuiWrapper::text(m_gui_cfg->translations.depth); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + double & value = m_volume_shape.projection.depth; + constexpr double step = 1.; + constexpr double step_fast = 10.; + std::optional result_scale; + const char * size_format = "%.2f mm"; + double input = value; + if (use_inch) { + size_format = "%.2f in"; + // input in inches + input *= GizmoObjectManipulation::mm_to_in * m_scale_depth.value_or(1.f); + result_scale = GizmoObjectManipulation::in_to_mm / m_scale_depth.value_or(1.f); + } else if (m_scale_depth.has_value()) { + // scale input + input *= (*m_scale_depth); + result_scale = 1. / (*m_scale_depth); + } + + if (ImGui::InputDouble("##depth", &input, step, step_fast, size_format)) { + if (result_scale.has_value()) + input *= (*result_scale); + apply(input, limits.depth); + if (!is_approx(input, value, 1e-4)) { + value = input; + process_job(); + } + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Size in emboss direction."), m_gui_cfg->max_tooltip_width); +} + +void GLGizmoSVG::draw_size() +{ + ImGui::AlignTextToFramePadding(); + ImGuiWrapper::text(m_gui_cfg->translations.size); + if (ImGui::IsItemHovered()) { + size_t count_points = 0; + for (const auto &s : m_volume_shape.shapes_with_ids) count_points += Slic3r::count_points(s.expoly); + // TRN: The placeholder contains a number. + m_imgui->tooltip(GUI::format(_L("Scale also changes amount of curve samples (%1%)"), count_points), m_gui_cfg->max_tooltip_width); + } + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + + Point size = m_shape_bb.size(); + double width = size.x() * m_volume_shape.scale * m_scale_width.value_or(1.f); + float ui_size_max = m_origin_shape_bb.size().x() * m_volume_shape.scale * 2; + if (use_inch) { + width *= GizmoObjectManipulation::mm_to_in; + //ui_size_max = 2 * width; + } + double height = size.y() * m_volume_shape.scale * m_scale_height.value_or(1.f); + if (use_inch) { height *= GizmoObjectManipulation::mm_to_in; } + + const auto is_valid_scale_ratio = [limit = &limits.relative_scale_ratio](double ratio) { + if (std::fabs(ratio - 1.) < limit->min) + return false; // too small ratio --> without effect + if (ratio > limit->max) + return false; + if (ratio < 1e-4) + return false; // negative scale is not allowed + return true; + }; + + std::optional new_relative_scale; + bool make_snap = false; + const MinMax &minmax = use_inch ? limits.ui_size_in : limits.ui_size; + if (m_keep_ratio) { + std::stringstream ss; + ss << _u8L("width:") << std::setprecision(2) << std::fixed << width << (use_inch ? "in" : "mm"); + std::stringstream ss_height; + ss_height << _u8L("height:") << std::setprecision(2) << std::fixed << height << (use_inch ? "in" : "mm"); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + float height_text_size_x = ImGui::CalcTextSize(ss_height.str().c_str()).x; + + // convert to float for slider + float width_f = static_cast(width); + if (m_imgui->slider_float("##width_size_slider", &width_f, minmax.min, ui_size_max, ss.str().c_str(), 1.f, false, _u8L("set width and height keep ratio with width"),false)) { + double width_ratio = width_f / width; + if (is_valid_scale_ratio(width_ratio)) { + m_scale_width = m_scale_width.value_or(1.f) * width_ratio; + m_scale_height = m_scale_height.value_or(1.f) * width_ratio; + new_relative_scale = Vec3d(width_ratio, width_ratio, 1.); + } + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(height_text_size_x); + m_imgui->text(ss_height.str()); + if (m_imgui->get_last_slider_status().deactivated_after_edit) + make_snap = true; // only last change of slider make snap + } else { + ImGuiInputTextFlags flags = 0; + + float space = m_gui_cfg->icon_width / 2; + float input_width = m_gui_cfg->input_width / 2 - space / 2; + float second_offset = m_gui_cfg->input_offset + input_width + space; + + const char *size_format = (use_inch) ? "%.2f in" : "%.1f mm"; + double step = -1.0; + double fast_step = -1.0; + + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(input_width); + double prev_width = width; + if (ImGui::InputDouble("##width", &width, step, fast_step, size_format, flags)) { + limit_value(width, (double) minmax.min, (double) MAX_NUM); + double width_ratio = width / prev_width; + if (is_valid_scale_ratio(width_ratio)) { + m_scale_width = m_scale_width.value_or(1.f) * width_ratio; + new_relative_scale = Vec3d(width_ratio, 1., 1.); + make_snap = true; + } + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Width of SVG."), m_gui_cfg->max_tooltip_width); + + ImGui::SameLine(second_offset); + ImGui::SetNextItemWidth(input_width); + double prev_height = height; + if (ImGui::InputDouble("##height", &height, step, fast_step, size_format, flags)) { + limit_value(height, (double) minmax.min, (double) MAX_NUM); + double height_ratio = height / prev_height; + if (is_valid_scale_ratio(height_ratio)) { + m_scale_height = m_scale_height.value_or(1.f) * height_ratio; + new_relative_scale = Vec3d(1., height_ratio, 1.); + make_snap = true; + } + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Height of SVG."), m_gui_cfg->max_tooltip_width); + } + + // Lock on ratio m_keep_ratio + ImGui::SameLine(m_gui_cfg->lock_offset); + const IconManager::Icon &icon = get_icon(m_icons, m_keep_ratio ? IconType::lock : IconType::unlock, IconState::activable); + const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_ratio ? IconType::lock : IconType::unlock, IconState::hovered); + if (button(icon, icon_hover, icon)) + m_keep_ratio = !m_keep_ratio; + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Lock/unlock the aspect ratio of the SVG."), m_gui_cfg->max_tooltip_width); + + // reset button + bool can_reset = m_scale_width.has_value() || m_scale_height.has_value(); + if (can_reset) { + if (reset_button(m_icons)) { + new_relative_scale = Vec3d(1. / m_scale_width.value_or(1.f), 1. / m_scale_height.value_or(1.f), 1.); + make_snap = true; + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Reset scale"), m_gui_cfg->max_tooltip_width); + } + + if (new_relative_scale.has_value()) { + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_scale_fnc = [&selection, rel_scale = *new_relative_scale]() { selection.scale(rel_scale, get_drag_transformation_type(selection)); }; + selection_transform(selection, selection_scale_fnc); + + std::string snap_name; // Empty mean do not store on undo/redo stack + m_parent.do_scale(snap_name); + wxGetApp().obj_manipul()->set_dirty(); + // should be the almost same + calculate_scale(); + + const NSVGimage *img = m_volume_shape.svg_file->image.get(); + assert(img != NULL); + if (img != NULL) { + NSVGLineParams params{get_tesselation_tolerance(get_scale_for_tolerance())}; + m_volume_shape.shapes_with_ids = create_shape_with_ids(*img, params); + m_volume_shape.final_shape = {}; // reset cache for final shape + if (!make_snap) // Be carefull: Last change may be without change of scale + process_job(false); + } + } + if (make_snap) + process_job(); // make undo/redo snap-shot} +} + +void GLGizmoSVG::draw_use_surface() +{ + m_imgui->disabled_begin(!m_can_use_surface); + ScopeGuard sc([imgui = m_imgui]() { imgui->disabled_end(); }); + + ImGui::AlignTextToFramePadding(); + ImGuiWrapper::text(m_gui_cfg->translations.use_surface); + ImGui::SameLine(m_gui_cfg->input_offset); + if (Emboss::UpdateSurfaceVolumeJob::is_use_surfae_error) { + Emboss::UpdateSurfaceVolumeJob::is_use_surfae_error = false; + m_volume_shape.projection.use_surface = false; + } + if (m_imgui->bbl_checkbox("##useSurface", m_volume_shape.projection.use_surface)) { + process_job(); + } +} + +void GLGizmoSVG::draw_distance() +{ + const EmbossProjection &projection = m_volume->emboss_shape->projection; + bool use_surface = projection.use_surface; + bool allowe_surface_distance = !use_surface && !m_volume->is_the_only_one_part(); + + float prev_distance = m_distance.value_or(.0f); + float min_distance = static_cast(-2 * projection.depth); + float max_distance = static_cast(2 * projection.depth); + + m_imgui->disabled_begin(!allowe_surface_distance); + ScopeGuard sg([imgui = m_imgui]() { imgui->disabled_end(); }); + + ImGui::AlignTextToFramePadding(); + ImGuiWrapper::text(m_gui_cfg->translations.distance); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + const wxString move_tooltip = _L("Distance of the center of the SVG to the model surface."); + bool is_moved = false; + if (use_inch) { + std::optional distance_inch; + if (m_distance.has_value()) distance_inch = (*m_distance * GizmoObjectManipulation::mm_to_in); + min_distance = static_cast(min_distance * GizmoObjectManipulation::mm_to_in); + max_distance = static_cast(max_distance * GizmoObjectManipulation::mm_to_in); + if (m_imgui->slider_optional_float("##distance", m_distance, min_distance, max_distance, "%.3f in", 1.f, false, move_tooltip)) { + limit_value(*m_distance, min_distance, max_distance); + if (distance_inch.has_value()) { + m_distance = *distance_inch * GizmoObjectManipulation::in_to_mm; + } else { + m_distance.reset(); + } + is_moved = true; + } + } else { + if (m_imgui->slider_optional_float("##distance", m_distance, min_distance, max_distance, "%.2f mm", 1.f, false, move_tooltip)) { + limit_value(*m_distance, min_distance, max_distance); + is_moved = true; + } + } + bool is_stop_sliding = m_imgui->get_last_slider_status().deactivated_after_edit; + bool is_reseted = false; + if (m_distance.has_value()) { + if (reset_button(m_icons)) { + m_distance.reset(); + is_reseted = true; + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Reset distance"), m_gui_cfg->max_tooltip_width); + } + + if (is_moved || is_reseted) + do_local_z_move(m_parent.get_selection(), m_distance.value_or(.0f) - prev_distance); + if (is_stop_sliding || is_reseted) + m_parent.do_move(move_snapshot_name); +} + +void GLGizmoSVG::draw_rotation() +{ + const EmbossProjection &projection = m_volume->emboss_shape->projection; + bool use_surface = projection.use_surface; + bool allowe_surface_distance = !use_surface && !m_volume->is_the_only_one_part(); + m_imgui->disabled_begin(!allowe_surface_distance); + ImGui::AlignTextToFramePadding(); + ImGuiWrapper::text(m_gui_cfg->translations.rotation); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + // slider for Clock-wise angle in degress + // stored angle is optional CCW and in radians + // Convert stored value to degress + // minus create clock-wise roation from CCW + float angle = m_angle.value_or(0.f); + float angle_deg = static_cast(-angle * 180 / M_PI); + if (m_imgui->slider_float("##angle", &angle_deg, limits.angle.min, limits.angle.max, u8"%.2f °", 1.f, false, _L("Rotate text Clock-wise."))) { + // convert back to radians and CCW + double angle_rad = -angle_deg * M_PI / 180.0; + Geometry::to_range_pi_pi(angle_rad); + + double diff_angle = angle_rad - angle; + + do_local_z_rotate(m_parent.get_selection(), diff_angle); + + // calc angle after rotation + m_angle = calc_angle(m_parent.get_selection()); + + // recalculate for surface cut + if (m_volume->emboss_shape->projection.use_surface) + process_job(); + } + bool is_stop_sliding = m_imgui->get_last_slider_status().deactivated_after_edit; + + // Reset button + bool is_reseted = false; + if (m_angle.has_value()) { + if (reset_button(m_icons)) { + do_local_z_rotate(m_parent.get_selection(), -(*m_angle)); + m_angle.reset(); + + // recalculate for surface cut + if (m_volume->emboss_shape->projection.use_surface) + process_job(); + + is_reseted = true; + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Reset rotation"), m_gui_cfg->max_tooltip_width); + } + + // Apply rotation on model (backend) + if (is_stop_sliding || is_reseted) m_parent.do_rotate(rotation_snapshot_name); + + // Keep up - lock button icon + if (!m_volume->is_the_only_one_part()) { + ImGui::SameLine(m_gui_cfg->lock_offset); + const IconManager::Icon &icon = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::activable); + const IconManager::Icon &icon_hover = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::hovered); + if (button(icon, icon_hover, icon)) + m_keep_up = !m_keep_up; + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Lock/unlock rotation angle when dragging above the surface."), m_gui_cfg->max_tooltip_width); + } + m_imgui->disabled_end(); +} + +void GLGizmoSVG::draw_mirroring() +{ + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", m_gui_cfg->translations.mirror.c_str()); + ImGui::SameLine(m_gui_cfg->input_offset); + Axis axis = Axis::UNKNOWN_AXIS; + if (draw_clickable(m_icons, IconType::reflection_x)) { + axis = Axis::X; + } else if (ImGui::IsItemHovered()) { + m_imgui->tooltip(_u8L("Mirror vertically"), m_gui_cfg->max_tooltip_width); + } + + ImGui::SameLine(); + if (draw_clickable(m_icons, IconType::reflection_y)) { + axis = Axis::Y; + } else if (ImGui::IsItemHovered()) { + m_imgui->tooltip(_u8L("Mirror horizontally"), m_gui_cfg->max_tooltip_width); + } + + if (axis != Axis::UNKNOWN_AXIS) { + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_mirror_fnc = [&selection, &axis]() { selection.mirror(axis, get_drag_transformation_type(selection)); }; + selection_transform(selection, selection_mirror_fnc); + m_parent.do_mirror(L("Set Mirror")); + + // Mirror is ignoring keep up !! + if (m_keep_up) m_angle = calc_angle(selection); + + volume_transformation_changed(); + + if (m_volume_shape.projection.use_surface) + process_job(); + } +} + +void GLGizmoSVG::draw_face_the_camera() +{ + if (ImGui::Button(_u8L("Face the camera").c_str())) { + const Camera &cam = wxGetApp().plater()->get_camera(); + auto wanted_up_limit = (m_keep_up) ? std::optional(UP_LIMIT) : std::optional{}; + if (face_selected_volume_to_camera(cam, m_parent, wanted_up_limit)) + volume_transformation_changed(); + } +} + +void GLGizmoSVG::draw_model_type() +{ + ImGui::AlignTextToFramePadding(); + bool is_last_solid_part = m_volume->is_the_only_one_part(); + std::string title = _u8L("Operation"); + if (is_last_solid_part) { + ImVec4 color{.5f, .5f, .5f, 1.f}; + m_imgui->text_colored(color, title.c_str()); + } else { + ImGui::Text("%s", title.c_str()); + } + + std::optional new_type; + ModelVolumeType modifier = ModelVolumeType::PARAMETER_MODIFIER; + ModelVolumeType negative = ModelVolumeType::NEGATIVE_VOLUME; + ModelVolumeType part = ModelVolumeType::MODEL_PART; + ModelVolumeType type = m_volume->type(); + + // TRN EmbossOperation + ImGuiWrapper::push_radio_style(); + if (ImGui::RadioButton(_u8L("Join").c_str(), type == part)) + new_type = part; + else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Click to change text into object part."), m_gui_cfg->max_tooltip_width); + ImGui::SameLine(); + + std::string last_solid_part_hint = _u8L("You can't change a type of the last solid part of the object."); + if (ImGui::RadioButton(_CTX_utf8(L_CONTEXT("Cut", "EmbossOperation"), "EmbossOperation").c_str(), type == negative)) + new_type = negative; + else if (ImGui::IsItemHovered()) { + if (is_last_solid_part) + m_imgui->tooltip(last_solid_part_hint, m_gui_cfg->max_tooltip_width); + else if (type != negative) + m_imgui->tooltip(_u8L("Click to change part type into negative volume."), m_gui_cfg->max_tooltip_width); + } + + // In simple mode are not modifiers + if (wxGetApp().plater()->printer_technology() != ptSLA && wxGetApp().get_mode() != ConfigOptionMode::comSimple) { + ImGui::SameLine(); + if (ImGui::RadioButton(_u8L("Modifier").c_str(), type == modifier)) + new_type = modifier; + else if (ImGui::IsItemHovered()) { + if (is_last_solid_part) + m_imgui->tooltip(last_solid_part_hint, m_gui_cfg->max_tooltip_width); + else if (type != modifier) + m_imgui->tooltip(_u8L("Click to change part type into modifier."), m_gui_cfg->max_tooltip_width); + } + } + ImGuiWrapper::pop_radio_style(); + + if (m_volume != nullptr && new_type.has_value() && !is_last_solid_part) { + GUI_App &app = wxGetApp(); + Plater * plater = app.plater(); + // TRN: This is the name of the action that shows in undo/redo stack (changing part type from SVG to something else). + Plater::TakeSnapshot snapshot(plater, _u8L("Change SVG Type"), UndoRedo::SnapshotType::GizmoAction); + m_volume->set_type(*new_type); + + bool is_volume_move_inside = (type == part); + bool is_volume_move_outside = (*new_type == part); + // Update volume position when switch (from part) or (into part) + if ((is_volume_move_inside || is_volume_move_outside)) + process_job(); + + // inspiration in ObjectList::change_part_type() + // how to view correct side panel with objects + ObjectList * obj_list = app.obj_list(); + wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(obj_list->get_selected_obj_idx(), + [volume = m_volume](const ModelVolume *vol) { + return vol == volume; + }); + if (!sel.IsEmpty()) + obj_list->select_item(sel.front()); + + // NOTE: on linux, function reorder_volumes_and_get_selection call GLCanvas3D::reload_scene(refresh_immediately = false) + // which discard m_volume pointer and set it to nullptr also selection is cleared so gizmo is automaticaly closed + auto &mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() != GLGizmosManager::Svg) + mng.open_gizmo(GLGizmosManager::Svg); + // TODO: select volume back - Ask @Sasa + } +} + +void GLGizmoSVG::volume_transformation_changed() +{ + if (m_volume == nullptr || !m_volume->emboss_shape.has_value()) { + assert(false); + return; + } + + if (!m_keep_up) { + // update current style + m_angle = calc_angle(m_parent.get_selection()); + } else { + // angle should be the same + assert(is_approx(m_angle, calc_angle(m_parent.get_selection()))); + } + + // Update surface by new position + if (m_volume->emboss_shape->projection.use_surface) { + process_job(); + } else { + // inform slicing process that model changed + // SLA supports, processing + // ensure on bed + wxGetApp().plater()->changed_object(*m_volume->get_object()); + } + + // Show correct value of height & depth inside of inputs + calculate_scale(); +} + +bool GLGizmoSVG::on_mouse_for_rotation(const wxMouseEvent &mouse_event) +{ + if (mouse_event.Moving()) + return false; + + bool used = use_grabbers(mouse_event); + if (!m_dragging) + return used; + + if (mouse_event.Dragging()) + dragging_rotate_gizmo(m_rotate_gizmo.get_angle(), m_angle, m_rotate_start_angle, m_parent.get_selection()); + + return used; +} + +bool GLGizmoSVG::on_mouse_for_translate(const wxMouseEvent &mouse_event) +{ // exist selected volume? + if (m_volume == nullptr) + return false; + + auto up_limit = m_keep_up ? std::optional(UP_LIMIT) : std::optional{}; + const Camera &camera = wxGetApp().plater()->get_camera(); + + bool was_dragging = m_surface_drag.has_value(); + bool res = on_mouse_surface_drag(mouse_event, camera, m_surface_drag, m_parent, m_raycast_manager, up_limit); + bool is_dragging = m_surface_drag.has_value(); + + // End with surface dragging? + if (was_dragging && !is_dragging) { + // Update surface by new position + if (m_volume->emboss_shape->projection.use_surface) + process_job(); + + // TODO: Remove it when it will be stable + // Distance should not change during dragging + const GLVolume *gl_volume = m_parent.get_selection().get_first_volume(); + m_distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + // Show correct value of height & depth inside of inputs + calculate_scale(); + } + // Start with dragging + else if (!was_dragging && is_dragging) { + // Cancel job to prevent interuption of dragging (duplicit result) + //if (m_job_cancel != nullptr) + // m_job_cancel->store(true); + } + // during drag + else if (was_dragging && is_dragging) { + // update scale of selected volume --> should be approx the same + calculate_scale(); + // Recalculate angle for GUI + if (!m_keep_up) + m_angle = calc_angle(m_parent.get_selection()); + } + return res; +} + +void GLGizmoSVG::register_single_mesh_pick() +{ + std::map>().swap(m_mesh_raycaster_map); + Selection & selection = m_parent.get_selection(); + int object_idx; + ModelObject * mo = selection.get_selected_single_object(object_idx); + std::vector volume_idxs = selection.get_volume_idxs_from_object(object_idx); + if (volume_idxs.empty()) { + m_raycast_condition.clear(); + m_raycast_manager.clear(); + return; + } + for (unsigned int idx : volume_idxs) { + GLVolume *v = const_cast(selection.get_volume(idx)); + if (v == m_svg_volume) { + continue; + } + auto world_tran = v->get_instance_transformation() * v->get_volume_transformation(); + auto mesh = const_cast(v->ori_mesh); + if (m_mesh_raycaster_map.find(v) != m_mesh_raycaster_map.end()) { + m_mesh_raycaster_map[v]->world_tran.set_from_transform(world_tran.get_matrix()); + } else { + m_mesh_raycaster_map[v] = std::make_shared(mesh, -1); + m_mesh_raycaster_map[v]->world_tran.set_from_transform(world_tran.get_matrix()); + } + } + + const ModelInstance * mi = mo->instances[selection.get_instance_idx()]; + if (m_svg_volume) { + const ModelVolume *svg_mv = get_model_volume(*m_svg_volume, *mo); + m_raycast_condition = create_condition(mo->volumes, svg_mv->id()); + } + else { + m_raycast_condition = create_condition(mo->volumes, -1); + } + + RaycastManager::Meshes meshes; // = create_meshes(input.canvas, cond); + meshes.reserve(m_mesh_raycaster_map.size()); + for (auto iter : m_mesh_raycaster_map) { + auto gl_volume = iter.first; + const ModelVolume *mv = get_model_volume(*gl_volume, *mo); + size_t id = mv->id().id; + auto mesh = std::make_unique(iter.second->mesh_raycaster->get_aabb_mesh()); + meshes.emplace_back(std::make_pair(id, std::move(mesh))); + } + m_raycast_manager.actualize(*mi, &m_raycast_condition, &meshes); // input.raycaster.actualize(*instance, &cond, &meshes); +} + +bool GLGizmoSVG::gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_position, bool shift_down, bool alt_down, bool control_down) +{ + if (m_volume && m_volume->is_the_only_one_part()){ + return false; + } + const Selection & selection = m_parent.get_selection(); + if (action == SLAGizmoEventType::Moving) { + + } else if (action == SLAGizmoEventType::LeftDown) { + if (!selection.is_empty() && get_hover_id() != -1) { + //start_dragging(); + return true; + } + } + return true; +} + +void GLGizmoSVG::update_single_mesh_pick(GLVolume *v) +{ + if (m_mesh_raycaster_map.find(v) != m_mesh_raycaster_map.end()) { + auto world_tran = v->get_instance_transformation() * v->get_volume_transformation(); + m_mesh_raycaster_map[v]->world_tran.set_from_transform(world_tran.get_matrix()); + } +} + +}} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp new file mode 100644 index 000000000..945791d97 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp @@ -0,0 +1,166 @@ +#ifndef slic3r_GLGizmoSVG_hpp_ +#define slic3r_GLGizmoSVG_hpp_ + +#include "GLGizmoRotate.hpp" +#include "slic3r/GUI/SurfaceDrag.hpp" +#include "slic3r/GUI/GLTexture.hpp" + +#include "slic3r/GUI/IconManager.hpp" +//BBS: add size adjust related +#include "libslic3r/Model.hpp" +#include "slic3r/GUI/Jobs/EmbossJob.hpp" +namespace Slic3r { +class ModelVolume; +enum class ModelVolumeType : int; +} // namespace Slic3r + +namespace Slic3r { +namespace GUI { +//#define DEBUG_SVG +struct Camera; +class Worker; +enum class SLAGizmoEventType : unsigned char; +class GLGizmoSVG : public GLGizmoBase +{ + Emboss::DataBasePtr create_emboss_data_base(std::shared_ptr> &cancel, ModelVolumeType volume_type, std::string_view filepath); + Emboss::CreateVolumeParams create_input(GLCanvas3D &canvas, ModelVolumeType volume_type); + + +public: + GLGizmoSVG(GLCanvas3D& parent, unsigned int sprite_id); + virtual ~GLGizmoSVG() = default; + + std::string get_tooltip() const override; + void data_changed(bool is_serializing) override; + void on_set_hover_id() override { m_rotate_gizmo.set_hover_id(m_hover_id); } + void on_enable_grabber(unsigned int id) override; + void on_disable_grabber(unsigned int id) override; + + bool create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type = ModelVolumeType::MODEL_PART); + /// + /// Check whether volume is object containing only emboss volume + /// + /// Pointer to volume + /// True when object otherwise False + static bool is_svg_object(const ModelVolume &volume); + + /// + /// Check whether volume has emboss data + /// + /// Pointer to volume + /// True when constain emboss data otherwise False + static bool is_svg(const ModelVolume &volume); + + void register_single_mesh_pick(); + + void update_single_mesh_pick(GLVolume *v); + bool gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_position, bool shift_down, bool alt_down, bool control_down); + +protected: + bool on_is_selectable() const override { return false; } + virtual bool on_init() override; + virtual std::string on_get_name() const override; + std::string on_get_name_str() override { return "Move"; } + virtual bool on_is_activable() const override; + virtual void on_set_state() override; + virtual void on_start_dragging() override; + virtual void on_stop_dragging() override; + /// + /// Rotate by text on dragging rotate grabers + /// + /// Information about mouse + /// Propagete normaly return false. + bool on_mouse(const wxMouseEvent &mouse_event) override; + virtual void on_update(const UpdateData& data) override; + virtual void on_render() override; + virtual void on_render_for_picking() override; + virtual void on_render_input_window(float x, float y, float bottom_limit); + +private: + void set_volume_by_selection(); + void delete_texture(Emboss::Texture &texture); + void reset_volume(); + void calculate_scale(); + float get_scale_for_tolerance(); + std::vector create_shape_warnings(const EmbossShape &shape, float scale); + + bool process_job(bool make_snapshot = true); // process(bool make_snapshot = true) + void close(); + void draw_window(); + bool init_texture(Emboss::Texture &texture, const ExPolygonsWithIds &shapes_with_ids, unsigned max_size_px, const std::vector &shape_warnings); + void draw_preview(); + void draw_filename(); + void draw_depth(); + void draw_size(); + void draw_use_surface(); + void draw_distance(); + void draw_rotation(); + void draw_mirroring(); + void draw_face_the_camera(); + void draw_model_type(); + + void volume_transformation_changed(); + + // process mouse event + bool on_mouse_for_rotation(const wxMouseEvent &mouse_event); + bool on_mouse_for_translate(const wxMouseEvent &mouse_event); + +private: + struct GuiCfg; + std::unique_ptr m_gui_cfg; + // actual selected only one volume - with emboss data + ModelVolume *m_volume = nullptr; + const GLVolume * m_svg_volume{nullptr}; + // Is used to edit eboss and send changes to job + // Inside volume is current state of shape WRT Volume + EmbossShape m_volume_shape; // copy from m_volume for edit + // same index as volumes in + std::vector m_shape_warnings; + // cancel for previous update of volume to cancel finalize part + std::shared_ptr> m_job_cancel = nullptr; + // When work with undo redo stack there could be situation that + // m_volume point to unexisting volume so One need also objectID + ObjectID m_volume_id; + // move gizmo + Grabber m_move_grabber; + // Rotation gizmo + GLGizmoRotate m_rotate_gizmo; + std::optional m_angle; + std::optional m_distance; + + bool m_can_use_surface; + // Value is set only when dragging rotation to calculate actual angle + std::optional m_rotate_start_angle; + // TODO: it should be accessible by other gizmo too. + // May be move to plater? + RaycastManager m_raycast_manager; + RaycastManager::AllowVolumes m_raycast_condition; + std::map> m_mesh_raycaster_map;//for text + // When true keep up vector otherwise relative rotation + bool m_keep_up = true; + // Keep size aspect ratio when True. + bool m_keep_ratio = true; + // Keep data about dragging only during drag&drop + std::optional m_surface_drag; + // For volume on scaled objects + std::optional m_scale_width; + std::optional m_scale_height; + std::optional m_scale_depth; + + // keep SVG data rendered on GPU + Emboss::Texture m_texture; + // bounding box of shape + // Note: Scaled mm to int value by m_volume_shape.scale + BoundingBox m_shape_bb; + BoundingBox m_origin_shape_bb; + std::string m_filename_preview; + IconManager m_icon_manager; + IconManager::VIcons m_icons; +}; + + + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoMove_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 06c495e09..77a47dd25 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -24,6 +24,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" #include "slic3r/GUI/Gizmos/GLGizmoText.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp" #include "slic3r/GUI/Gizmos/GLGizmoAssembly.hpp" @@ -222,6 +223,7 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoFdmSupports(m_parent, m_is_dark ? "toolbar_support_dark.svg" : "toolbar_support.svg", EType::FdmSupports)); m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam)); m_gizmos.emplace_back(new GLGizmoText(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Text)); + m_gizmos.emplace_back(new GLGizmoSVG(m_parent, EType::Svg)); m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmuSegmentation)); m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure)); m_gizmos.emplace_back(new GLGizmoAssembly(m_parent, m_is_dark ? "toolbar_assembly_dark.svg" : "toolbar_assembly.svg", EType::Assembly)); @@ -377,6 +379,10 @@ bool GLGizmosManager::open_gizmo(EType type) return false; } +bool GLGizmosManager::open_gizmo(unsigned char type) +{ + return open_gizmo((EType)type); +} bool GLGizmosManager::check_gizmos_closed_except(EType type) const { @@ -766,6 +772,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[MmuSegmentation].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Text) return dynamic_cast(m_gizmos[Text].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == Svg) + return dynamic_cast(m_gizmos[Svg].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Measure) return dynamic_cast(m_gizmos[Measure].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Assembly) @@ -928,7 +936,7 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) // mouse anywhere if (evt.Moving()) { m_tooltip = update_hover_state(mouse_pos); - if (m_current == MmuSegmentation || m_current == FdmSupports || m_current == Text || m_current == BrimEars) + if (m_current == MmuSegmentation || m_current == FdmSupports || m_current == Text || m_current == BrimEars || m_current == Svg) // BBS gizmo_event(SLAGizmoEventType::Moving, mouse_pos, evt.ShiftDown(), evt.AltDown(), evt.ControlDown()); } else if (evt.LeftUp()) { @@ -1091,7 +1099,7 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) m_tooltip.clear(); if (evt.LeftDown() && (!control_down || grabber_contains_mouse())) { - if ((m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || + if ((m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Svg || m_current == Seam || m_current == MmuSegmentation || m_current == Text || m_current == Cut || m_current == MeshBoolean || m_current == BrimEars) && gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, evt.ShiftDown(), evt.AltDown())) // the gizmo got the event and took some action, there is no need to do anything more @@ -1155,6 +1163,10 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) // in case SLA/FDM gizmo is selected, we just pass the LeftUp event and stop processing - neither // object moving or selecting is suppressed in that case processed = true; + } else if (evt.LeftUp() && m_current == Svg && m_gizmos[m_current]->get_hover_id() != -1) { + // BBS + // wxGetApp().obj_manipul()->set_dirty(); + processed = true; } else if (evt.LeftUp() && m_current == Flatten && m_gizmos[m_current]->get_hover_id() != -1) { // to avoid to loose the selection when user clicks an the white faces of a different object while the Flatten gizmo is active @@ -1714,13 +1726,14 @@ void GLGizmosManager::do_render_overlay() const GLTexture::render_sub_texture(icons_texture_id, zoomed_top_x, zoomed_top_x + zoomed_icons_size, zoomed_top_y - zoomed_icons_size, zoomed_top_y, { { u_left, v_bottom }, { u_right, v_bottom }, { u_right, v_top }, { u_left, v_top } }); - if (idx == m_current) { + if (idx == m_current// Orca: Show Svg dialog at the same place as emboss gizmo + || (m_current == Svg && idx == Text)) { //BBS: GUI refactor: GLToolbar&&Gizmo adjust //render_input_window uses a different coordination(imgui) //1. no need to scale by camera zoom, set {0,0} at left-up corner for imgui #if BBS_TOOLBAR_ON_TOP //gizmo->render_input_window(width, 0.5f * cnv_h - zoomed_top_y * zoom, toolbar_top); - gizmo->render_input_window(0.5 * cnv_w + zoomed_top_x * zoom, height, cnv_h); + m_gizmos[m_current]->render_input_window(0.5 * cnv_w + zoomed_top_x * zoom, height, cnv_h); is_render_current = true; #else diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 20ebe66a7..f95b30f59 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -77,6 +77,7 @@ public: Seam, // BBS Text, + Svg, MmuSegmentation, Measure, Assembly, @@ -229,6 +230,7 @@ public: void reset_all_states(); bool is_serializing() const { return m_serializing; } bool open_gizmo(EType type); + bool open_gizmo(unsigned char type); bool check_gizmos_closed_except(EType) const; void set_hover_id(int id); diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp index aa2f0d5cb..616d7ff11 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp @@ -18,8 +18,7 @@ #include -#define MAX_NUM 9999.99 -#define MAX_SIZE "9999.99" + namespace Slic3r { diff --git a/src/slic3r/GUI/IconManager.cpp b/src/slic3r/GUI/IconManager.cpp new file mode 100644 index 000000000..15d242dcd --- /dev/null +++ b/src/slic3r/GUI/IconManager.cpp @@ -0,0 +1,413 @@ +#include "IconManager.hpp" +#include +#include +#include +#include +#include +#include "nanosvg/nanosvg.h" +#include "nanosvg/nanosvgrast.h" +#include "libslic3r/Utils.hpp" // ScopeGuard + +#include "3DScene.hpp" // glsafe +#include "GL/glew.h" + +#define STB_RECT_PACK_IMPLEMENTATION +#include "imgui/imstb_rectpack.h" // distribute rectangles + +using namespace Slic3r::GUI; + +namespace priv { +// set shared pointer to point on bad texture +static void clear(IconManager::Icons &icons); +static const std::vector>& get_states(IconManager::RasterType type); +static void draw_transparent_icon(const IconManager::Icon &icon); // only help function +} + +IconManager::~IconManager() { + priv::clear(m_icons); + // release opengl texture is made in ~GLTexture() + + if (m_id != 0) + glsafe(::glDeleteTextures(1, &m_id)); +} + +namespace { +NSVGimage *parse_file(const char * filepath) { + FILE *fp = boost::nowide::fopen(filepath, "rb"); + assert(fp != nullptr); + if (fp == nullptr) + return nullptr; + + Slic3r::ScopeGuard sg([fp]() { fclose(fp); }); + + fseek(fp, 0, SEEK_END); + size_t size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + // Note: +1 is for null termination + auto data_ptr = std::make_unique(size+1); + data_ptr[size] = '\0'; // Must be null terminated. + + size_t readed_size = fread(data_ptr.get(), 1, size, fp); + assert(readed_size == size); + if (readed_size != size) + return nullptr; + + return nsvgParse(data_ptr.get(), "px", 96.0f); +} + +void subdata(unsigned char *data, size_t data_stride, const std::vector &data2, size_t data2_row) { + assert(data_stride >= data2_row); + for (size_t data2_offset = 0, data_offset = 0; + data2_offset < data2.size(); + data2_offset += data2_row, data_offset += data_stride) + ::memcpy((void *)(data + data_offset), (const void *)(data2.data() + data2_offset), data2_row); +} +} + +IconManager::Icons IconManager::init(const InitTypes &input) +{ + assert(!input.empty()); + if (input.empty()) + return {}; + + // TODO: remove in future + if (m_id != 0) { + glsafe(::glDeleteTextures(1, &m_id)); + m_id = 0; + } + + int total_surface = 0; + for (const InitType &i : input) + total_surface += i.size.x * i.size.y; + const int surface_sqrt = (int)sqrt((float)total_surface) + 1; + + // Start packing + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + const int TEX_HEIGHT_MAX = 1024 * 32; + int width = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512; + + int num_nodes = width; + std::vector nodes(num_nodes); + stbrp_context context; + stbrp_init_target(&context, width, TEX_HEIGHT_MAX, nodes.data(), num_nodes); + + ImVector pack_rects; + pack_rects.resize(input.size()); + memset(pack_rects.Data, 0, (size_t) pack_rects.size_in_bytes()); + for (size_t i = 0; i < input.size(); i++) { + const ImVec2 &size = input[i].size; + assert(size.x > 1); + assert(size.y > 1); + pack_rects[i].w = size.x; + pack_rects[i].h = size.y; + } + int pack_rects_res = stbrp_pack_rects(&context, &pack_rects[0], pack_rects.Size); + assert(pack_rects_res == 1); + if (pack_rects_res != 1) + return {}; + + ImVec2 tex_size(width, width); + for (const stbrp_rect &rect : pack_rects) { + float x = rect.x + rect.w; + float y = rect.y + rect.h; + if(x > tex_size.x) tex_size.x = x; + if(y > tex_size.y) tex_size.y = y; + } + + Icons result(input.size()); + for (int i = 0; i < pack_rects.Size; i++) { + const stbrp_rect &rect = pack_rects[i]; + assert(rect.was_packed); + if (!rect.was_packed) + return {}; + + ImVec2 tl(rect.x / tex_size.x, rect.y / tex_size.y); + ImVec2 br((rect.x + rect.w) / tex_size.x, (rect.y + rect.h) / tex_size.y); + + assert(input[i].size.x == rect.w); + assert(input[i].size.y == rect.h); + Icon icon = {input[i].size, tl, br}; + result[i] = std::make_shared(std::move(icon)); + } + + NSVGrasterizer *rast = nsvgCreateRasterizer(); + assert(rast != nullptr); + if (rast == nullptr) + return {}; + ScopeGuard sg_rast([rast]() { ::nsvgDeleteRasterizer(rast); }); + + int channels = 4; + int n_pixels = tex_size.x * tex_size.y; + // store data for whole texture + std::vector data(n_pixels * channels, {0}); + + // initialize original index locations + std::vector idx(input.size()); + std::iota(idx.begin(), idx.end(), 0); + + // Group same filename by sort inputs + // sort indexes based on comparing values in input + std::sort(idx.begin(), idx.end(), [&input](size_t i1, size_t i2) { return input[i1].filepath < input[i2].filepath; }); + for (size_t j: idx) { + const InitType &i = input[j]; + if (i.filepath.empty()) + continue; // no file path only reservation of space for texture + assert(boost::filesystem::exists(i.filepath)); + if (!boost::filesystem::exists(i.filepath)) + continue; + assert(boost::algorithm::iends_with(i.filepath, ".svg")); + if (!boost::algorithm::iends_with(i.filepath, ".svg")) + continue; + + NSVGimage *image = parse_file(i.filepath.c_str()); + assert(image != nullptr); + if (image == nullptr) + return {}; + + ScopeGuard sg_image([image]() { ::nsvgDelete(image); }); + + float svg_scale = i.size.y / image->height; + // scale should be same in both directions + assert(is_approx(svg_scale, i.size.y / image->width)); + + const stbrp_rect &rect = pack_rects[j]; + int n_pixels = rect.w * rect.h; + std::vector icon_data(n_pixels * channels, {0}); + ::nsvgRasterize(rast, image, 0, 0, svg_scale, icon_data.data(), i.size.x, i.size.y, i.size.x * channels); + + // makes white or gray only data in icon + if (i.type == RasterType::white_only_data || + i.type == RasterType::gray_only_data) { + unsigned char value = (i.type == RasterType::white_only_data) ? 255 : 127; + for (size_t k = 0; k < icon_data.size(); k += channels) + if (icon_data[k] != 0 || icon_data[k + 1] != 0 || icon_data[k + 2] != 0) { + icon_data[k] = value; + icon_data[k + 1] = value; + icon_data[k + 2] = value; + } + } + + int start_offset = (rect.y*tex_size.x + rect.x) * channels; + int data_stride = tex_size.x * channels; + subdata(data.data() + start_offset, data_stride, icon_data, rect.w * channels); + } + + if (m_id != 0) + glsafe(::glDeleteTextures(1, &m_id)); + + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + glsafe(::glGenTextures(1, &m_id)); + glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint) m_id)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei) tex_size.x, (GLsizei) tex_size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*) data.data())); + + // bind no texture + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + + for (const auto &i : result) + i->tex_id = m_id; + return result; +} + +std::vector IconManager::init(const std::vector &file_paths, const ImVec2 &size, RasterType type) +{ + assert(!file_paths.empty()); + assert(size.x >= 1); + assert(size.x < 256*16); + + // TODO: remove in future + if (!m_icons.empty()) { + // not first initialization + priv::clear(m_icons); + m_icons.clear(); + m_icons_texture.reset(); + } + + // only rectangle are supported + assert(size.x == size.y); + // no subpixel supported + unsigned int width = static_cast(std::abs(std::round(size.x))); + assert(size.x == static_cast(width)); + + // state order has to match the enum IconState + const auto& states = priv::get_states(type); + + bool compress = false; + bool is_loaded = m_icons_texture.load_from_svg_files_as_sprites_array(file_paths, states, width, compress); + if (!is_loaded || (size_t) m_icons_texture.get_width() < (states.size() * width) || + (size_t) m_icons_texture.get_height() < (file_paths.size() * width)) { + // bad load of icons, but all usage of m_icons_texture check that texture is initialized + assert(false); + m_icons_texture.reset(); + return {}; + } + + unsigned count_files = file_paths.size(); + // count icons per file + unsigned count = states.size(); + // create result + std::vector result; + result.reserve(count_files); + + Icon def_icon; + def_icon.tex_id = m_icons_texture.get_id(); + def_icon.size = size; + + // float beacouse of dividing + float tex_height = static_cast(m_icons_texture.get_height()); + float tex_width = static_cast(m_icons_texture.get_width()); + + //for (const auto &f: file_paths) { + for (unsigned f = 0; f < count_files; ++f) { + // NOTE: there are space between icons + unsigned start_y = static_cast(f) * (width + 1) + 1; + float y1 = start_y / tex_height; + float y2 = (start_y + width) / tex_height; + Icons file_icons; + file_icons.reserve(count); + //for (const auto &s : states) { + for (unsigned j = 0; j < count; ++j) { + auto icon = std::make_shared(def_icon); + // NOTE: there are space between icons + unsigned start_x = static_cast(j) * (width + 1) + 1; + float x1 = start_x / tex_width; + float x2 = (start_x + width) / tex_width; + icon->tl = ImVec2(x1, y1); + icon->br = ImVec2(x2, y2); + file_icons.push_back(icon); + m_icons.push_back(std::move(icon)); + } + result.emplace_back(std::move(file_icons)); + } + return result; +} + +void IconManager::release() { + BOOST_LOG_TRIVIAL(error) << "Not implemented yet"; +} + +void priv::clear(IconManager::Icons &icons) { + std::string message; + for (auto &icon : icons) { + // Exist more than this instance of shared ptr? + long count = icon.use_count(); + if (count != 1) { + // in existing icon change texture to non existing one + icon->tex_id = 0; + + std::string descr = + ((count > 2) ? (std::to_string(count - 1) + "x") : "") + // count + std::to_string(icon->size.x) + "x" + std::to_string(icon->size.y); // resolution + if (message.empty()) + message = descr; + else + message += ", " + descr; + } + } + + if (!message.empty()) + BOOST_LOG_TRIVIAL(warning) << "There is still used icons(" << message << ")."; +} + +const std::vector> &priv::get_states(IconManager::RasterType type) { + static std::vector> color = {std::make_pair(0, false)}; + static std::vector> white = {std::make_pair(1, false)}; + static std::vector> gray = {std::make_pair(2, false)}; + static std::vector> color_wite_gray = { + std::make_pair(1, false), // Activable + std::make_pair(0, false), // Hovered + std::make_pair(2, false) // Disabled + }; + + switch (type) { + case IconManager::RasterType::color: return color; + case IconManager::RasterType::white_only_data: return white; + case IconManager::RasterType::gray_only_data: return gray; + case IconManager::RasterType::color_wite_gray: return color_wite_gray; + default: return color; + } +} + +void priv::draw_transparent_icon(const IconManager::Icon &icon) +{ + // Check input + if (!icon.is_valid()) { + assert(false); + BOOST_LOG_TRIVIAL(warning) << "Drawing invalid Icon."; + ImGui::Text("?"); + return; + } + + // size UV texture coors [in texture ratio] + ImVec2 size_uv(icon.br.x - icon.tl.x, icon.br.y - icon.tl.y); + ImVec2 one_px(size_uv.x / icon.size.x, size_uv.y / icon.size.y); + + // use top left corner of first icon + IconManager::Icon icon_px = icon; // copy + // reduce uv coors to one pixel + icon_px.tl = ImVec2(0, 0); + icon_px.br = one_px; + draw(icon_px); +} + +#include "imgui/imgui_internal.h" //ImGuiWindow +namespace Slic3r::GUI { + +void draw(const IconManager::Icon &icon, const ImVec2 &size, const ImVec4 &tint_col, const ImVec4 &border_col) +{ + // Check input + if (!icon.is_valid()) { + assert(false); + BOOST_LOG_TRIVIAL(warning) << "Drawing invalid Icon."; + ImGui::Text("?"); + return; + } + ImTextureID id = (void *)static_cast(icon.tex_id); + const ImVec2 &s = (size.x < 1 || size.y < 1) ? icon.size : size; + + // Orca: Align icon center vertically + ImGuiWindow *window = ImGui::GetCurrentWindow(); + ImGuiContext &g = *GImGui; + float cursor_y = window->DC.CursorPos.y; + float line_height = ImGui::GetTextLineHeight() + g.Style.FramePadding.y * 2; + int offset_y = (line_height - s.y) / 2; // Make sure its int otherwise it will be pixelated + window->DC.CursorPos.y += offset_y; + + ImGui::Image(id, s, icon.tl, icon.br, tint_col, border_col); + + // Reset offset + window->DC.CursorPosPrevLine.y = cursor_y; +} + +bool clickable(const IconManager::Icon &icon, const IconManager::Icon &icon_hover) +{ + // check of hover + ImGuiWindow *window = ImGui::GetCurrentWindow(); + float cursor_x = ImGui::GetCursorPosX() + - window->DC.GroupOffset.x + - window->DC.ColumnsOffset.x; + priv::draw_transparent_icon(icon); + ImGui::SameLine(cursor_x); + if (ImGui::IsItemHovered()) { + // redraw image + draw(icon_hover); + } else { + // redraw normal image + draw(icon); + } + return ImGui::IsItemClicked(); +} + +bool button(const IconManager::Icon &activ, const IconManager::Icon &hover, const IconManager::Icon &disable, bool disabled) +{ + if (disabled) { + draw(disable); + return false; + } + return clickable(activ, hover); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/IconManager.hpp b/src/slic3r/GUI/IconManager.hpp new file mode 100644 index 000000000..0b30a84cb --- /dev/null +++ b/src/slic3r/GUI/IconManager.hpp @@ -0,0 +1,134 @@ +#ifndef slic3r_IconManager_hpp_ +#define slic3r_IconManager_hpp_ + +#include +#include +#include "imgui/imgui.h" // ImVec2 +#include "slic3r/GUI/GLTexture.hpp" // texture storage + +namespace Slic3r::GUI { + +/// +/// Keep texture with icons for UI +/// Manage texture live -> create and destruct texture +/// by live of icon shared pointers. +/// +class IconManager +{ +public: + /// + /// Release texture + /// Set shared pointers to invalid texture + /// + ~IconManager(); + + /// + /// Define way to convert svg data to raster + /// + enum class RasterType: int{ + color = 1 << 1, + white_only_data = 1 << 2, + gray_only_data = 1 << 3, + color_wite_gray = color | white_only_data | gray_only_data + // TODO: add type with backgrounds + }; + + struct InitType { + // path to file with image .. svg + std::string filepath; + + // resolution of stored rasterized icon + ImVec2 size; // float will be rounded + + // could contain more than one type + RasterType type = RasterType::color; + // together color, white and gray = color | white_only_data | gray_only_data + }; + using InitTypes = std::vector; + + /// + /// Data for render texture with icon + /// + struct Icon { + // stored texture size + ImVec2 size = ImVec2(-1, -1); // [in px] --> unsigned int values stored as float + + // SubTexture UV coordinate in range from 0. to 1. + ImVec2 tl; // top left -> uv0 + ImVec2 br; // bottom right -> uv1 + + // OpenGL texture id + unsigned int tex_id = 0; + bool is_valid() const { return tex_id != 0;} + // && size.x > 0 && size.y > 0 && tl.x != br.x && tl.y != br.y; + }; + using Icons = std::vector >; + // Vector of icons, each vector contain multiple use of a SVG render + using VIcons = std::vector; + + /// + /// Initialize raster texture on GPU with given images + /// NOTE: Have to be called after OpenGL initialization + /// + /// Define files and its size with rasterization + /// Rasterized icons stored on GPU, + /// Same size and order as input, each item of vector is set of texture in order by RasterType + Icons init(const InitTypes &input); + + /// + /// Initialize multiple icons with same settings for size and type + /// NOTE: Have to be called after OpenGL initialization + /// + /// Define files with icon + /// Size of stored texture[in px], float will be rounded + /// Define way to rasterize icon, + /// together color, white and gray = RasterType::color | RasterType::white_only_data | RasterType::gray_only_data + /// Rasterized icons stored on GPU, + /// Same size and order as file_paths, each item of vector is set of texture in order by RasterType + VIcons init(const std::vector &file_paths, const ImVec2 &size, RasterType type = RasterType::color); + + /// + /// Release icons which are hold only by this manager + /// May change texture and position of icons. + /// + void release(); + +private: + // keep data stored on GPU + GLTexture m_icons_texture; + + unsigned int m_id{ 0 }; + Icons m_icons; +}; + +/// +/// Draw imgui image with icon +/// +/// Place in texture +/// [optional]Size of image, wen zero than use same size as stored texture +/// viz ImGui::Image +/// viz ImGui::Image +void draw(const IconManager::Icon &icon, + const ImVec2 &size = ImVec2(0, 0), + const ImVec4 &tint_col = ImVec4(1, 1, 1, 1), + const ImVec4 &border_col = ImVec4(0, 0, 0, 0)); + +/// +/// Draw icon which change on hover +/// +/// Draw when no hover +/// Draw when hover +/// True when click, otherwise False +bool clickable(const IconManager::Icon &icon, const IconManager::Icon &icon_hover); + +/// +/// Use icon as button with 3 states activ hover and disabled +/// +/// Not disabled not hovered image +/// Hovered image +/// Disabled image +/// True when click on enabled, otherwise False +bool button(const IconManager::Icon &activ, const IconManager::Icon &hover, const IconManager::Icon &disable, bool disabled = false); + +} // namespace Slic3r::GUI +#endif // slic3r_IconManager_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index e88c982e6..77e08cff8 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -519,7 +519,7 @@ void ImGuiWrapper::render() m_new_frame_open = false; } -ImVec2 ImGuiWrapper::calc_text_size(const wxString &text, float wrap_width) const +ImVec2 ImGuiWrapper::calc_text_size(const wxString &text, float wrap_width) { auto text_utf8 = into_u8(text); ImVec2 size = ImGui::CalcTextSize(text_utf8.c_str(), NULL, false, wrap_width); @@ -991,13 +991,13 @@ void ImGuiWrapper::text(const char *label) void ImGuiWrapper::text(const std::string &label) { - this->text(label.c_str()); + ImGuiWrapper::text(label.c_str()); } void ImGuiWrapper::text(const wxString &label) { auto label_utf8 = into_u8(label); - this->text(label_utf8.c_str()); + ImGuiWrapper::text(label_utf8.c_str()); } void ImGuiWrapper::warning_text(const char *label) @@ -1912,6 +1912,163 @@ bool ImGuiWrapper::want_any_input() const return io.WantCaptureMouse || io.WantCaptureKeyboard || io.WantTextInput; } +template +static bool input_optional(std::optional &v, Func &f, std::function is_default, const T &def_val) +{ + if (v.has_value()) { + if (f(*v)) { + if (is_default(*v)) v.reset(); + return true; + } + } else { + T val = def_val; + if (f(val)) { + if (!is_default(val)) v = val; + return true; + } + } + return false; +} + +bool ImGuiWrapper::input_optional_int(const char *label, std::optional &v, int step, int step_fast, ImGuiInputTextFlags flags, int def_val) +{ + auto func = [&](int &value) { return ImGui::InputInt(label, &value, step, step_fast, flags); }; + std::function is_default = [def_val](const int &value) -> bool { return value == def_val; }; + return input_optional(v, func, is_default, def_val); +} + +bool ImGuiWrapper::input_optional_float(const char *label, std::optional &v, float step, float step_fast, const char *format, ImGuiInputTextFlags flags, float def_val) +{ + auto func = [&](float &value) { return ImGui::InputFloat(label, &value, step, step_fast, format, flags); }; + std::function is_default = [def_val](const float &value) -> bool { return std::fabs(value - def_val) <= std::numeric_limits::epsilon(); }; + return input_optional(v, func, is_default, def_val); +} + +bool ImGuiWrapper::drag_optional_float(const char *label, std::optional &v, float v_speed, float v_min, float v_max, const char *format, float power, float def_val) +{ + auto func = [&](float &value) { return ImGui::DragFloat(label, &value, v_speed, v_min, v_max, format, power); }; + std::function is_default = [def_val](const float &value) -> bool { return std::fabs(value - def_val) <= std::numeric_limits::epsilon(); }; + return input_optional(v, func, is_default, def_val); +} + +bool ImGuiWrapper::slider_optional_float( + const char *label, std::optional &v, float v_min, float v_max, const char *format, float power, bool clamp, const wxString &tooltip, bool show_edit_btn, float def_val) +{ + auto func = [&](float &value) { return slider_float(label, &value, v_min, v_max, format, power, clamp, tooltip, show_edit_btn); }; + std::function is_default = [def_val](const float &value) -> bool { return std::fabs(value - def_val) <= std::numeric_limits::epsilon(); }; + return input_optional(v, func, is_default, def_val); +} + +bool ImGuiWrapper::slider_optional_int( + const char *label, std::optional &v, int v_min, int v_max, const char *format, float power, bool clamp, const wxString &tooltip, bool show_edit_btn, int def_val) +{ + std::optional val; + if (v.has_value()) val = static_cast(*v); + auto func = [&](float &value) { return slider_float(label, &value, v_min, v_max, format, power, clamp, tooltip, show_edit_btn); }; + std::function is_default = [def_val](const float &value) -> bool { return std::fabs(value - def_val) < 0.9f; }; + + float default_value = static_cast(def_val); + if (input_optional(val, func, is_default, default_value)) { + if (val.has_value()) + v = static_cast(std::round(*val)); + else + v.reset(); + return true; + } else + return false; +} + +std::optional ImGuiWrapper::change_window_position(const char *window_name, bool try_to_fix) +{ + ImGuiWindow *window = ImGui::FindWindowByName(window_name); + // is window just created + if (window == NULL) return {}; + + // position of window on screen + ImVec2 position = window->Pos; + ImVec2 size = window->SizeFull; + + // screen size + ImVec2 screen = ImGui::GetMainViewport()->Size; + + std::optional output_window_offset; + if (position.x < 0) { + if (position.y < 0) + // top left + output_window_offset = ImVec2(0, 0); + else + // only left + output_window_offset = ImVec2(0, position.y); + } else if (position.y < 0) { + // only top + output_window_offset = ImVec2(position.x, 0); + } else if (screen.x < (position.x + size.x)) { + if (screen.y < (position.y + size.y)) + // right bottom + output_window_offset = ImVec2(screen.x - size.x, screen.y - size.y); + else + // only right + output_window_offset = ImVec2(screen.x - size.x, position.y); + } else if (screen.y < (position.y + size.y)) { + // only bottom + output_window_offset = ImVec2(position.x, screen.y - size.y); + } + + if (!try_to_fix && output_window_offset.has_value()) output_window_offset = ImVec2(-1, -1); // Put on default position + + return output_window_offset; +} + +void ImGuiWrapper::left_inputs() { ImGui::ClearActiveID(); } + +std::string ImGuiWrapper::trunc(const std::string &text, float width, const char *tail) +{ + float text_width = ImGui::CalcTextSize(text.c_str()).x; + if (text_width < width) return text; + float tail_width = ImGui::CalcTextSize(tail).x; + assert(width > tail_width); + if (width <= tail_width) return "Error: Can't add tail and not be under wanted width."; + float allowed_width = width - tail_width; + + // guess approx count of letter + wxString temp{"n"}; + float average_letter_width = calc_text_size(temp).x; // average letter width + unsigned count_letter = static_cast(allowed_width / average_letter_width); + + //std::string_view text_ = text; + //std::string_view result_text = text_.substr(0, count_letter); + wxString result_text(text.substr(0, count_letter)); + text_width = calc_text_size(result_text).x; + if (text_width < allowed_width) { + // increase letter count + while (count_letter < text.length()) { + ++count_letter; + wxString act_text(text.substr(0, count_letter)); + text_width = calc_text_size(act_text).x; + if (text_width > allowed_width) break; + result_text = act_text; + } + } else { + // decrease letter count + while (count_letter > 1) { + --count_letter; + result_text = text.substr(0, count_letter); + text_width = calc_text_size(result_text).x; + if (text_width < allowed_width) break; + } + } + return result_text .ToStdString()+ tail; +} + +void ImGuiWrapper::escape_double_hash(std::string &text) +{ // add space between hashes + const std::string search = "##"; + const std::string replace = "# #"; + size_t pos = 0; + while ((pos = text.find(search, pos)) != std::string::npos) text.replace(pos, search.length(), replace); +} + + void ImGuiWrapper::disable_background_fadeout_animation() { GImGui->DimBgRatio = 1.0f; @@ -2206,6 +2363,19 @@ void ImGuiWrapper::pop_combo_style() ImGui::PopStyleColor(7); } +void ImGuiWrapper::push_radio_style() +{ + if (m_is_dark_mode) { + ImGui::PushStyleColor(ImGuiCol_CheckMark, to_ImVec4(decode_color_to_float_array("#00675b"))); // ORCA use orca color for radio buttons + } else { + ImGui::PushStyleColor(ImGuiCol_CheckMark, to_ImVec4(decode_color_to_float_array("#009688"))); // ORCA use orca color for radio buttons + } +} + +void ImGuiWrapper::pop_radio_style() { + ImGui::PopStyleColor(1); +} + void ImGuiWrapper::init_font(bool compress) { destroy_font(); diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index 4edb1e87e..ca4259714 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -90,7 +90,7 @@ public: float scaled(float x) const { return x * m_font_size; } ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); } - ImVec2 calc_text_size(const wxString &text, float wrap_width = -1.0f) const; + static ImVec2 calc_text_size(const wxString &text, float wrap_width = -1.0f); ImVec2 calc_button_size(const wxString &text, const ImVec2 &button_size = ImVec2(0, 0)) const; float find_widest_text(std::vector &text_list); ImVec2 get_item_spacing() const; @@ -140,9 +140,9 @@ public: bool bbl_checkbox(const wxString &label, bool &value); bool bbl_radio_button(const char *label, bool active); bool bbl_sliderin(const char *label, int *v, int v_min, int v_max, const char *format = "%d", ImGuiSliderFlags flags = 0); - void text(const char *label); - void text(const std::string &label); - void text(const wxString &label); + static void text(const char *label); + static void text(const std::string &label); + static void text(const wxString &label); void warning_text(const char *all_text); void warning_text(const wxString &all_text); void text_colored(const ImVec4& color, const char* label); @@ -195,6 +195,71 @@ public: bool want_text_input() const; bool want_any_input() const; + // Optional inputs are used for set up value inside of an optional, with default value + // + // Extended function ImGui::InputInt to work with std::optional, when value == def_val optional is released. + static bool input_optional_int(const char *label, std::optional &v, int step = 1, int step_fast = 100, ImGuiInputTextFlags flags = 0, int def_val = 0); + // Extended function ImGui::InputFloat to work with std::optional value near def_val cause release of optional + static bool input_optional_float( + const char *label, std::optional &v, float step = 0.0f, float step_fast = 0.0f, const char *format = "%.3f", ImGuiInputTextFlags flags = 0, float def_val = .0f); + // Extended function ImGui::DragFloat to work with std::optional value near def_val cause release of optional + static bool drag_optional_float(const char *label, std::optional &v, float v_speed, float v_min, float v_max, const char *format, float power, float def_val = .0f); + // Extended function ImGuiWrapper::slider_float to work with std::optional value near def_val cause release of optional + bool slider_optional_float(const char * label, + std::optional &v, + float v_min, + float v_max, + const char * format = "%.3f", + float power = 1.0f, + bool clamp = true, + const wxString & tooltip = {}, + bool show_edit_btn = true, + float def_val = .0f); + // Extended function ImGuiWrapper::slider_float to work with std::optional, when value == def_val than optional release its value + bool slider_optional_int(const char * label, + std::optional &v, + int v_min, + int v_max, + const char * format = "%.3f", + float power = 1.0f, + bool clamp = true, + const wxString & tooltip = {}, + bool show_edit_btn = true, + int def_val = 0); + + /// + /// Change position of imgui window + /// + /// ImGui identifier of window + /// [output] optional + /// When True Only move to be full visible otherwise reset position + /// New offset of window for function ImGui::SetNextWindowPos + static std::optional change_window_position(const char *window_name, bool try_to_fix); + + /// + /// Use ImGui internals to unactivate (lose focus) in input. + /// When input is activ it can't change value by application. + /// + static void left_inputs(); + + /// + /// Truncate text by ImGui draw function to specific width + /// NOTE 1: ImGui must be initialized + /// NOTE 2: Calculation for actual acive imgui font + /// + /// Text to be truncated + /// Maximal width before truncate + /// String puted on end of text to be visible truncation + /// Truncated text + static std::string trunc(const std::string &text, float width, const char *tail = " .."); + + /// + /// Escape ## in data by add space between hashes + /// Needed when user written text is visualized by ImGui. + /// + /// In/Out text to be escaped + static void escape_double_hash(std::string &text); + #if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT bool requires_extra_frame() const { return m_requires_extra_frame; } void set_requires_extra_frame() { m_requires_extra_frame = true; } @@ -241,7 +306,8 @@ public: static void pop_button_disable_style(); static void push_combo_style(const float scale); static void pop_combo_style(); - + static void push_radio_style(); + static void pop_radio_style(); //BBS static int TOOLBAR_WINDOW_FLAGS; diff --git a/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp new file mode 100644 index 000000000..3a223544d --- /dev/null +++ b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp @@ -0,0 +1,182 @@ +#include + +#include "BoostThreadWorker.hpp" + +namespace Slic3r { namespace GUI { + +void BoostThreadWorker::WorkerMessage::deliver(BoostThreadWorker &runner) +{ + switch(MsgType(get_type())) { + case Empty: break; + case Status: { + auto info = boost::get(m_data); + if (runner.get_pri()) { + runner.get_pri()->set_progress(info.status); + runner.get_pri()->set_status_text(info.msg.c_str()); + } + break; + } + case Finalize: { + auto& entry = boost::get(m_data); + entry.job->finalize(entry.canceled, entry.eptr); + + // Unhandled exceptions are rethrown without mercy. + if (entry.eptr) + std::rethrow_exception(entry.eptr); + + break; + } + case MainThreadCall: { + auto &calldata = boost::get(m_data); + calldata.fn(); + calldata.promise.set_value(); + + break; + } + } +} + +void BoostThreadWorker::run() +{ + bool stop = false; + while (!stop) { + m_input_queue + .consume_one(BlockingWait{0, &m_running}, [this, &stop](JobEntry &e) { + if (!e.job) + stop = true; + else { + m_canceled.store(false); + + try { + e.job->process(*this); + } catch (...) { + e.eptr = std::current_exception(); + } + + e.canceled = m_canceled.load(); + m_output_queue.push(std::move(e)); // finalization message + } + m_running.store(false); + }); + }; +} + +void BoostThreadWorker::update_status(int st, const std::string &msg) +{ + m_output_queue.push(st, msg); +} + +std::future BoostThreadWorker::call_on_main_thread(std::function fn) +{ + MainThreadCallData cbdata{std::move(fn), {}}; + std::future future = cbdata.promise.get_future(); + + m_output_queue.push(std::move(cbdata)); + + return future; +} + +BoostThreadWorker::BoostThreadWorker(std::shared_ptr pri, + boost::thread::attributes &attribs, + const char * name) + : m_progress(std::move(pri)), m_name{name} +{ + if (m_progress) + m_progress->set_cancel_callback([this](){ cancel(); }); + + m_thread = create_thread(attribs, [this] { this->run(); }); + + std::string nm{name}; + if (!nm.empty()) set_thread_name(m_thread, name); +} + +constexpr int ABORT_WAIT_MAX_MS = 10000; + +BoostThreadWorker::~BoostThreadWorker() +{ + bool joined = false; + try { + cancel_all(); + wait_for_idle(ABORT_WAIT_MAX_MS); + m_input_queue.push(JobEntry{nullptr}); + joined = join(ABORT_WAIT_MAX_MS); + } catch(...) {} + + if (!joined) + BOOST_LOG_TRIVIAL(error) + << "Could not join worker thread '" << m_name << "'"; +} + +bool BoostThreadWorker::join(int timeout_ms) +{ + if (!m_thread.joinable()) + return true; + + if (timeout_ms <= 0) { + m_thread.join(); + } + else if (m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms))) { + return true; + } + else + return false; + + return true; +} + +void BoostThreadWorker::process_events() +{ + while (m_output_queue.consume_one([this](WorkerMessage &msg) { + msg.deliver(*this); + })); +} + +bool BoostThreadWorker::wait_for_current_job(unsigned timeout_ms) +{ + bool ret = true; + + if (!is_idle()) { + bool was_finish = false; + bool timeout_reached = false; + while (!timeout_reached && !was_finish) { + timeout_reached = + !m_output_queue.consume_one(BlockingWait{timeout_ms}, + [this, &was_finish]( + WorkerMessage &msg) { + msg.deliver(*this); + if (msg.get_type() == + WorkerMessage::Finalize) + was_finish = true; + }); + } + + ret = !timeout_reached; + } + + return ret; +} + +bool BoostThreadWorker::wait_for_idle(unsigned timeout_ms) +{ + bool timeout_reached = false; + while (!timeout_reached && !is_idle()) { + timeout_reached = !m_output_queue + .consume_one(BlockingWait{timeout_ms}, + [this](WorkerMessage &msg) { + msg.deliver(*this); + }); + } + + return !timeout_reached; +} + +bool BoostThreadWorker::push(std::unique_ptr job) +{ + if (!job) + return false; + + m_input_queue.push(JobEntry{std::move(job)}); + return true; +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/BoostThreadWorker.hpp b/src/slic3r/GUI/Jobs/BoostThreadWorker.hpp new file mode 100644 index 000000000..90836688b --- /dev/null +++ b/src/slic3r/GUI/Jobs/BoostThreadWorker.hpp @@ -0,0 +1,155 @@ +#ifndef BOOSTTHREADWORKER_HPP +#define BOOSTTHREADWORKER_HPP + +#include + +#include "Worker.hpp" + +#include +#include + +#include "ThreadSafeQueue.hpp" +#include "slic3r/GUI/GUI.hpp" + +namespace Slic3r { namespace GUI { + +// An implementation of the Worker interface which uses the boost::thread +// API and two thread safe message queues to communicate with the main thread +// back and forth. The queue from the main thread to the worker thread holds the +// job entries that will be performed on the worker. The other queue holds messages +// from the worker to the main thread. These messages include status updates, +// finishing operation and arbitrary functiors that need to be performed +// on the main thread during the jobs execution, like displaying intermediate +// results. +class BoostThreadWorker : public Worker, private JobNew::Ctl +{ + struct JobEntry // Goes into worker and also out of worker as a finalize msg + { + std::unique_ptr job; + bool canceled = false; + std::exception_ptr eptr = nullptr; + }; + + // A message data for status updates. Only goes from worker to main thread. + struct StatusInfo { int status; std::string msg; }; + + // An arbitrary callback to be called on the main thread. Only from worker + // to main thread. + struct MainThreadCallData + { + std::function fn; + std::promise promise; + }; + + struct EmptyMessage {}; + + class WorkerMessage + { + public: + enum MsgType { Empty, Status, Finalize, MainThreadCall }; + + private: + boost::variant m_data; + + public: + WorkerMessage() = default; + WorkerMessage(int s, std::string txt) + : m_data{StatusInfo{s, std::move(txt)}} + {} + WorkerMessage(JobEntry &&entry) : m_data{std::move(entry)} {} + WorkerMessage(MainThreadCallData fn) : m_data{std::move(fn)} {} + + int get_type () const { return m_data.which(); } + + void deliver(BoostThreadWorker &runner); + }; + + using JobQueue = ThreadSafeQueueSPSC; + using MessageQueue = ThreadSafeQueueSPSC; + + boost::thread m_thread; + std::atomic m_running{false}, m_canceled{false}; + std::shared_ptr m_progress; + JobQueue m_input_queue; // from main thread to worker + MessageQueue m_output_queue; // form worker to main thread + std::string m_name; + + void run(); + + bool join(int timeout_ms = 0); + +protected: + // Implement Job::Ctl interface: + + void update_status(int st, const std::string &msg = "") override; + + bool was_canceled() const override { return m_canceled.load(); } + + std::future call_on_main_thread(std::function fn) override; + +public: + explicit BoostThreadWorker(std::shared_ptr pri, + boost::thread::attributes & attr, + const char * name = ""); + + explicit BoostThreadWorker(std::shared_ptr pri, + boost::thread::attributes && attr, + const char * name = "") + : BoostThreadWorker{std::move(pri), attr, name} + {} + + explicit BoostThreadWorker(std::shared_ptr pri, + const char * name = "") + : BoostThreadWorker{std::move(pri), {}, name} + {} + + ~BoostThreadWorker(); + + BoostThreadWorker(const BoostThreadWorker &) = delete; + BoostThreadWorker(BoostThreadWorker &&) = delete; + BoostThreadWorker &operator=(const BoostThreadWorker &) = delete; + BoostThreadWorker &operator=(BoostThreadWorker &&) = delete; + + bool push(std::unique_ptr job) override; + + bool is_idle() const override + { + // The assumption is that jobs can only be queued from a single main + // thread from which this method is also called. And the output + // messages are also processed only in this calling thread. In that + // case, if the input queue is empty, it will remain so during this + // function call. If the worker thread is also not running and the + // output queue is already processed, we can safely say that the + // worker is dormant. + return m_input_queue.empty() && !m_running.load() && m_output_queue.empty(); + } + + void cancel() override { m_canceled.store(true); } + void cancel_all() override { m_input_queue.clear(); cancel(); } + + ProgressIndicator * get_pri() { return m_progress.get(); } + const ProgressIndicator * get_pri() const { return m_progress.get(); } + + void clear_percent() override + { + if (m_progress) { + m_progress->clear_percent(); + } + } + + void show_error_info(const std::string &msg, int code, const std::string &description, const std::string &extra) override + { + if (m_progress) { + m_progress->show_error_info(from_u8(msg), code, from_u8(description), from_u8(extra)); + } + } + + void process_events() override; + bool wait_for_current_job(unsigned timeout_ms = 0) override; + bool wait_for_idle(unsigned timeout_ms = 0) override; + +}; + +}} // namespace Slic3r::GUI + +#endif // BOOSTTHREADWORKER_HPP diff --git a/src/slic3r/GUI/Jobs/BusyCursorJob.hpp b/src/slic3r/GUI/Jobs/BusyCursorJob.hpp new file mode 100644 index 000000000..77ae4568d --- /dev/null +++ b/src/slic3r/GUI/Jobs/BusyCursorJob.hpp @@ -0,0 +1,54 @@ +#ifndef BUSYCURSORJOB_HPP +#define BUSYCURSORJOB_HPP + +#include "JobNew.hpp" + +#include +#include + +namespace Slic3r { namespace GUI { + +struct CursorSetterRAII +{ + JobNew::Ctl &ctl; + CursorSetterRAII(JobNew::Ctl &c) : ctl{c} + { + ctl.call_on_main_thread([] { wxBeginBusyCursor(); }); + } + ~CursorSetterRAII() + { + try { + ctl.call_on_main_thread([] { wxEndBusyCursor(); }); + } catch(...) { + BOOST_LOG_TRIVIAL(error) << "Can't revert cursor from busy to normal"; + } + } +}; + +template +class BusyCursored : public JobNew +{ + JobSubclass m_job; + +public: + template + BusyCursored(Args &&...args) : m_job{std::forward(args)...} + {} + + void process(Ctl &ctl) override + { + CursorSetterRAII cursor_setter{ctl}; + m_job.process(ctl); + } + + void finalize(bool canceled, std::exception_ptr &eptr) override + { + m_job.finalize(canceled, eptr); + } +}; + + +} +} + +#endif // BUSYCURSORJOB_HPP diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp new file mode 100644 index 000000000..04e5fd154 --- /dev/null +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -0,0 +1,1153 @@ +#include "EmbossJob.hpp" +#include +#include +#include +// +#include +#include // load_obj for default mesh +#include // use surface cuts +#include // create object +#include + +#include "libslic3r/libslic3r.h" +#include "slic3r/GUI/Plater.hpp" +////#include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/SurfaceDrag.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +//#include "slic3r/GUI/MainFrame.hpp" +//#include "slic3r/GUI/GUI.hpp" +//#include "slic3r/GUI/GUI_App.hpp" +//#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" +#include "slic3r/GUI/Selection.hpp" +#include "slic3r/GUI/CameraUtils.hpp" +#include "slic3r/GUI/format.hpp" +//#include "slic3r/GUI/3DScene.hpp" +#include "slic3r/GUI/Jobs/Worker.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +// #define EXECUTE_UPDATE_ON_MAIN_THREAD // debug execution on main thread +using namespace Slic3r::Emboss; +namespace Slic3r { +namespace GUI { +namespace Emboss { +// Offset of clossed side to model +const float SAFE_SURFACE_OFFSET = 0.015f; // [in mm] +// create sure that emboss object is bigger than source object [in mm] +constexpr float safe_extension = 1.0f; +void create_message(const std::string &message) { show_error(nullptr, message.c_str()); } +// jobs +class JobException : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +bool was_canceled(const JobNew::Ctl &ctl, const DataBase &base) +{ + if (base.cancel->load()) return true; + return ctl.was_canceled(); +} + +bool _finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input) +{ + // doesn't care about exception when process was canceled by user + if (canceled || input.cancel->load()) { + eptr = nullptr; + return false; + } + return !exception_process(eptr); +} + +bool exception_process(std::exception_ptr &eptr) +{ + if (!eptr) return false; + try { + std::rethrow_exception(eptr); + } catch (JobException &e) { + create_message(e.what()); + eptr = nullptr; + } + return true; +} + +void update_name_in_list(const ObjectList &object_list, const ModelVolume &volume) +{ + const ModelObjectPtrs *objects_ptr = object_list.objects(); + if (objects_ptr == nullptr) return; + + const ModelObjectPtrs &objects = *objects_ptr; + const ModelObject * object = volume.get_object(); + const ObjectID & object_id = object->id(); + + // search for index of object + int object_index = -1; + for (size_t i = 0; i < objects.size(); ++i) + if (objects[i]->id() == object_id) { + object_index = static_cast(i); + break; + } + + const ModelVolumePtrs volumes = object->volumes; + const ObjectID & volume_id = volume.id(); + + // search for index of volume + int volume_index = -1; + for (size_t i = 0; i < volumes.size(); ++i) + if (volumes[i]->id() == volume_id) { + volume_index = static_cast(i); + break; + } + + if (object_index < 0 || volume_index < 0) return; + + object_list.update_name_in_list(object_index, volume_index); +} + +ExPolygons create_shape(DataBase &input) +{ + EmbossShape &es = input.create_shape(); + // TODO: improve to use real size of volume + // ... need world matrix for volume + // ... printer resolution will be fine too + return union_with_delta(es, UNION_DELTA, UNION_MAX_ITERATIN); +} + +void _update_volume(TriangleMesh &&mesh, const DataUpdate &data, const Transform3d *tr) +{ + // for sure that some object will be created + if (mesh.its.empty()) return create_message("Empty mesh can't be created."); + + Plater *plater = wxGetApp().plater(); + // Check gizmo is still open otherwise job should be canceled + assert(plater->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss || + plater->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Svg); + + if (data.make_snapshot) { + // TRN: This is the title of the action appearing in undo/redo stack. + // It is same for Text and SVG. + std::string snap_name = _u8L("Emboss attribute change"); + Plater::TakeSnapshot snapshot(plater, snap_name, UndoRedo::SnapshotType::GizmoAction); + } + + ModelVolume *volume = get_model_volume(data.volume_id, plater->model().objects); + + // could appear when user delete edited volume + if (volume == nullptr) return; + + if (tr) { + volume->set_transformation(*tr); + } else { + // apply fix matrix made by store to .3mf + const std::optional &emboss_shape = volume->emboss_shape; + assert(emboss_shape.has_value()); + if (emboss_shape.has_value() && emboss_shape->fix_3mf_tr.has_value()) volume->set_transformation(volume->get_matrix() * emboss_shape->fix_3mf_tr->inverse()); + } + + UpdateJob::update_volume(volume, std::move(mesh), *data.base); +} + +std::vector create_line_bounds(const ExPolygonsWithIds &shapes, size_t count_lines = 0) +{ + if (count_lines == 0) count_lines = get_count_lines(shapes); + assert(count_lines == get_count_lines(shapes)); + + std::vector result(count_lines); + size_t text_line_index = 0; + // s_i .. shape index + for (const ExPolygonsWithId &shape_id : shapes) { + const ExPolygons &shape = shape_id.expoly; + BoundingBox bb; + if (!shape.empty()) { bb = get_extents(shape); } + BoundingBoxes &line_bbs = result[text_line_index]; + line_bbs.push_back(bb); + if (shape_id.id == ENTER_UNICODE) { + // skip enters on beginig and tail + ++text_line_index; + } + } + return result; +} + +bool is_valid(ModelVolumeType volume_type) +{ + assert(volume_type != ModelVolumeType::INVALID); + assert(volume_type == ModelVolumeType::MODEL_PART || volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::PARAMETER_MODIFIER); + if (volume_type == ModelVolumeType::MODEL_PART || volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::PARAMETER_MODIFIER) return true; + + BOOST_LOG_TRIVIAL(error) << "Can't create embossed volume with this type: " << (int) volume_type; + return false; +} + +bool check(unsigned char gizmo_type) { return gizmo_type == (unsigned char) GLGizmosManager::Svg; } + +bool check(const CreateVolumeParams &input) +{ + bool res = is_valid(input.volume_type); + auto gizmo_type = static_cast(input.gizmo_type); + res &= check(gizmo_type); + return res; +} + +bool check(const DataBase &input, bool check_fontfile, bool use_surface) +{ + bool res = true; + // if (check_fontfile) { + // assert(input.font_file.has_value()); + // res &= input.font_file.has_value(); + // } + // assert(!input.text_configuration.fix_3mf_tr.has_value()); + // res &= !input.text_configuration.fix_3mf_tr.has_value(); + // assert(!input.text_configuration.text.empty()); + // res &= !input.text_configuration.text.empty(); + assert(!input.volume_name.empty()); + res &= !input.volume_name.empty(); + // const FontProp& prop = input.text_configuration.style.prop; + // assert(prop.per_glyph == !input.text_lines.empty()); + // res &= prop.per_glyph == !input.text_lines.empty(); + // if (prop.per_glyph) { + // assert(get_count_lines(input.text_configuration.text) == input.text_lines.size()); + // res &= get_count_lines(input.text_configuration.text) == input.text_lines.size(); + //} + return res; +} + +bool check(const DataCreateObject &input) +{ + bool check_fontfile = false; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile); + assert(input.screen_coor.x() >= 0.); + res &= input.screen_coor.x() >= 0.; + assert(input.screen_coor.y() >= 0.); + res &= input.screen_coor.y() >= 0.; + assert(input.bed_shape.size() >= 3); // at least triangle + res &= input.bed_shape.size() >= 3; + res &= check(input.gizmo_type); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; + return res; +} + +bool check(const DataUpdate &input, bool is_main_thread, bool use_surface) +{ + bool check_fontfile = true; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile, use_surface); + if (is_main_thread) assert(get_model_volume(input.volume_id, wxGetApp().model().objects) != nullptr); + // assert(input.base->cancel != nullptr); + if (is_main_thread) assert(!input.base->cancel->load()); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; + return res; +} + +bool check(const CreateSurfaceVolumeData &input, bool is_main_thread) +{ + bool use_surface = true; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, is_main_thread, use_surface); + assert(!input.sources.empty()); + res &= !input.sources.empty(); + res &= check(input.gizmo_type); + assert(input.base->shape.projection.use_surface); + res &= input.base->shape.projection.use_surface; + return res; +} + +bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread) +{ + bool use_surface = true; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, is_main_thread, use_surface); + assert(!input.sources.empty()); + res &= !input.sources.empty(); + assert(input.base->shape.projection.use_surface); + res &= input.base->shape.projection.use_surface; + return res; +} + +bool check(const DataCreateVolume &input, bool is_main_thread) +{ + bool check_fontfile = false; + assert(input.base != nullptr); + bool res = input.base != nullptr; + res &= check(*input.base, check_fontfile); + res &= is_valid(input.volume_type); + res &= check(input.gizmo_type); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; + return res; +} + +void DataBase::write(ModelVolume &volume) const +{ + volume.name = volume_name; + volume.emboss_shape = shape; + volume.emboss_shape->fix_3mf_tr.reset(); +} + +UpdateSurfaceVolumeJob::UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input) : m_input(std::move(input)) { +} + + +void UpdateSurfaceVolumeJob::process(Ctl &ctl) +{ + if (!check(m_input)) + throw JobException("Bad input data for UseSurfaceJob."); + m_result = cut_surface(*m_input.base, m_input); //, was_canceled(ctl, *m_input.base) +} +bool UpdateSurfaceVolumeJob::is_use_surfae_error =false; + +void UpdateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + if (!_finalize(canceled, eptr, *m_input.base)) + return; + // when start using surface it is wanted to move text origin on surface of model + // also when repeteadly move above surface result position should match + _update_volume(std::move(m_result), m_input, &m_input.transform); +} + +UpdateJob::UpdateJob(DataUpdate &&input) : m_input(std::move(input)) {} + +void UpdateJob::process(Ctl &ctl) +{ + if (!check(m_input)) throw JobException("Bad input data for EmbossUpdateJob."); + + m_result = try_create_mesh(*m_input.base); + if (was_canceled(ctl, *m_input.base)) + return; + if (m_result.its.empty()) + throw JobException("Created text volume is empty. Change text or font."); +} + +void UpdateJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + if (!_finalize(canceled, eptr, *m_input.base)) return; + _update_volume(std::move(m_result), m_input); +} + +void UpdateJob::update_volume(ModelVolume *volume, TriangleMesh &&mesh, const DataBase &base) +{ // check inputs + bool is_valid_input = volume != nullptr && !mesh.empty() && !base.volume_name.empty(); + assert(is_valid_input); + if (!is_valid_input) return; + + // update volume + volume->set_mesh(std::move(mesh)); + volume->set_new_unique_id(); + volume->calculate_convex_hull(); + + // write data from base into volume + base.write(*volume); + + GUI_App &app = wxGetApp(); // may be move to input + if (volume->name != base.volume_name) { + volume->name = base.volume_name; + + const ObjectList *obj_list = app.obj_list(); + if (obj_list != nullptr) + update_name_in_list(*obj_list, *volume); + } + + ModelObject *object = volume->get_object(); + assert(object != nullptr); + if (object == nullptr) return; + + Plater *plater = app.plater(); + if (plater->printer_technology() == ptSLA) + sla::reproject_points_and_holes(object); + plater->changed_object(*object); +} + +CreateObjectJob::CreateObjectJob(DataCreateObject &&input) : m_input(std::move(input)) {} +void CreateObjectJob::process(Ctl &ctl) +{ + if (!check(m_input)) return throw JobException("Bad input data for EmbossCreateObjectJob."); + + // can't create new object with using surface + if (m_input.base->shape.projection.use_surface) m_input.base->shape.projection.use_surface = false; + + // auto was_canceled = ::was_canceled(ctl, *m_input.base); + m_result = create_mesh(*m_input.base); + + // Create new object + // calculate X,Y offset position for lay on platter in place of + // mouse click + Vec2d bed_coor = CameraUtils::get_z0_position(m_input.camera, m_input.screen_coor); + + // check point is on build plate: + Points bed_shape_; + bed_shape_.reserve(m_input.bed_shape.size()); + for (const Vec2d &p : m_input.bed_shape) bed_shape_.emplace_back(p.cast()); + Slic3r::Polygon bed(bed_shape_); + if (!bed.contains(bed_coor.cast())) + // mouse pose is out of build plate so create object in center of plate + bed_coor = bed.centroid().cast(); + + double z = m_input.base->shape.projection.depth / 2; + Vec3d offset(bed_coor.x(), bed_coor.y(), z); + offset -= m_result.center(); + Transform3d::TranslationType tt(offset.x(), offset.y(), offset.z()); + m_transformation = Transform3d(tt); + + // rotate around Z by style settings + if (m_input.angle.has_value()) { + std::optional distance; // new object ignore surface distance from style settings + apply_transformation(m_input.angle, distance, m_transformation); + } +} + +void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + if (!_finalize(canceled, eptr, *m_input.base)) return; + // only for sure + if (m_result.empty()) { + create_message("Can't create empty object."); + return; + } + GUI_App &app = wxGetApp(); + Plater * plater = app.plater(); + plater->take_snapshot(_u8L("Add Emboss text object")); + + Model &model = plater->model(); +#ifdef _DEBUG + check_model_ids_validity(model); +#endif /* _DEBUG */ + { + // INFO: inspiration for create object is from ObjectList::load_mesh_object() + ModelObject *new_object = model.add_object(); + new_object->name = m_input.base->volume_name; + new_object->add_instance(); // each object should have at list one instance + + ModelVolume *new_volume = new_object->add_volume(std::move(m_result)); + // set a default extruder value, since user can't add it manually + new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); + + // write emboss data into volume + m_input.base->write(*new_volume); + + // set transformation + Slic3r::Geometry::Transformation tr(m_transformation); + new_object->instances.front()->set_transformation(tr); + new_object->ensure_on_bed(); + + // Actualize right panel and set inside of selection + app.obj_list()->paste_objects_into_list({model.objects.size() - 1}); + } +#ifdef _DEBUG + check_model_ids_validity(model); +#endif /* _DEBUG */ + + // When add new object selection is empty. + // When cursor move and no one object is selected than + // Manager::reset_all() So Gizmo could be closed before end of creation object + GLCanvas3D * canvas = plater->get_view3D_canvas3D(); + GLGizmosManager &manager = canvas->get_gizmos_manager(); + if (manager.get_current_type() != m_input.gizmo_type) // GLGizmosManager::EType::svg + manager.open_gizmo(m_input.gizmo_type); + + // redraw scene + canvas->reload_scene(true); +} + +CreateSurfaceVolumeJob::CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input) : m_input(std::move(input)) {} +void CreateSurfaceVolumeJob::process(Ctl &ctl) +{ + if (!check(m_input)) + throw JobException("Bad input data for CreateSurfaceVolumeJob."); + m_result = cut_surface(*m_input.base, m_input); // was_canceled(ctl, *m_input.base) +} +void CreateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + if (!_finalize(canceled, eptr, *m_input.base)) + return; + create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.transform, *m_input.base, m_input.gizmo_type); +} + + +CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input) : m_input(std::move(input)) {} + +void CreateVolumeJob::process(Ctl &ctl) +{ + if (!check(m_input)) + throw std::runtime_error("Bad input data for EmbossCreateVolumeJob."); + m_result = create_mesh(*m_input.base); +} + +void CreateVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + if (!_finalize(canceled, eptr, *m_input.base)) + return; + if (m_result.its.empty()) return create_message("Can't create empty volume."); + create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.trmat, *m_input.base, m_input.gizmo_type); +} +/// Update Volume +TriangleMesh create_mesh_per_glyph(DataBase &input) +{ + // method use square of coord stored into int64_t + // static_assert(std::is_same()); + const EmbossShape &shape = input.create_shape(); + if (shape.shapes_with_ids.empty()) return {}; + + // Precalculate bounding boxes of glyphs + // Separate lines of text to vector of Bounds + assert(get_count_lines(shape.shapes_with_ids) == input.text_lines.size()); + size_t count_lines = input.text_lines.size(); + std::vector bbs = create_line_bounds(shape.shapes_with_ids, count_lines); + + double depth = shape.projection.depth / shape.scale; + auto scale_tr = Eigen::Scaling(shape.scale); + + // half of font em size for direction of letter emboss + // double em_2_mm = prop.size_in_mm / 2.; // TODO: fix it + double em_2_mm = 5.; + coord_t em_2_polygon = static_cast(std::round(scale_(em_2_mm))); + + size_t s_i_offset = 0; // shape index offset(for next lines) + indexed_triangle_set result; + for (size_t text_line_index = 0; text_line_index < input.text_lines.size(); ++text_line_index) { + const BoundingBoxes &line_bbs = bbs[text_line_index]; + const TextLine & line = input.text_lines[text_line_index]; + PolygonPoints samples = sample_slice(line, line_bbs, shape.scale); + std::vector angles = calculate_angles(em_2_polygon, samples, line.polygon); + + for (size_t i = 0; i < line_bbs.size(); ++i) { + const BoundingBox &letter_bb = line_bbs[i]; + if (!letter_bb.defined) continue; + + Vec2d to_zero_vec = letter_bb.center().cast() * shape.scale; // [in mm] + float surface_offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (-shape.projection.depth + SAFE_SURFACE_OFFSET); + + if (input.from_surface.has_value()) surface_offset += *input.from_surface; + + Eigen::Translation to_zero(-to_zero_vec.x(), 0., static_cast(surface_offset)); + + const double & angle = angles[i]; + Eigen::AngleAxisd rotate(angle + M_PI_2, Vec3d::UnitY()); + + const PolygonPoint & sample = samples[i]; + Vec2d offset_vec = unscale(sample.point); // [in mm] + Eigen::Translation offset_tr(offset_vec.x(), 0., -offset_vec.y()); + Transform3d tr = offset_tr * rotate * to_zero * scale_tr; + + const ExPolygons &letter_shape = shape.shapes_with_ids[s_i_offset + i].expoly; + assert(get_extents(letter_shape) == letter_bb); + auto projectZ = std::make_unique(depth); + ProjectTransform project(std::move(projectZ), tr); + indexed_triangle_set glyph_its = polygons2model(letter_shape, project); + its_merge(result, std::move(glyph_its)); + + if (((s_i_offset + i) % 15)) return {}; + } + s_i_offset += line_bbs.size(); + +#ifdef STORE_SAMPLING + { // Debug store polygon + // std::string stl_filepath = "C:/data/temp/line" + std::to_string(text_line_index) + "_model.stl"; + // bool suc = its_write_stl_ascii(stl_filepath.c_str(), "label", result); + + BoundingBox bbox = get_extents(line.polygon); + std::string file_path = "C:/data/temp/line" + std::to_string(text_line_index) + "_letter_position.svg"; + SVG svg(file_path, bbox); + svg.draw(line.polygon); + int32_t radius = bbox.size().x() / 300; + for (size_t i = 0; i < samples.size(); i++) { + const PolygonPoint &pp = samples[i]; + const Point & p = pp.point; + svg.draw(p, "green", radius); + std::string label = std::string(" ") + tc.text[i]; + svg.draw_text(p, label.c_str(), "black"); + + double a = angles[i]; + double length = 3.0 * radius; + Point n(length * std::cos(a), length * std::sin(a)); + svg.draw(Slic3r::Line(p - n, p + n), "Lime"); + } + } +#endif // STORE_SAMPLING + } + return TriangleMesh(std::move(result)); +} + + + +TriangleMesh try_create_mesh(DataBase &input) +{ + if (!input.text_lines.empty()) { + TriangleMesh tm = create_mesh_per_glyph(input); + if (!tm.empty()) return tm; + } + + ExPolygons shapes = create_shape(input); + if (shapes.empty()) return {}; + + // NOTE: SHAPE_SCALE is applied in ProjectZ + double scale = input.shape.scale; + double depth = input.shape.projection.depth / scale; + auto projectZ = std::make_unique(depth); + float offset = input.is_outside ? -SAFE_SURFACE_OFFSET : (SAFE_SURFACE_OFFSET - input.shape.projection.depth); + if (input.from_surface.has_value()) offset += *input.from_surface; + Transform3d tr = Eigen::Translation(0., 0., static_cast(offset)) * Eigen::Scaling(scale); + ProjectTransform project(std::move(projectZ), tr); + + return TriangleMesh(polygons2model(shapes, project)); +} + +TriangleMesh create_default_mesh() +{ + // When cant load any font use default object loaded from file + std::string path = Slic3r::resources_dir() + "/data/embossed_text.obj"; + TriangleMesh triangle_mesh; + std::string message; + ObjInfo obj_info; + if (!load_obj(path.c_str(), &triangle_mesh, obj_info, message)) { + // when can't load mesh use cube + return TriangleMesh(its_make_cube(36., 4., 2.5)); + } + return triangle_mesh; +} + +TriangleMesh create_mesh(DataBase &input) +{ + // It is neccessary to create some shape + // Emboss text window is opened by creation new emboss text object + TriangleMesh result = try_create_mesh(input); + + if (result.its.empty()) { + result = create_default_mesh(); + create_message("It is used default volume for embossed text, try to change text or font to fix it."); + // only info + /*ctl.call_on_main_thread([]() { + create_message("It is used default volume for embossed text, try to change text or font to fix it."); + });*/ + } + + assert(!result.its.empty()); + return result; +} + +void create_volume( + TriangleMesh &&mesh, const ObjectID &object_id, const ModelVolumeType type, const std::optional &trmat, const DataBase &data, unsigned char gizmo_type) +{ + GUI_App & app = wxGetApp(); + Plater * plater = app.plater(); + ObjectList * obj_list = app.obj_list(); + GLCanvas3D * canvas = plater->get_view3D_canvas3D(); + ModelObjectPtrs &objects = plater->model().objects; + + ModelObject *obj = nullptr; + size_t object_idx = 0; + for (; object_idx < objects.size(); ++object_idx) { + ModelObject *o = objects[object_idx]; + if (o->id() == object_id) { + obj = o; + break; + } + } + + // Parent object for text volume was propably removed. + // Assumption: User know what he does, so text volume is no more needed. + if (obj == nullptr) return create_message("Bad object to create volume."); + + if (mesh.its.empty()) return create_message("Can't create empty volume."); + + plater->take_snapshot(_u8L("Add Emboss text Volume")); + + BoundingBoxf3 instance_bb; + if (!trmat.has_value()) { + // used for align to instance + size_t instance_index = 0; // must exist + instance_bb = obj->instance_bounding_box(instance_index); + } + + // NOTE: be carefull add volume also center mesh !!! + // So first add simple shape(convex hull is also calculated) + ModelVolume *volume = obj->add_volume(make_cube(1., 1., 1.), type); + + // TODO: Refactor to create better way to not set cube at begining + // Revert mesh centering by set mesh after add cube + volume->set_mesh(std::move(mesh)); + volume->calculate_convex_hull(); + + // set a default extruder value, since user can't add it manually + volume->config.set_key_value("extruder", new ConfigOptionInt(0)); + + // do not allow model reload from disk + volume->source.is_from_builtin_objects = true; + + volume->name = data.volume_name; // copy + + if (trmat.has_value()) { + volume->set_transformation(*trmat); + } else { + assert(!data.shape.projection.use_surface); + // Create transformation for volume near from object(defined by glVolume) + // Transformation is inspired add generic volumes in ObjectList::load_generic_subobject + Vec3d volume_size = volume->mesh().bounding_box().size(); + // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. + Vec3d offset_tr(0, // center of instance - Can't suggest width of text before it will be created + -instance_bb.size().y() / 2 - volume_size.y() / 2, // under + volume_size.z() / 2 - instance_bb.size().z() / 2); // lay on bed + // use same instance as for calculation of instance_bounding_box + Transform3d tr = obj->instances.front()->get_transformation().get_matrix_no_offset().inverse(); + Transform3d volume_trmat = tr * Eigen::Translation3d(offset_tr); + volume->set_transformation(volume_trmat); + } + + data.write(*volume); + + // update printable state on canvas + if (type == ModelVolumeType::MODEL_PART) { + volume->get_object()->ensure_on_bed(); + canvas->update_instance_printable_state_for_object(object_idx); + } + + // update volume name in object list + // updata selection after new volume added + // change name of volume in right panel + // select only actual volume + // when new volume is created change selection to this volume + auto add_to_selection = [volume](const ModelVolume *vol) { return vol == volume; }; + wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(object_idx, add_to_selection); + if (!sel.IsEmpty()) obj_list->select_item(sel.front()); + + obj_list->selection_changed(); + + // Now is valid text volume selected open emboss gizmo + GLGizmosManager &manager = canvas->get_gizmos_manager(); + if (manager.get_current_type() != gizmo_type) manager.open_gizmo(gizmo_type); + + // update model and redraw scene + // canvas->reload_scene(true); + plater->update(); +} + +OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range) +{ + double min_z = z_range.first - safe_extension; + double max_z = z_range.second + safe_extension; + assert(min_z < max_z); + // range between min and max value + double projection_size = max_z - min_z; + Matrix3d transformation_for_vector = tr.linear(); + // Projection must be negative value. + // System of text coordinate + // X .. from left to right + // Y .. from bottom to top + // Z .. from text to eye + Vec3d untransformed_direction(0., 0., projection_size); + Vec3d project_direction = transformation_for_vector * untransformed_direction; + + // Projection is in direction from far plane + tr.translate(Vec3d(0., 0., min_z)); + tr.scale(shape_scale); + return OrthoProject(tr, project_direction); +} + +OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut) +{ + float front_move = (is_outside) ? emboss : SAFE_SURFACE_OFFSET, back_move = -((is_outside) ? SAFE_SURFACE_OFFSET : emboss); + its_transform(cut, tr.pretranslate(Vec3d(0., 0., front_move))); + Vec3d from_front_to_back(0., 0., back_move - front_move); + return OrthoProject3d(from_front_to_back); +} + +indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transform3d &tr, const SurfaceVolumeData::ModelSources &sources, DataBase &input) +{ + assert(!sources.empty()); + BoundingBox bb = get_extents(shapes); + double shape_scale = input.shape.scale; + + const SurfaceVolumeData::ModelSource *biggest = &sources.front(); + + size_t biggest_count = 0; + // convert index from (s)ources to (i)ndexed (t)riangle (s)ets + std::vector s_to_itss(sources.size(), std::numeric_limits::max()); + std::vector itss; + itss.reserve(sources.size()); + for (const SurfaceVolumeData::ModelSource &s : sources) { + Transform3d mesh_tr_inv = s.tr.inverse(); + Transform3d cut_projection_tr = mesh_tr_inv * tr; + std::pair z_range{0., 1.}; + OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + // copy only part of source model + indexed_triangle_set its = Slic3r::its_cut_AoI(s.mesh->its, bb, cut_projection); + if (its.indices.empty()) continue; + if (biggest_count < its.vertices.size()) { + biggest_count = its.vertices.size(); + biggest = &s; + } + size_t source_index = &s - &sources.front(); + size_t its_index = itss.size(); + s_to_itss[source_index] = its_index; + itss.emplace_back(std::move(its)); + } + if (itss.empty()) return {}; + + Transform3d tr_inv = biggest->tr.inverse(); + Transform3d cut_projection_tr = tr_inv * tr; + + size_t itss_index = s_to_itss[biggest - &sources.front()]; + BoundingBoxf3 mesh_bb = bounding_box(itss[itss_index]); + for (const SurfaceVolumeData::ModelSource &s : sources) { + itss_index = s_to_itss[&s - &sources.front()]; + if (itss_index == std::numeric_limits::max()) continue; + if (&s == biggest) continue; + + Transform3d tr = s.tr * tr_inv; + bool fix_reflected = true; + indexed_triangle_set &its = itss[itss_index]; + its_transform(its, tr, fix_reflected); + BoundingBoxf3 its_bb = bounding_box(its); + mesh_bb.merge(its_bb); + } + + // tr_inv = transformation of mesh inverted + Transform3d emboss_tr = cut_projection_tr.inverse(); + BoundingBoxf3 mesh_bb_tr = mesh_bb.transformed(emboss_tr); + std::pair z_range{mesh_bb_tr.min.z(), mesh_bb_tr.max.z()}; + OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + float projection_ratio = (-z_range.first + safe_extension) / (z_range.second - z_range.first + 2 * safe_extension); + + ExPolygons shapes_data; // is used only when text is reflected to reverse polygon points order + const ExPolygons *shapes_ptr = &shapes; + bool is_text_reflected = Slic3r::has_reflection(tr); + if (is_text_reflected) { + // revert order of points in expolygons + // CW --> CCW + shapes_data = shapes; // copy + for (ExPolygon &shape : shapes_data) { + shape.contour.reverse(); + for (Slic3r::Polygon &hole : shape.holes) hole.reverse(); + } + shapes_ptr = &shapes_data; + } + + // Use CGAL to cut surface from triangle mesh + SurfaceCut cut = Slic3r::cut_surface(*shapes_ptr, itss, cut_projection, projection_ratio); + + if (is_text_reflected) { + for (SurfaceCut::Contour &c : cut.contours) std::reverse(c.begin(), c.end()); + for (Vec3i32 &t : cut.indices) std::swap(t[0], t[1]); + } + + if (cut.empty()) return {}; // There is no valid surface for text projection. + // if (was_canceled()) return {}; + + // !! Projection needs to transform cut + OrthoProject3d projection = create_emboss_projection(input.is_outside, input.shape.projection.depth, emboss_tr, cut); + return cut2model(cut, projection); +} + +TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &input2) +{ + // Precalculate bounding boxes of glyphs + // Separate lines of text to vector of Bounds + const EmbossShape &es = input1.create_shape(); + // if (was_canceled()) return {}; + if (es.shapes_with_ids.empty()) { + throw JobException(_u8L("Font doesn't have any shape for given text.").c_str()); + } + assert(get_count_lines(es.shapes_with_ids) == input1.text_lines.size()); + size_t count_lines = input1.text_lines.size(); + std::vector bbs = create_line_bounds(es.shapes_with_ids, count_lines); + + // half of font em size for direction of letter emboss + double em_2_mm = 5.; // TODO: fix it + int32_t em_2_polygon = static_cast(std::round(scale_(em_2_mm))); + + size_t s_i_offset = 0; // shape index offset(for next lines) + indexed_triangle_set result; + for (size_t text_line_index = 0; text_line_index < input1.text_lines.size(); ++text_line_index) { + const BoundingBoxes &line_bbs = bbs[text_line_index]; + const TextLine & line = input1.text_lines[text_line_index]; + PolygonPoints samples = sample_slice(line, line_bbs, es.scale); + std::vector angles = calculate_angles(em_2_polygon, samples, line.polygon); + + for (size_t i = 0; i < line_bbs.size(); ++i) { + const BoundingBox &glyph_bb = line_bbs[i]; + if (!glyph_bb.defined) continue; + + const double &angle = angles[i]; + auto rotate = Eigen::AngleAxisd(angle + M_PI_2, Vec3d::UnitY()); + + const PolygonPoint &sample = samples[i]; + Vec2d offset_vec = unscale(sample.point); // [in mm] + auto offset_tr = Eigen::Translation(offset_vec.x(), 0., -offset_vec.y()); + + ExPolygons glyph_shape = es.shapes_with_ids[s_i_offset + i].expoly; + assert(get_extents(glyph_shape) == glyph_bb); + + Point offset(-glyph_bb.center().x(), 0); + for (ExPolygon &s : glyph_shape) s.translate(offset); + + Transform3d modify = offset_tr * rotate; + Transform3d tr = input2.transform * modify; + indexed_triangle_set glyph_its = cut_surface_to_its(glyph_shape, tr, input2.sources, input1); + // move letter in volume on the right position + its_transform(glyph_its, modify); + + // Improve: union instead of merge + its_merge(result, std::move(glyph_its)); + + if (((s_i_offset + i) % 15)) return {}; + } + s_i_offset += line_bbs.size(); + } + + // if (was_canceled()) return {}; + if (result.empty()) { + UpdateSurfaceVolumeJob::is_use_surfae_error = true; + throw JobException(_u8L("There is no valid surface for text projection.").c_str()); + } + return TriangleMesh(std::move(result)); +} + +// input can't be const - cache of font +TriangleMesh cut_surface(DataBase &input1, const SurfaceVolumeData &input2) +{ + if (!input1.text_lines.empty()) + return cut_per_glyph_surface(input1, input2); + + ExPolygons shapes = create_shape(input1); + // if (was_canceled()) return {}; + if (shapes.empty()) { + throw JobException(_u8L("Font doesn't have any shape for given text.").c_str()); + } + indexed_triangle_set its = cut_surface_to_its(shapes, input2.transform, input2.sources, input1); + // if (was_canceled()) return {}; + if (its.empty()) { + UpdateSurfaceVolumeJob::is_use_surfae_error = true; + throw JobException(_u8L("There is no valid surface for text projection.").c_str()); + } + return TriangleMesh(std::move(its)); +} + +bool start_update_volume(DataUpdate &&data, const ModelVolume &volume, const Selection &selection, RaycastManager &raycaster) +{ + assert(data.volume_id == volume.id()); + + // check cutting from source mesh + bool &use_surface = data.base->shape.projection.use_surface; + if (use_surface && volume.is_the_only_one_part()) use_surface = false; + + std::unique_ptr job = nullptr; + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_volume_sources(volume); + if (sources.empty()) return false; + + Transform3d volume_tr = volume.get_matrix(); + const std::optional &fix_3mf = volume.emboss_shape->fix_3mf_tr; + if (fix_3mf.has_value()) volume_tr = volume_tr * fix_3mf->inverse(); + + // when it is new applying of use surface than move origin onto surfaca + if (!volume.emboss_shape->projection.use_surface) { + auto offset = calc_surface_offset(selection, raycaster); + if (offset.has_value()) volume_tr *= Eigen::Translation(*offset); + } + + UpdateSurfaceVolumeData surface_data{std::move(data), {volume_tr, std::move(sources)}}; + job = std::make_unique(std::move(surface_data)); + } else { + job = std::make_unique(std::move(data)); + } +#ifndef EXECUTE_UPDATE_ON_MAIN_THREAD + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + auto is_idle = worker.is_idle(); + return queue_job(worker, std::move(job)); +#else + // Run Job on main thread (blocking) - ONLY DEBUG + return execute_job(std::move(job)); +#endif // EXECUTE_UPDATE_ON_MAIN_THREAD +} + +bool start_create_object_job(const CreateVolumeParams &input, DataBasePtr emboss_data, const Vec2d &coor) +{ + const Pointfs & bed_shape = input.build_volume.printable_area(); + DataCreateObject m_input{std::move(emboss_data), coor, input.camera, bed_shape, input.gizmo_type, input.angle}; + + //// Fix: adding text on print bed with style containing use_surface + if (m_input.base->shape.projection.use_surface) + // // Til the print bed is flat using surface for Object is useless + m_input.base->shape.projection.use_surface = false; + + auto job = std::make_unique(std::move(m_input)); + return queue_job(input.worker, std::move(job)); +} + +const GLVolume *find_closest(const Selection &selection, const Vec2d &screen_center, const Camera &camera, const ModelObjectPtrs &objects, Vec2d *closest_center) +{ + assert(closest_center != nullptr); + const GLVolume * closest = nullptr; + const Selection::IndicesList &indices = selection.get_volume_idxs(); + assert(!indices.empty()); // no selected volume + if (indices.empty()) return closest; + + double center_sq_distance = std::numeric_limits::max(); + for (unsigned int id : indices) { + const GLVolume *gl_volume = selection.get_volume(id); + if (const ModelVolume *volume = get_model_volume(*gl_volume, objects); volume == nullptr || !volume->is_model_part()) continue; + Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *gl_volume); + Vec2d c = hull.centroid().cast(); + Vec2d d = c - screen_center; + bool is_bigger_x = std::fabs(d.x()) > std::fabs(d.y()); + if ((is_bigger_x && d.x() * d.x() > center_sq_distance) || (!is_bigger_x && d.y() * d.y() > center_sq_distance)) continue; + + double distance = d.squaredNorm(); + if (center_sq_distance < distance) continue; + center_sq_distance = distance; + + *closest_center = c; + closest = gl_volume; + } + return closest; +} + +bool start_create_volume_without_position(CreateVolumeParams &input, DataBasePtr data) +{ + assert(data != nullptr); + if (data == nullptr) return false; + if (!check(input)) return false; + + // select position by camera position and view direction + const Selection &selection = input.canvas.get_selection(); + int object_idx = selection.get_object_idx(); + + Size s = input.canvas.get_canvas_size(); + Vec2d screen_center(s.get_width() / 2., s.get_height() / 2.); + const ModelObjectPtrs &objects = selection.get_model()->objects; + + // No selected object so create new object + if (selection.is_empty() || object_idx < 0 || static_cast(object_idx) >= objects.size()) + // create Object on center of screen + // when ray throw center of screen not hit bed it create object on center of bed + return start_create_object_job(input, std::move(data), screen_center); + + // create volume inside of selected object + Vec2d coor; + const Camera &camera = wxGetApp().plater()->get_camera(); + input.gl_volume = find_closest(selection, screen_center, camera, objects, &coor); + if (input.gl_volume == nullptr) + return start_create_object_job(input, std::move(data), screen_center); + else { + return start_create_volume_on_surface_job(input, std::move(data), coor); + } +} + +bool start_create_volume_job( + Worker &worker, const ModelObject &object, const std::optional &volume_tr, DataBasePtr data, ModelVolumeType volume_type, unsigned char gizmo_type) +{ + bool & use_surface = data->shape.projection.use_surface; + std::unique_ptr job; + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_sources(object.volumes); + if (sources.empty() || !volume_tr.has_value()) { + use_surface = false; + } else { + SurfaceVolumeData sfvd{*volume_tr, std::move(sources)}; + CreateSurfaceVolumeData surface_data{std::move(sfvd), std::move(data), volume_type, object.id(), gizmo_type}; + job = std::make_unique(std::move(surface_data)); + } + } + if (!use_surface) { + // create volume + DataCreateVolume create_volume_data{std::move(data), volume_type, object.id(), volume_tr, gizmo_type}; + job = std::make_unique(std::move(create_volume_data)); + } + return queue_job(worker, std::move(job)); +} + +bool start_create_volume_on_surface_job(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos) +{ + auto on_bad_state = [&input](DataBasePtr data_, const ModelObject *object = nullptr) { + // In centroid of convex hull is not hit with object. e.g. torid + // soo create transfomation on border of object + + // there is no point on surface so no use of surface will be applied + if (data_->shape.projection.use_surface) data_->shape.projection.use_surface = false; + + auto gizmo_type = static_cast(input.gizmo_type); + return start_create_volume_job(input.worker, *object, {}, std::move(data_), input.volume_type, gizmo_type); + }; + const Model * model = input.canvas.get_model(); + const ModelObjectPtrs &objects = model->objects; + const ModelVolume * mv = get_model_volume(*input.gl_volume, objects); + if (mv == nullptr) + return false; + const ModelInstance *instance = get_model_instance(*input.gl_volume, objects); + assert(instance != nullptr); + if (instance == nullptr) + return false; + const ModelObject *object = mv->get_object(); + assert(object != nullptr); + if (object == nullptr) + return false; + + input.on_register_mesh_pick(); // modify by bbs + + std::optional hit = ray_from_camera(input.raycaster, mouse_pos, input.camera, &input.raycast_condition); + // context menu for add text could be open only by right click on an + // object. After right click, object is selected and object_idx is set + // also hit must exist. But there is options to add text by object list + if (!hit.has_value()) { // modify by bbs + return start_create_object_job(input, std::move(data), mouse_pos); // return on_bad_state(std::move(data), object); + } + + // Create result volume transformation + Transform3d surface_trmat = create_transformation_onto_surface(hit->position, hit->normal, UP_LIMIT); + apply_transformation(input.angle, input.distance, surface_trmat); + Transform3d transform = instance->get_matrix().inverse() * surface_trmat; + auto gizmo_type = static_cast(input.gizmo_type); + // Try to cast ray into scene and find object for add volume + return start_create_volume_job(input.worker, *object, transform, std::move(data), input.volume_type, gizmo_type); +} + +bool start_create_volume(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos) +{ + if (data == nullptr) return false; + if (!check(input)) return false; + + if (input.gl_volume == nullptr || !input.gl_volume->selected) + // object is not under mouse position soo create object on plater + return start_create_object_job(input, std::move(data), mouse_pos); + else { // modify by bbs + return start_create_volume_on_surface_job(input, std::move(data), mouse_pos); + } +} + + +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume &volume) +{ + const ModelVolumePtrs &volumes = volume.get_object()->volumes; + // no other volume in object + if (volumes.size() <= 1) return {}; + return create_sources(volumes, volume.id().id); +} + + +SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id) +{ + SurfaceVolumeData::ModelSources result; + result.reserve(volumes.size() - 1); + for (const ModelVolume *v : volumes) { + if (text_volume_id.has_value() && v->id().id == *text_volume_id) continue; + // skip modifiers and negative volumes, ... + if (!v->is_model_part()) continue; + const TriangleMesh &tm = v->mesh(); + if (tm.empty()) continue; + if (tm.its.empty()) continue; + result.push_back({v->get_mesh_shared_ptr(), v->get_matrix()}); + } + return result; +} + +} +} // namespace Slic3r::GUI +} // namespace Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/Jobs/EmbossJob.hpp b/src/slic3r/GUI/Jobs/EmbossJob.hpp new file mode 100644 index 000000000..990b13228 --- /dev/null +++ b/src/slic3r/GUI/Jobs/EmbossJob.hpp @@ -0,0 +1,351 @@ +#ifndef slic3r_EmbossJob_hpp_ +#define slic3r_EmbossJob_hpp_ + +#include +#include +#include +#include +#include +#include // ExPolygonsWithIds +#include "libslic3r/Point.hpp" // Transform3d +#include "libslic3r/ObjectID.hpp" + +#include "slic3r/GUI/Camera.hpp" +//#include "slic3r/GUI/TextLines.hpp" +#include "slic3r/Utils/RaycastManager.hpp" +#include "JobNew.hpp" + +namespace Slic3r { +class TriangleMesh; +class ModelVolume; +class ModelObject; +enum class ModelVolumeType : int; +class BuildVolume; +namespace GUI { +//class Plater; +class GLCanvas3D; +class Worker; +class Selection; +namespace Emboss { +class DataBase +{ +public: + DataBase(const std::string &volume_name, std::shared_ptr> cancel) : volume_name(volume_name), cancel(std::move(cancel)) {} + DataBase(const std::string &volume_name, std::shared_ptr> cancel, EmbossShape &&shape) + : volume_name(volume_name), cancel(std::move(cancel)), shape(std::move(shape)) + {} + DataBase(DataBase &&) = default; + virtual ~DataBase() = default; + + /// + /// Create shape + /// e.g. Text extract glyphs from font + /// Not 'const' function because it could modify shape + /// + virtual EmbossShape &create_shape() { return shape; }; + + /// + /// Write data how to reconstruct shape to volume + /// + /// Data object for store emboss params + virtual void write(ModelVolume &volume) const; + + // Define projection move + // True (raised) .. move outside from surface (MODEL_PART) + // False (engraved).. move into object (NEGATIVE_VOLUME) + bool is_outside = true; + + // Define per letter projection on one text line + // [optional] It is not used when empty + Slic3r::Emboss::TextLines text_lines = {}; + // [optional] Define distance for surface + // It is used only for flat surface (not cutted) + // Position of Zero(not set value) differ for MODEL_PART and NEGATIVE_VOLUME + std::optional from_surface; + // new volume name + std::string volume_name; + // flag that job is canceled + // for time after process. + std::shared_ptr> cancel; + // shape to emboss + EmbossShape shape; +}; + +struct DataCreateVolumeUtil : public DataBase // modfiy bu bbs //struct DataCreateVolume : public DataBase +{ + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; + + // new created volume transformation + Transform3d trmat; +}; +using DataBasePtr = std::unique_ptr; +/// +/// Hold neccessary data to update embossed text object in job +/// +struct DataUpdate +{ + // Hold data about shape + DataBasePtr base; + + // unique identifier of volume to change + ObjectID volume_id; + + // Used for prevent flooding Undo/Redo stack on slider. + bool make_snapshot; +}; + + struct CreateVolumeParams +{ + GLCanvas3D &canvas; + // Direction of ray into scene + const Camera &camera; + // To put new object on the build volume + const BuildVolume &build_volume; + // used to emplace job for execution + Worker &worker; + // Contain AABB trees from scene + typedef std::function register_mesh_pick; + register_mesh_pick on_register_mesh_pick{nullptr}; + RaycastManager &raycaster; + RaycastManager::AllowVolumes& raycast_condition; + // New created volume type + ModelVolumeType volume_type; + // Define which gizmo open on the success + unsigned char gizmo_type; // GLGizmosManager::EType + // Volume define object to add new volume + const GLVolume *gl_volume; + // Contain AABB trees from scene + // RaycastManager &raycaster; + // Wanted additionl move in Z(emboss) direction of new created volume + std::optional distance = {}; + // Wanted additionl rotation around Z of new created volume + std::optional angle = {}; +}; +struct DataCreateObject +{ + // Hold data about shape + DataBasePtr base; + // define position on screen where to create object + Vec2d screen_coor; + // projection property + const Camera &camera; + // shape of bed in case of create volume on bed + std::vector bed_shape; + // Define which gizmo open on the success + unsigned char gizmo_type; + // additionl rotation around Z axe, given by style settings + std::optional angle = {}; +}; + + /// +/// Hold neccessary data to create ModelVolume in job +/// Volume is created on the surface of existing volume in object. +/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! +/// +struct DataCreateVolume +{ + // Hold data about shape + DataBasePtr base; + + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; + + // new created volume transformation + std::optional trmat; + + // Define which gizmo open on the success + unsigned char gizmo_type; +}; + +struct SurfaceVolumeData +{ + // Transformation of volume inside of object + Transform3d transform; + + struct ModelSource + { + // source volumes + std::shared_ptr mesh; + // Transformation of volume inside of object + Transform3d tr; + }; + using ModelSources = std::vector; + ModelSources sources; +}; + + + /// +/// Hold neccessary data to update embossed text object in job +/// +struct UpdateSurfaceVolumeData : public DataUpdate, public SurfaceVolumeData +{}; + +static bool was_canceled(const JobNew::Ctl &ctl, const DataBase &base); +static bool exception_process(std::exception_ptr &eptr); +static bool finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input); +/// +/// Update text volume to use surface from object +/// +class UpdateSurfaceVolumeJob : public JobNew +{ + UpdateSurfaceVolumeData m_input; + TriangleMesh m_result; + +public: + // move params to private variable + explicit UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; + static bool is_use_surfae_error; +}; + +/// +/// Update text shape in existing text volume +/// Predict that there is only one runnig(not canceled) instance of it +/// +class UpdateJob : public JobNew +{ + DataUpdate m_input; + TriangleMesh m_result; + +public: + // move params to private variable + explicit UpdateJob(DataUpdate &&input); + + /// + /// Create new embossed volume by m_input data and store to m_result + /// + /// Control containing cancel flag + void process(Ctl &ctl) override; + + /// + /// Update volume - change object_id + /// + /// Was process canceled. + /// NOTE: Be carefull it doesn't care about + /// time between finished process and started finalize part. + /// unused + void finalize(bool canceled, std::exception_ptr &eptr) override; + + /// + /// Update text volume + /// + /// Volume to be updated + /// New Triangle mesh for volume + /// Data to write into volume + static void update_volume(ModelVolume *volume, TriangleMesh &&mesh, const DataBase &base); +}; + +/// +/// Create new TextObject on the platter +/// Should not be stopped +/// +class CreateObjectJob : public JobNew +{ + DataCreateObject m_input; + TriangleMesh m_result; + Transform3d m_transformation; + +public: + explicit CreateObjectJob(DataCreateObject &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +struct Texture +{ + unsigned id{0}; + unsigned width{0}; + unsigned height{0}; +}; + +/// +/// Hold neccessary data to create(cut) volume from surface object in job +/// +struct CreateSurfaceVolumeData : public SurfaceVolumeData +{ + // Hold data about shape + DataBasePtr base; + // define embossed volume type + ModelVolumeType volume_type; + // parent ModelObject index where to create volume + ObjectID object_id; + // Define which gizmo open on the success + unsigned char gizmo_type; +}; +class CreateSurfaceVolumeJob : public JobNew +{ + CreateSurfaceVolumeData m_input; + TriangleMesh m_result; + +public: + explicit CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +class CreateVolumeJob : public JobNew +{ + DataCreateVolume m_input; + TriangleMesh m_result; + +public: + explicit CreateVolumeJob(DataCreateVolume &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; +static bool check(unsigned char gizmo_type); +static bool check(const DataBase &input, bool check_fontfile, bool use_surface = false); +static bool check(const CreateVolumeParams &input); +static bool check(const DataCreateObject &input); +static bool check(const DataUpdate &input, bool is_main_thread = false, bool use_surface = false); +static bool check(const CreateSurfaceVolumeData &input, bool is_main_thread = false); +static bool check(const DataCreateVolume &input, bool is_main_thread = false); +static bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread = false); +bool start_create_object_job(const CreateVolumeParams &input, DataBasePtr emboss_data, const Vec2d &coor); +bool start_create_volume_without_position(CreateVolumeParams &input, DataBasePtr data); +bool start_create_volume_job( Worker &worker, const ModelObject &object, const std::optional &volume_tr, DataBasePtr data, ModelVolumeType volume_type, unsigned char gizmo_type); +bool start_create_volume_on_surface_job(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos); +bool start_create_volume(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos); +static ExPolygons create_shape(DataBase &input); +static TriangleMesh create_mesh_per_glyph(DataBase &input); +static TriangleMesh try_create_mesh(DataBase &input); +static TriangleMesh create_mesh(DataBase &input); +static indexed_triangle_set cut_surface_to_its(const ExPolygons &shapes, const Transform3d &tr, const SurfaceVolumeData::ModelSources &sources, DataBase &input); +static TriangleMesh cut_per_glyph_surface(DataBase &input1, const SurfaceVolumeData &input2); +static TriangleMesh cut_surface(DataBase &input1, const SurfaceVolumeData &input2); +static void _update_volume(TriangleMesh &&mesh, const DataUpdate &data, const Transform3d *tr = nullptr); +static void create_volume( + TriangleMesh &&mesh, const ObjectID &object_id, const ModelVolumeType type, const std::optional &trmat, const DataBase &data, unsigned char gizmo_type); +/// Update text volume +/// +/// Volume to be updated +/// New Triangle mesh for volume +/// Data to write into volume +bool start_update_volume(DataUpdate &&data, const ModelVolume &volume, const Selection &selection, RaycastManager &raycaster); + +/// +/// Copied triangles from object to be able create mesh for cut surface from +/// +/// Define embossed volume +/// Source data for cut surface from +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume &volume); +/// +/// Copied triangles from object to be able create mesh for cut surface from +/// +/// Source object volumes for cut surface from +/// Source volume id +/// Source data for cut surface from +SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id = {}); +} +} // namespace Slic3r::GUI +} // namespace Slic3r + +#endif // slic3r_EmbossJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/JobNew.hpp b/src/slic3r/GUI/Jobs/JobNew.hpp new file mode 100644 index 000000000..5d671a845 --- /dev/null +++ b/src/slic3r/GUI/Jobs/JobNew.hpp @@ -0,0 +1,68 @@ +#ifndef JOBNEW_HPP +#define JOBNEW_HPP + +#include +#include +#include + +#include + +#include "libslic3r/libslic3r.h" +#include "ProgressIndicator.hpp" + +namespace Slic3r { namespace GUI { + +// A class representing a job that is to be run in the background, not blocking +// the main thread. Running it is up to a Worker object (see Worker interface) +class JobNew { +public: + + enum JobPrepareState { + PREPARE_STATE_DEFAULT = 0, + PREPARE_STATE_MENU = 1, + }; + + // A controller interface that informs the job about cancellation and + // makes it possible for the job to advertise its status. + class Ctl { + public: + virtual ~Ctl() = default; + + // status update, to be used from the work thread (process() method) + virtual void update_status(int st, const std::string &msg = "") = 0; + + // Returns true if the job was asked to cancel itself. + virtual bool was_canceled() const = 0; + + // Orca: + virtual void clear_percent() = 0; + virtual void show_error_info(const std::string &msg, int code, const std::string &description, const std::string &extra) = 0; + + // Execute a functor on the main thread. Note that the exact time of + // execution is hard to determine. This can be used to make modifications + // on the UI, like displaying some intermediate results or modify the + // cursor. + // This function returns a std::future object which enables the + // caller to optionally wait for the main thread to finish the function call. + virtual std::future call_on_main_thread(std::function fn) = 0; + }; + + virtual ~JobNew() = default; + + // The method where the actual work of the job should be defined. This is + // run on the worker thread. + virtual void process(Ctl &ctl) = 0; + + // Launched when the job is finished on the UI thread. + // If the job was cancelled, the first parameter will have a true value. + // Exceptions occuring in process() are redirected from the worker thread + // into the main (UI) thread. This method receives the exception and can + // handle it properly. Assign nullptr to this second argument before + // function return to prevent further action. Leaving it with a non-null + // value will result in rethrowing by the worker. + virtual void finalize(bool /*canceled*/, std::exception_ptr &) {} +}; + +}} // namespace Slic3r::GUI + +#endif // JOB_HPP diff --git a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp index 13470f7ad..17249d44e 100644 --- a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp +++ b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp @@ -22,11 +22,14 @@ void NotificationProgressIndicator::set_range(int range) void NotificationProgressIndicator::set_cancel_callback(CancelFn fn) { - m_nm->progress_indicator_set_cancel_callback(std::move(fn)); + m_cancelfn = std::move(fn); + m_nm->progress_indicator_set_cancel_callback(m_cancelfn); } void NotificationProgressIndicator::set_progress(int pr) { + if (!pr) + set_cancel_callback(m_cancelfn); m_nm->progress_indicator_set_progress(pr); } diff --git a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp index ba0e89916..f3150ed89 100644 --- a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp +++ b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp @@ -9,6 +9,7 @@ class NotificationManager; class NotificationProgressIndicator: public ProgressIndicator { NotificationManager *m_nm = nullptr; + CancelFn m_cancelfn; public: diff --git a/src/slic3r/GUI/Jobs/PlaterWorker.hpp b/src/slic3r/GUI/Jobs/PlaterWorker.hpp new file mode 100644 index 000000000..8e344e6f7 --- /dev/null +++ b/src/slic3r/GUI/Jobs/PlaterWorker.hpp @@ -0,0 +1,155 @@ +#ifndef PLATERWORKER_HPP +#define PLATERWORKER_HPP + +#include +#include + +#include "Worker.hpp" +#include "BusyCursorJob.hpp" + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/I18N.hpp" + +namespace Slic3r { namespace GUI { + +template +class PlaterWorker: public Worker { + WorkerSubclass m_w; + wxWindow *m_plater; + + class PlaterJob : public JobNew { + std::unique_ptr m_job; + wxWindow *m_plater; + long long m_process_duration; // [ms] + + public: + void process(Ctl &c) override + { + // Ensure that wxWidgets processing wakes up to handle outgoing + // messages in plater's wxIdle handler. Otherwise it might happen + // that the message will only be processed when an event like mouse + // move comes along which might be too late. + struct WakeUpCtl: Ctl { + Ctl &ctl; + WakeUpCtl(Ctl &c) : ctl{c} {} + + void update_status(int st, const std::string &msg = "") override + { + ctl.update_status(st, msg); + wxWakeUpIdle(); + } + + bool was_canceled() const override { return ctl.was_canceled(); } + + std::future call_on_main_thread(std::function fn) override + { + auto ftr = ctl.call_on_main_thread(std::move(fn)); + wxWakeUpIdle(); + + return ftr; + } + + void clear_percent() override { + ctl.clear_percent(); + wxWakeUpIdle(); + } + + void show_error_info(const std::string &msg, int code, const std::string &description, const std::string &extra) override + { + ctl.show_error_info(msg, code, description, extra); + wxWakeUpIdle(); + } + + } wctl{c}; + + CursorSetterRAII busycursor{wctl}; + + using namespace std::chrono; + steady_clock::time_point process_start = steady_clock::now(); + m_job->process(wctl); + steady_clock::time_point process_end = steady_clock::now(); + m_process_duration = duration_cast(process_end - process_start).count(); + } + + void finalize(bool canceled, std::exception_ptr &eptr) override + { + using namespace std::chrono; + steady_clock::time_point finalize_start = steady_clock::now(); + m_job->finalize(canceled, eptr); + steady_clock::time_point finalize_end = steady_clock::now(); + long long finalize_duration = duration_cast(finalize_end - finalize_start).count(); + + BOOST_LOG_TRIVIAL(info) + << std::fixed // do not use scientific notations + << "Job '" << typeid(*m_job).name() << "' " + << "spend " << m_process_duration + finalize_duration << "ms " + << "(process " << m_process_duration << "ms + finalize " << finalize_duration << "ms)"; + + if (eptr) try { + std::rethrow_exception(eptr); + } catch (std::exception &e) { + show_error(m_plater, _L("An unexpected error occured") + ": " + e.what()); + eptr = nullptr; + } + } + + PlaterJob(wxWindow *p, std::unique_ptr j) + : m_job{std::move(j)}, m_plater{p} + { + // TODO: decide if disabling slice button during UI job is what we + // want. + // if (m_plater) + // m_plater->sidebar().enable_buttons(false); + } + + ~PlaterJob() override + { + // TODO: decide if disabling slice button during UI job is what we want. + + // Reload scene ensures that the slice button gets properly + // enabled or disabled after the job finishes, depending on the + // state of slicing. This might be an overkill but works for now. + // if (m_plater) + // m_plater->canvas3D()->reload_scene(false); + } + }; + + EventGuard on_idle_evt; + EventGuard on_paint_evt; + +public: + + template + PlaterWorker(wxWindow *plater, WorkerArgs &&...args) + : m_w{std::forward(args)...} + , m_plater{plater} + // Ensure that messages from the worker thread to the UI thread are + // processed continuously. + , on_idle_evt(plater, wxEVT_IDLE, [this](wxIdleEvent&) { process_events(); }) + , on_paint_evt(plater, wxEVT_PAINT, [this](wxPaintEvent&) { process_events(); }) + { + } + + // Always package the job argument into a PlaterJob + bool push(std::unique_ptr job) override + { + return m_w.push(std::make_unique(m_plater, std::move(job))); + } + + bool is_idle() const override { return m_w.is_idle(); } + void cancel() override { m_w.cancel(); } + void cancel_all() override { m_w.cancel_all(); } + void process_events() override { m_w.process_events(); } + bool wait_for_current_job(unsigned timeout_ms = 0) override + { + return m_w.wait_for_current_job(timeout_ms); + } + bool wait_for_idle(unsigned timeout_ms = 0) override + { + return m_w.wait_for_idle(timeout_ms); + } +}; + +}} // namespace Slic3r::GUI + +#endif // PLATERJOB_HPP diff --git a/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp b/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp new file mode 100644 index 000000000..3ee1672bd --- /dev/null +++ b/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp @@ -0,0 +1,124 @@ +#ifndef THREADSAFEQUEUE_HPP +#define THREADSAFEQUEUE_HPP + +#include +#include +#include +#include +#include + +namespace Slic3r { namespace GUI { + +// Helper structure for overloads of ThreadSafeQueueSPSC::consume_one() +// to block if the queue is empty. +struct BlockingWait +{ + // Timeout to wait for the arrival of new element into the queue. + unsigned timeout_ms = 0; + + // An optional atomic flag to set true if an incoming element gets + // consumed. The flag will be atomically set to true when popping the + // front of the queue. + std::atomic *pop_flag = nullptr; +}; + +// A thread safe queue for one producer and one consumer. +template class Container = std::deque, + class... ContainerArgs> +class ThreadSafeQueueSPSC +{ + std::queue> m_queue; + mutable std::mutex m_mutex; + std::condition_variable m_cond_var; +public: + + // Consume one element, block if the queue is empty. + template bool consume_one(const BlockingWait &blkw, Fn &&fn) + { + static_assert(!std::is_reference_v, ""); + static_assert(std::is_default_constructible_v, ""); + static_assert(std::is_move_assignable_v || std::is_copy_assignable_v, ""); + + T el; + { + std::unique_lock lk{m_mutex}; + + auto pred = [this]{ return !m_queue.empty(); }; + if (blkw.timeout_ms > 0) { + auto timeout = std::chrono::milliseconds(blkw.timeout_ms); + if (!m_cond_var.wait_for(lk, timeout, pred)) + return false; + } + else + m_cond_var.wait(lk, pred); + + if constexpr (std::is_move_assignable_v) + el = std::move(m_queue.front()); + else + el = m_queue.front(); + + m_queue.pop(); + + if (blkw.pop_flag) + // The optional flag is set before the lock us unlocked. + blkw.pop_flag->store(true); + } + + fn(el); + return true; + } + + // Consume one element, return true if consumed, false if queue was empty. + template bool consume_one(Fn &&fn) + { + T el; + { + std::unique_lock lk{m_mutex}; + if (!m_queue.empty()) { + if constexpr (std::is_move_assignable_v) + el = std::move(m_queue.front()); + else + el = m_queue.front(); + + m_queue.pop(); + } else + return false; + } + + fn(el); + + return true; + } + + // Push element into the queue. + template void push(TArgs&&...el) + { + std::lock_guard lk{m_mutex}; + m_queue.emplace(std::forward(el)...); + m_cond_var.notify_one(); + } + + bool empty() const + { + std::lock_guard lk{m_mutex}; + return m_queue.empty(); + } + + size_t size() const + { + std::lock_guard lk{m_mutex}; + return m_queue.size(); + } + + void clear() + { + std::lock_guard lk{m_mutex}; + while (!m_queue.empty()) + m_queue.pop(); + } +}; + +}} // namespace Slic3r::GUI + +#endif // THREADSAFEQUEUE_HPP diff --git a/src/slic3r/GUI/Jobs/Worker.hpp b/src/slic3r/GUI/Jobs/Worker.hpp new file mode 100644 index 000000000..dd2f80c68 --- /dev/null +++ b/src/slic3r/GUI/Jobs/Worker.hpp @@ -0,0 +1,140 @@ +#ifndef PRUSALSICER_WORKER_HPP +#define PRUSALSICER_WORKER_HPP + +#include + +#include "JobNew.hpp" + +namespace Slic3r { namespace GUI { +// #define EXECUTE_UPDATE_ON_MAIN_THREAD // debug execution on main thread +// An interface of a worker that runs jobs on a dedicated worker thread, one +// after the other. It is assumed that every method of this class is called +// from the same main thread. + + #ifdef EXECUTE_UPDATE_ON_MAIN_THREAD +namespace { +// Run Job on main thread (blocking) - ONLY DEBUG +static inline bool execute_job(std::shared_ptr j) +{ + struct MyCtl : public JobNew::Ctl + { + void update_status(int st, const std::string &msg = "") override{}; + bool was_canceled() const override { return false; } + std::future call_on_main_thread(std::function fn) override { return std::future{}; } + } ctl; + j->process(ctl); + wxGetApp().plater()->CallAfter([j]() { + std::exception_ptr e_ptr = nullptr; + j->finalize(false, e_ptr); + }); + return true; +} +} // namespace +#endif +class Worker { +public: + // Queue up a new job after the current one. This call does not block. + // Returns false if the job gets discarded. + virtual bool push(std::unique_ptr job) = 0; + + // Returns true if no job is running, the job queue is empty and no job + // message is left to be processed. This means that nothing is left to + // finalize or take care of in the main thread. + virtual bool is_idle() const = 0; + + // Ask the current job gracefully to cancel. This call is not blocking and + // the job may or may not cancel eventually, depending on its + // implementation. Note that it is not trivial to kill a thread forcefully + // and we don't need that. + virtual void cancel() = 0; + + // This method will delete the queued jobs and cancel the current one. + virtual void cancel_all() = 0; + + // Needs to be called continuously to process events (like status update + // or finalizing of jobs) in the main thread. This can be done e.g. in a + // wxIdle handler. + virtual void process_events() = 0; + + // Wait until the current job finishes. Timeout will only be considered + // if not zero. Returns false if timeout is reached but the job has not + // finished. + virtual bool wait_for_current_job(unsigned timeout_ms = 0) = 0; + + // Wait until the whole job queue finishes. Timeout will only be considered + // if not zero. Returns false only if timeout is reached but the worker has + // not reached the idle state. + virtual bool wait_for_idle(unsigned timeout_ms = 0) = 0; + + // The destructor shall properly close the worker thread. + virtual ~Worker() = default; +}; + +template constexpr bool IsProcessFn = std::is_invocable_v; +template constexpr bool IsFinishFn = std::is_invocable_v; + +// Helper function to use the worker with arbitrary functors. +template>, + class = std::enable_if_t> > +bool queue_job(Worker &w, ProcessFn fn, FinishFn finishfn) +{ + struct LambdaJob: JobNew { + ProcessFn fn; + FinishFn finishfn; + + LambdaJob(ProcessFn pfn, FinishFn ffn) + : fn{std::move(pfn)}, finishfn{std::move(ffn)} + {} + + void process(Ctl &ctl) override { fn(ctl); } + void finalize(bool canceled, std::exception_ptr &eptr) override + { + finishfn(canceled, eptr); + } + }; + + auto j = std::make_unique(std::move(fn), std::move(finishfn)); + return w.push(std::move(j)); +} + +template>> +bool queue_job(Worker &w, ProcessFn fn) +{ + return queue_job(w, std::move(fn), [](bool, std::exception_ptr &) {}); +} + +inline bool queue_job(Worker &w, std::unique_ptr j) +{ + return w.push(std::move(j)); +} + +// Replace the current job queue with a new job. The signature is the same +// as for queue_job(). This cancels all jobs and +// will not wait. The new job will begin after the queue cancels properly. +// Note that this can be called from the UI thread and will not block it if +// the jobs take longer to cancel. +template bool replace_job(Worker &w, Args&& ...args) +{ + w.cancel_all(); + return queue_job(w, std::forward(args)...); +} + +// Cancel the current job and wait for it to actually be stopped. +inline bool stop_current_job(Worker &w, unsigned timeout_ms = 0) +{ + w.cancel(); + return w.wait_for_current_job(timeout_ms); +} + +// Cancel all pending jobs including current one and wait until the worker +// becomes idle. +inline bool stop_queue(Worker &w, unsigned timeout_ms = 0) +{ + w.cancel_all(); + return w.wait_for_idle(timeout_ms); +} + +}} // namespace Slic3r::GUI + +#endif // WORKER_HPP diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 41d70e650..70090c60f 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -883,6 +883,7 @@ void MainFrame::shutdown() #endif // _WIN32 if (m_plater != nullptr) { + m_plater->get_ui_job_worker().cancel_all(); m_plater->stop_jobs(); // Unbinding of wxWidgets event handling in canvases needs to be done here because on MAC, diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 73525be58..17bb6eb17 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -147,8 +147,6 @@ NotificationManager::PopNotification::PopNotification(const NotificationData &n, , m_evt_handler (evt_handler) , m_notification_start (GLCanvas3D::timestamp_now()) { - m_is_dark = wxGetApp().plater()->get_current_canvas3D()->get_dark_mode_status(); - m_ErrorColor = ImVec4(0.9, 0.36, 0.36, 1); m_WarnColor = ImVec4(0.99, 0.69, 0.455, 1); m_NormalColor = ImVec4(0.03, 0.6, 0.18, 1); @@ -158,17 +156,31 @@ NotificationManager::PopNotification::PopNotification(const NotificationData &n, m_WindowBkgColor = ImVec4(1, 1, 1, 1); m_TextColor = ImVec4(.2f, .2f, .2f, 1.0f); m_HyperTextColor = ImVec4(0.03, 0.6, 0.18, 1); +} - m_WindowRadius = 4.0f * wxGetApp().plater()->get_current_canvas3D()->get_scale(); +// We cannot call plater()->get_current_canvas3D() from constructor, so we do it here +void NotificationManager::PopNotification::ensure_ui_inited() +{ + if (!m_is_dark_inited) { + m_is_dark = wxGetApp().plater()->get_current_canvas3D()->get_dark_mode_status(); + m_is_dark_inited = true; + } + + if (!m_WindowRadius_inited) { + m_WindowRadius = 4.0f * wxGetApp().plater()->get_current_canvas3D()->get_scale(); + m_WindowRadius_inited = true; + } } void NotificationManager::PopNotification::on_change_color_mode(bool is_dark) { + m_is_dark_inited = true; m_is_dark = is_dark; } void NotificationManager::PopNotification::use_bbl_theme() { + ensure_ui_inited(); ImGuiStyle &OldStyle = ImGui::GetStyle(); m_DefaultTheme.mWindowPadding = OldStyle.WindowPadding; @@ -356,7 +368,7 @@ void NotificationManager::PopNotification::bbl_render_block_notification(GLCanva std::string name = "!!Ntfctn" + std::to_string(m_id); use_bbl_theme(); - if (m_data.level == NotificationLevel::SeriousWarningNotificationLevel) + if (m_data.level == NotificationLevel::SeriousWarningNotificationLevel) { push_style_color(ImGuiCol_Border, {245.f / 255.f, 155 / 255.f, 22 / 255.f, 1}, true, m_current_fade_opacity); push_style_color(ImGuiCol_WindowBg, {245.f / 255.f, 155 / 255.f, 22 / 255.f, 1}, true, m_current_fade_opacity); @@ -704,17 +716,17 @@ void NotificationManager::PopNotification::render_hypertext(ImGuiWrapper& imgui, //hover color ImVec4 HyperColor = m_HyperTextColor;//ImVec4(150.f / 255.f, 100.f / 255.f, 0.f / 255.f, 1) - if (m_data.level == NotificationLevel::SeriousWarningNotificationLevel) - HyperColor = ImVec4(0.f, 0.f, 0.f, 0.4f); - if (m_data.level == NotificationLevel::ErrorNotificationLevel) - HyperColor = ImVec4(135.f / 255.f, 43 / 255.f, 43 / 255.f, 1); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly)) - { - HyperColor.y += 0.1f; - if (m_data.level == NotificationLevel::SeriousWarningNotificationLevel || m_data.level == NotificationLevel::SeriousWarningNotificationLevel) - HyperColor.x += 0.2f; + if (m_data.level == NotificationLevel::SeriousWarningNotificationLevel) + HyperColor = ImVec4(0.f, 0.f, 0.f, 0.4f); + if (m_data.level == NotificationLevel::ErrorNotificationLevel) + HyperColor = ImVec4(135.f / 255.f, 43 / 255.f, 43 / 255.f, 1); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly)) + { + HyperColor.y += 0.1f; + if (m_data.level == NotificationLevel::SeriousWarningNotificationLevel || m_data.level == NotificationLevel::SeriousWarningNotificationLevel) + HyperColor.x += 0.2f; } - + //text push_style_color(ImGuiCol_Text, HyperColor, m_state == EState::FadingOut, m_current_fade_opacity); @@ -856,11 +868,12 @@ void NotificationManager::PopNotification::bbl_render_block_notif_left_sign(ImGu ImGui::SetCursorPosX(m_line_height / 3); ImGui::SetCursorPosY(m_window_height / 2 - m_line_height); imgui.text(text.c_str()); - + } void NotificationManager::PopNotification::bbl_render_left_sign(ImGuiWrapper &imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) { + ensure_ui_inited(); ImDrawList *draw_list = ImGui::GetWindowDrawList(); ImVec2 round_rect_pos = ImVec2(win_pos_x - win_size_x + ImGui::GetStyle().WindowBorderSize, win_pos_y + ImGui::GetStyle().WindowBorderSize); ImVec2 round_rect_size = ImVec2(m_WindowRadius * 2, win_size_y - 2 * ImGui::GetStyle().WindowBorderSize); @@ -892,6 +905,7 @@ void NotificationManager::PopNotification::render_left_sign(ImGuiWrapper& imgui) } void NotificationManager::PopNotification::render_minimize_button(ImGuiWrapper& imgui, const float win_pos_x, const float win_pos_y) { + ensure_ui_inited(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); push_style_color(ImGuiCol_ButtonActive, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_state == EState::FadingOut, m_current_fade_opacity); @@ -1075,6 +1089,7 @@ void NotificationManager::ExportFinishedNotification::render_text(ImGuiWrapper& void NotificationManager::ExportFinishedNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) { + ensure_ui_inited(); PopNotification::render_close_button(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); if (m_to_removable && !m_eject_pending) render_eject_button(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); @@ -1883,7 +1898,7 @@ void NotificationManager::upload_job_notification_show_error(int id, const std:: } } -void NotificationManager::push_slicing_serious_warning_notification(const std::string &text, std::vector objs) +void NotificationManager::push_slicing_serious_warning_notification(const std::string &text, std::vector objs) { std::vector ids; for (auto optr : objs) { @@ -1924,7 +1939,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s set_slicing_progress_hidden(); } -void NotificationManager::close_slicing_serious_warning_notification(const std::string &text) +void NotificationManager::close_slicing_serious_warning_notification(const std::string &text) { for (std::unique_ptr ¬ification : m_pop_notifications) { if (notification->get_type() == NotificationType::SlicingSeriousWarning && notification->compare_text(_u8L("Serious warning:") + "\n" + text)) { notification->close(); } @@ -2199,7 +2214,7 @@ bool NotificationManager::push_notification_data(std::unique_ptrget_data().level == NotificationLevel::ErrorNotificationLevel || notification->get_data().level == NotificationLevel::SeriousWarningNotificationLevel) { notification->bbl_render_block_notification(canvas, bottom_up_last_y, m_move_from_overlay && !m_in_preview, overlay_width * m_scale, right_margin * m_scale); - if (notification->get_state() != PopNotification::EState::Finished) + if (notification->get_state() != PopNotification::EState::Finished) bottom_up_last_y = notification->get_top() + GAP_WIDTH; } else { @@ -2392,7 +2407,7 @@ void NotificationManager::set_in_preview(bool preview) notification->hide(preview); if (m_in_preview && notification->get_type() == NotificationType::DidYouKnowHint) notification->close(); - if (notification->get_type() == NotificationType::ValidateWarning) + if (notification->get_type() == NotificationType::ValidateWarning) notification->hide(preview); } } diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 3bab8e741..73b835453 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -462,9 +462,9 @@ private: virtual bool push_background_color(); // used this function instead of reading directly m_data.duration. Some notifications might need to return changing value. virtual int get_duration() { return m_data.duration; } - + void ensure_ui_inited(); bool m_is_dark = false; - + bool m_is_dark_inited = false; const NotificationData m_data; // For reusing ImGUI windows. NotificationIDProvider &m_id_provider; @@ -493,7 +493,7 @@ private: ImVec4 m_CurrentColor; float m_WindowRadius; - + bool m_WindowRadius_inited = false; void use_bbl_theme(); void restore_default_theme(); @@ -867,7 +867,7 @@ private: }}, NotificationData{NotificationType::BBLUserPresetExceedLimit, NotificationLevel::WarningNotificationLevel, BBL_NOTICE_MAX_INTERVAL, - _u8L("The number of user presets cached in the cloud has exceeded the upper limit, newly created user presets can only be used locally."), + _u8L("The number of user presets cached in the cloud has exceeded the upper limit, newly created user presets can only be used locally."), _u8L("Wiki"), [](wxEvtHandler* evnthndlr) { wxLaunchDefaultBrowser("https://wiki.bambulab.com/en/software/bambu-studio/3rd-party-printer-profile#cloud-user-presets-limit"); diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index b4a9e2eb9..1d853c438 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -72,6 +72,7 @@ const std::map INFO_ITEMS{ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, + const bool is_svg_volume, const wxBitmap& bmp, const wxString& extruder, const int idx/* = -1*/, @@ -80,6 +81,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* pare m_name(sub_obj_name), m_type(itVolume), m_volume_type(type), + m_is_svg_volume(is_svg_volume), m_idx(idx), m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : ""), m_warning_icon_name(warning_icon_name) @@ -356,10 +358,10 @@ bool ObjectDataViewModelNode::SetValue(const wxVariant& variant, unsigned col) return false; } -void ObjectDataViewModelNode::SetName(const wxString &tempName) -{ +void ObjectDataViewModelNode::SetName(const wxString &tempName) +{ if (m_name != tempName) { - m_name = tempName; + m_name = tempName; } } @@ -450,6 +452,8 @@ ObjectDataViewModel::ObjectDataViewModel() m_bitmap_cache = new Slic3r::GUI::BitmapCache; m_volume_bmps = MenuFactory::get_volume_bitmaps(); + m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); + m_svg_volume_bmps = MenuFactory::get_svg_volume_bitmaps(); m_warning_bmp = create_scaled_bitmap(WarningIcon); m_warning_manifold_bmp = create_scaled_bitmap(WarningManifoldIcon); m_lock_bmp = create_scaled_bitmap(LockIcon); @@ -511,7 +515,7 @@ wxDataViewItem ObjectDataViewModel::AddPlate(PartPlate* part_plate, wxString nam if (!is_added) { m_plates.push_back(plate_node); } - + wxDataViewItem plate_item(plate_node); if (refresh) { ItemAdded(wxDataViewItem(nullptr), plate_item); @@ -542,7 +546,10 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode *node) is_volume_node &= (vol_type >= int(ModelVolumeType::MODEL_PART) && vol_type <= int(ModelVolumeType::SUPPORT_ENFORCER)); if (!node->has_warning_icon() && !node->has_lock()) { - node->SetBitmap(is_volume_node ? m_volume_bmps.at(vol_type) : m_empty_bmp); + node->SetBitmap(is_volume_node ? (node->is_text_volume() ? m_text_volume_bmps.at(vol_type) : + node->is_svg_volume() ? m_svg_volume_bmps.at(vol_type) : + m_volume_bmps.at(vol_type)) : + m_empty_bmp); return; } @@ -561,16 +568,22 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode *node) bmps.emplace_back(node->warning_icon_name() == WarningIcon ? m_warning_bmp : m_warning_manifold_bmp); if (node->has_lock()) bmps.emplace_back(m_lock_bmp); - if (is_volume_node) - bmps.emplace_back(m_volume_bmps[vol_type]); + if (is_volume_node) { + if (!bmps.empty()) // ORCA: Add spacing between icons if there are multiple + bmps.emplace_back(create_scaled_bitmap("dot", nullptr, int(wxGetApp().em_unit() / 10) * 4)); + bmps.emplace_back(node->is_text_volume() ? m_text_volume_bmps[vol_type] : + node->is_svg_volume() ? m_svg_volume_bmps[vol_type] : + m_volume_bmps[vol_type]); + } bmp = m_bitmap_cache->insert(scaled_bitmap_name, bmps); } node->SetBitmap(*bmp); } -void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode *node, bool has_lock) +void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode *node, const std::string &warning_icon_name, bool has_lock) { + node->SetWarningIconName(warning_icon_name); node->SetLock(has_lock); UpdateBitmapForNode(node); } @@ -597,7 +610,7 @@ wxDataViewItem ObjectDataViewModel::AddObject(ModelObject *model_object, std::st const wxString extruder_str = wxString::Format("%d", extruder); auto obj_node = new ObjectDataViewModelNode(name, extruder_str, plate_idx, model_object); obj_node->SetWarningBitmap(GetWarningBitmap(warning_bitmap), warning_bitmap); - UpdateBitmapForNode(obj_node, has_lock); + UpdateBitmapForNode(obj_node, warning_bitmap, has_lock); if (plate_node != nullptr) { obj_node->m_parent = plate_node; @@ -623,6 +636,7 @@ wxDataViewItem ObjectDataViewModel::AddObject(ModelObject *model_object, std::st wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent_item, const wxString &name, const Slic3r::ModelVolumeType volume_type, + const bool is_svg_volume, const std::string& warning_icon_name/* = std::string()*/, const int extruder/* = 0*/, const bool create_frst_child/* = true*/) @@ -638,8 +652,9 @@ wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent if (create_frst_child && root->m_volumes_cnt == 0) { const Slic3r::ModelVolumeType type = Slic3r::ModelVolumeType::MODEL_PART; - const auto node = new ObjectDataViewModelNode(root, root->m_name, type, GetVolumeIcon(type, root->m_warning_icon_name), root->m_extruder, 0, root->m_warning_icon_name); - + const auto node = new ObjectDataViewModelNode(root, root->m_name, type, is_svg_volume, GetVolumeIcon(type, root->m_warning_icon_name), root->m_extruder, + 0, root->m_warning_icon_name); + UpdateBitmapForNode(node, root->m_warning_icon_name, root->has_lock()); insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); // notify control const wxDataViewItem child((void*)node); @@ -661,15 +676,16 @@ wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent extruder_str = wxString::Format("%d", extruder); } - const auto node = new ObjectDataViewModelNode(root, name, volume_type, GetVolumeIcon(volume_type, warning_icon_name), + const auto node = new ObjectDataViewModelNode(root, name, volume_type, is_svg_volume, GetVolumeIcon(volume_type, warning_icon_name), extruder_str, root->m_volumes_cnt, warning_icon_name); + UpdateBitmapForNode(node, warning_icon_name, root->has_lock() && volume_type < ModelVolumeType::PARAMETER_MODIFIER); insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); // if part with errors is added, but object wasn't marked, then mark it - if (!warning_icon_name.empty() && warning_icon_name != root->m_warning_icon_name && - (root->m_warning_icon_name.empty() || root->m_warning_icon_name == WarningManifoldIcon) ) + if (!warning_icon_name.empty() && warning_icon_name != root->m_warning_icon_name && (root->m_warning_icon_name.empty() || root->m_warning_icon_name == WarningManifoldIcon)) { root->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); - + UpdateBitmapForNode(root); + } // notify control const wxDataViewItem child((void*)node); ItemAdded(parent_item, child); @@ -1291,9 +1307,9 @@ wxDataViewItem ObjectDataViewModel::GetItemByPlateId(int plate_idx) return wxDataViewItem(nullptr); } -void ObjectDataViewModel::SetCurSelectedPlateFullName(int plate_idx, const std::string & custom_name) { +void ObjectDataViewModel::SetCurSelectedPlateFullName(int plate_idx, const std::string & custom_name) { for (auto plate : m_plates) { - if (plate->m_plate_idx == plate_idx) { + if (plate->m_plate_idx == plate_idx) { wxString plate_full_name =_L("Plate"); plate_full_name += wxString::Format(" %d", plate_idx + 1); if (!custom_name.empty()) { @@ -2100,7 +2116,7 @@ bool ObjectDataViewModel::HasInfoItem(InfoItemType type) const ItemType ObjectDataViewModel::GetItemType(const wxDataViewItem &item) const { - if (!item.IsOk()) + if (!item.IsOk()) return itUndef; ObjectDataViewModelNode *node = static_cast(item.GetID()); return node->m_type < 0 ? itUndef : node->m_type; @@ -2350,6 +2366,8 @@ void ObjectDataViewModel::SetSinkState(const bool painted, wxDataViewItem obj_it void ObjectDataViewModel::Rescale() { m_volume_bmps = MenuFactory::get_volume_bitmaps(); + m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); + m_svg_volume_bmps = MenuFactory::get_svg_volume_bitmaps(); m_warning_bmp = create_scaled_bitmap(WarningIcon); m_warning_manifold_bmp = create_scaled_bitmap(WarningManifoldIcon); m_lock_bmp = create_scaled_bitmap(LockIcon); @@ -2371,10 +2389,8 @@ void ObjectDataViewModel::Rescale() switch (node->m_type) { case itObject: - if (node->m_bmp.IsOk()) node->m_bmp = GetWarningBitmap(node->m_warning_icon_name); - break; case itVolume: - node->m_bmp = GetVolumeIcon(node->m_volume_type, node->m_warning_icon_name); + UpdateBitmapForNode(node); break; case itLayerRoot: node->m_bmp = create_scaled_bitmap(LayerRootIcon); @@ -2420,13 +2436,14 @@ void ObjectDataViewModel::AddWarningIcon(const wxDataViewItem& item, const std:: ObjectDataViewModelNode *node = static_cast(item.GetID()); if (node->GetType() & itObject) { - node->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + UpdateBitmapForNode(node, warning_icon_name, node->has_lock()); return; } if (node->GetType() & itVolume) { - node->SetWarningBitmap(GetVolumeIcon(node->GetVolumeType(), warning_icon_name), warning_icon_name); - node->GetParent()->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + UpdateBitmapForNode(node, warning_icon_name, node->has_lock()); + if (ObjectDataViewModelNode *parent = node->GetParent()) + UpdateBitmapForNode(parent, warning_icon_name, parent->has_lock()); return; } } @@ -2447,6 +2464,7 @@ void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bo } node->SetWarningBitmap(wxNullBitmap, ""); + UpdateBitmapForNode(node); if (unmark_object) { wxDataViewItemArray children; diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 108157989..84cbfcd21 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -109,6 +109,8 @@ class ObjectDataViewModelNode std::string m_action_icon_name = ""; ModelVolumeType m_volume_type = ModelVolumeType(-1); + bool m_is_text_volume{false}; + bool m_is_svg_volume{false}; InfoItemType m_info_item_type {InfoItemType::Undef}; bool m_action_enable = false; // can undo all settings // BBS @@ -139,6 +141,7 @@ public: ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, + const bool is_svg_volume, const wxBitmap& bmp, const wxString& extruder, const int idx = -1, @@ -237,6 +240,7 @@ public: void SetVolumeType(ModelVolumeType type) { m_volume_type = type; } void SetBitmap(const wxBitmap &icon) { m_bmp = icon; } void SetExtruder(const wxString &extruder) { m_extruder = extruder; } + void SetWarningIconName(const std::string &warning_icon_name) { m_warning_icon_name = warning_icon_name; } void SetWarningBitmap(const wxBitmap& icon, const std::string& warning_icon_name) { m_bmp = icon; m_warning_icon_name = warning_icon_name; } void SetLock(bool has_lock) { m_has_lock = has_lock; } const wxBitmap& GetBitmap() const { return m_bmp; } @@ -307,6 +311,8 @@ public: void update_settings_digest_bitmaps(); bool update_settings_digest(const std::vector& categories); int volume_type() const { return int(m_volume_type); } + bool is_text_volume() const { return m_is_text_volume; } + bool is_svg_volume() const { return m_is_svg_volume; } void msw_rescale(); #ifndef NDEBUG @@ -335,6 +341,8 @@ class ObjectDataViewModel :public wxDataViewModel std::vector m_plates; std::vector m_objects; std::vector m_volume_bmps; + std::vector m_text_volume_bmps; + std::vector m_svg_volume_bmps; std::map m_info_bmps; wxBitmap m_empty_bmp; wxBitmap m_warning_bmp; @@ -356,7 +364,7 @@ public: std::map &get_ui_and_3d_volume_map() { return m_ui_and_3d_volume_map; } int get_real_volume_index_in_3d(int ui_value) { - if (m_ui_and_3d_volume_map.find(ui_value) != m_ui_and_3d_volume_map.end()) { + if (m_ui_and_3d_volume_map.find(ui_value) != m_ui_and_3d_volume_map.end()) { return m_ui_and_3d_volume_map[ui_value]; } return ui_value; @@ -375,6 +383,7 @@ public: wxDataViewItem AddVolumeChild( const wxDataViewItem &parent_item, const wxString &name, const Slic3r::ModelVolumeType volume_type, + const bool is_svg_volume, const std::string& warning_icon_name = std::string(), const int extruder = 0, const bool create_frst_child = true); @@ -540,7 +549,7 @@ private: wxDataViewItem AddOutsidePlate(bool refresh = true); void UpdateBitmapForNode(ObjectDataViewModelNode *node); - void UpdateBitmapForNode(ObjectDataViewModelNode *node, bool has_lock); + void UpdateBitmapForNode(ObjectDataViewModelNode *node, const std::string &warning_icon_name, bool has_lock); }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index bdb920714..758f0f188 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -88,6 +88,8 @@ #include "Jobs/SLAImportJob.hpp" #include "Jobs/PrintJob.hpp" #include "Jobs/NotificationProgressIndicator.hpp" +#include "Jobs/PlaterWorker.hpp" +#include "Jobs/BoostThreadWorker.hpp" #include "BackgroundSlicingProcess.hpp" #include "SelectMachine.hpp" #include "SendMultiMachinePage.hpp" @@ -107,7 +109,7 @@ #include "MsgDialog.hpp" #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification - +#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file // BBS #include "Widgets/ProgressDialog.hpp" #include "BBLStatusBar.hpp" @@ -2218,7 +2220,9 @@ struct Plater::priv BackgroundSlicingProcess background_process; bool suppressed_backround_processing_update { false }; - + // UIThreadWorker can be used as a replacement for BoostThreadWorker if + // no additional worker threads are desired (useful for debugging or profiling) + PlaterWorker m_worker; // Jobs defined inside the group class will be managed so that only one can // run at a time. Also, the background process will be stopped if a job is // started. It is up the the plater to ensure that the background slicing @@ -2747,6 +2751,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) })) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) + , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} , m_ui_jobs(this) , m_job_prepare_state(Job::JobPrepareState::PREPARE_STATE_DEFAULT) , delayed_scene_refresh(false) @@ -3364,6 +3369,10 @@ void Plater::priv::reset_all_gizmos() view3D->get_canvas3d()->reset_all_gizmos(); } +Worker &Plater::get_ui_job_worker() { return p->m_worker; } + +const Worker &Plater::get_ui_job_worker() const { return p->m_worker; } + // Called after the Preferences dialog is closed and the program settings are saved. // Update the UI based on the current preferences. void Plater::priv::update_ui_from_settings() @@ -7357,8 +7366,15 @@ void Plater::priv::on_right_click(RBtnEvent& evt) menu = is_some_full_instances ? menus.assemble_object_menu() : is_part ? menus.assemble_part_menu() : menus.assemble_multi_selection_menu(); } else { - menu = is_some_full_instances ? menus.object_menu() : - is_part ? menus.part_menu() : menus.multi_selection_menu(); + if (is_some_full_instances) + menu = printer_technology == ptSLA ? menus.sla_object_menu() : menus.object_menu(); + else if (is_part) { + const GLVolume * gl_volume = selection.get_first_volume(); + const ModelVolume *model_volume = get_model_volume(*gl_volume, selection.get_model()->objects); + menu = (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : + menus.part_menu(); + } else + menu = menus.multi_selection_menu(); } } } @@ -10338,6 +10354,26 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) Refresh(); } +bool Plater::emboss_svg(const wxString &svg_file) +{ + std::string svg_file_str = into_u8(svg_file); + GLCanvas3D *canvas = canvas3D(); + if (canvas == nullptr) + return false; + auto base_svg = canvas->get_gizmos_manager().get_gizmo(GLGizmosManager::Svg); + if (base_svg == nullptr) + return false; + GLGizmoSVG *svg = dynamic_cast(base_svg); + if (svg == nullptr) + return false; + // Refresh hover state to find surface point under mouse + wxMouseEvent evt(wxEVT_MOTION); + auto mouse_drop_position =canvas->get_local_mouse_position(); + evt.SetPosition(wxPoint(mouse_drop_position.x(), mouse_drop_position.y())); + canvas->on_mouse(evt); // call render where is call GLCanvas3D::_picking_pass() + return svg->create_volume(svg_file_str, mouse_drop_position, ModelVolumeType::MODEL_PART); +} + //BBS: remove GCodeViewer as seperate APP logic bool Plater::load_files(const wxArrayString& filenames) { @@ -10347,6 +10383,20 @@ bool Plater::load_files(const wxArrayString& filenames) std::vector normal_paths; std::vector gcode_paths; + // When only one .svg file is dropped on scene + if (filenames.size() == 1) { + const wxString &filename = filenames.Last(); + const wxString file_extension = filename.substr(filename.length() - 4); + if (file_extension.CmpNoCase(".svg") == 0) { + // BBS: GUI refactor: move sidebar to the left + /* const wxPoint offset = GetPosition() + p->current_panel->GetPosition(); + Vec2d mouse_position(x - offset.x, y - offset.y);*/ + // Scale for retina displays + //canvas->apply_retina_scale(mouse_position); + return emboss_svg(filename); + } + } + for (const auto& filename : filenames) { fs::path path(into_path(filename)); if (std::regex_match(path.string(), pattern_drop)) @@ -10765,6 +10815,7 @@ void Plater::set_selected_visible(bool visible) return; Plater::TakeSnapshot snapshot(this, "Set Selected Objects Visible in AssembleView"); + get_ui_job_worker().cancel_all(); p->m_ui_jobs.cancel_all(); p->get_current_canvas3D()->set_selected_visible(visible); @@ -10783,6 +10834,7 @@ void Plater::remove_selected() return; Plater::TakeSnapshot snapshot(this, "Delete Selected Objects"); + get_ui_job_worker().cancel_all(); p->m_ui_jobs.cancel_all(); //BBS delete current selected @@ -11560,6 +11612,101 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls) ; // store failed } }*/ +namespace { +std::string get_file_name(const std::string &file_path) +{ + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + size_t pos_point = file_path.find_last_of('.'); + size_t offset = pos_last_delimiter + 1; + size_t count = pos_point - pos_last_delimiter - 1; + return file_path.substr(offset, count); +} +using SvgFile = EmbossShape::SvgFile; +using SvgFiles = std::vector; +std::string create_unique_3mf_filepath(const std::string &file, const SvgFiles svgs) +{ + // const std::string MODEL_FOLDER = "3D/"; // copy from file 3mf.cpp + std::string path_in_3mf = "3D/" + file + ".svg"; + size_t suffix_number = 0; + bool is_unique = false; + do { + is_unique = true; + path_in_3mf = "3D/" + file + ((suffix_number++) ? ("_" + std::to_string(suffix_number)) : "") + ".svg"; + for (SvgFile *svgfile : svgs) { + if (svgfile->path_in_3mf.empty()) continue; + if (svgfile->path_in_3mf.compare(path_in_3mf) == 0) { + is_unique = false; + break; + } + } + } while (!is_unique); + return path_in_3mf; +} + +bool set_by_local_path(SvgFile &svg, const SvgFiles &svgs) +{ + // Try to find already used svg file + for (SvgFile *svg_ : svgs) { + if (svg_->path_in_3mf.empty()) continue; + if (svg.path.compare(svg_->path) == 0) { + svg.path_in_3mf = svg_->path_in_3mf; + return true; + } + } + return false; +} + +/// +/// Function to secure private data before store to 3mf +/// +/// Data(also private) to clean before publishing +void publish(Model &model, SaveStrategy strategy) +{ + // SVG file publishing + bool exist_new = false; + SvgFiles svgfiles; + for (ModelObject *object : model.objects) { + for (ModelVolume *volume : object->volumes) { + if (!volume->emboss_shape.has_value()) continue; + + assert(volume->emboss_shape->svg_file.has_value()); + if (!volume->emboss_shape->svg_file.has_value()) continue; + + SvgFile *svg = &(*volume->emboss_shape->svg_file); + if (svg->path_in_3mf.empty()) exist_new = true; + svgfiles.push_back(svg); + } + } + + // Orca: don't show this in silence mode + if (exist_new && !(strategy & SaveStrategy::Silence)) { + MessageDialog dialog(nullptr, + _L("Are you sure you want to store original SVGs with their local paths into the 3MF file?\n" + "If you hit 'NO', all SVGs in the project will not be editable any more."), + _L("Private protection"), wxYES_NO | wxICON_QUESTION); + if (dialog.ShowModal() == wxID_NO) { + for (ModelObject *object : model.objects) + for (ModelVolume *volume : object->volumes) + if (volume->emboss_shape.has_value()) volume->emboss_shape.reset(); + } + } + + for (SvgFile *svgfile : svgfiles) { + if (!svgfile->path_in_3mf.empty()) + continue; // already suggested path (previous save) + + // create unique name for svgs, when local path differ + std::string filename = "unknown"; + if (!svgfile->path.empty()) { + if (set_by_local_path(*svgfile, svgfiles)) + continue; + // check whether original filename is already in: + filename = get_file_name(svgfile->path); + } + svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles); + } +} +} // namespace // BBS: backup int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn) @@ -11579,6 +11726,9 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy if (!path.Lower().EndsWith(".3mf")) return -1; + // take care about private data stored into .3mf + // modify model + publish(p->model, strategy); DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); @@ -12832,6 +12982,28 @@ void Plater::changed_mesh(int obj_idx) p->schedule_background_process(); } +void Plater::changed_object(ModelObject &object) +{ + assert(object.get_model() == &p->model); // is object from same model? + object.invalidate_bounding_box(); + + // recenter and re - align to Z = 0 + object.ensure_on_bed(p->printer_technology != ptSLA); + + if (p->printer_technology == ptSLA) { + // Update the SLAPrint from the current Model, so that the reload_scene() + // pulls the correct data, update the 3D scene. + p->update_restart_background_process(true, false); + } else + p->view3D->reload_scene(false); + + // update print + p->schedule_background_process(); + + // Check outside bed + get_current_canvas3D()->requires_check_outside_state(); +} + void Plater::changed_object(int obj_idx) { if (obj_idx < 0) @@ -14073,7 +14245,8 @@ bool Plater::PopupObjectTableBySelection() wxMenu* Plater::plate_menu() { return p->menus.plate_menu(); } wxMenu* Plater::object_menu() { return p->menus.object_menu(); } -wxMenu* Plater::part_menu() { return p->menus.part_menu(); } +wxMenu *Plater::part_menu() { return p->menus.part_menu(); } +wxMenu *Plater::svg_part_menu() { return p->menus.svg_part_menu(); } wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } wxMenu* Plater::default_menu() { return p->menus.default_menu(); } wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 0874fa003..6dbfb44f6 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -16,6 +16,7 @@ #include "libslic3r/BoundingBox.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "Jobs/Job.hpp" +#include "Jobs/Worker.hpp" #include "Search.hpp" #include "PartPlate.hpp" #include "GUI_App.hpp" @@ -287,12 +288,15 @@ public: // To be called when providing a list of files to the GUI slic3r on command line. std::vector load_files(const std::vector& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false); // to be called on drag and drop + bool emboss_svg(const wxString &svg_file); bool load_files(const wxArrayString& filenames); const wxString& get_last_loaded_gcode() const { return m_last_loaded_gcode; } void update(bool conside_update_flag = false, bool force_background_processing_update = false); //BBS + Worker & get_ui_job_worker(); + const Worker &get_ui_job_worker() const; void object_list_changed(); void stop_jobs(); bool is_any_job_running() const; @@ -382,6 +386,7 @@ public: void clear_before_change_mesh(int obj_idx); void changed_mesh(int obj_idx); + void changed_object(ModelObject &object); void changed_object(int obj_idx); void changed_objects(const std::vector& object_idxs); void schedule_background_process(bool schedule = true); @@ -719,6 +724,7 @@ public: wxMenu* plate_menu(); wxMenu* object_menu(); wxMenu* part_menu(); + wxMenu* svg_part_menu(); wxMenu* sla_object_menu(); wxMenu* default_menu(); wxMenu* instance_menu(); diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 72fbe83f3..44ad0d6d5 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -149,6 +149,10 @@ void Selection::set_model(Model* model) update_valid(); } +void Selection::set_mode(EMode mode) { + m_mode = mode; +} + int Selection::query_real_volume_idx_from_other_view(unsigned int object_idx, unsigned int instance_idx, unsigned int model_volume_idx) { for (int i = 0; i < m_volumes->size(); i++) { @@ -876,6 +880,10 @@ const GLVolume* Selection::get_volume(unsigned int volume_idx) const return (m_valid && (volume_idx < (unsigned int)m_volumes->size())) ? (*m_volumes)[volume_idx] : nullptr; } +GLVolume *Selection::get_volume(unsigned int volume_idx) { + return (m_valid && (volume_idx < (unsigned int) m_volumes->size())) ? (*m_volumes)[volume_idx] : nullptr; +} + const BoundingBoxf3& Selection::get_bounding_box() const { if (!m_bounding_box.has_value()) { @@ -3244,5 +3252,52 @@ void Selection::transform_volume_relative( assert(false); } +ModelVolume *get_selected_volume(const Selection &selection) +{ + const GLVolume *gl_volume = get_selected_gl_volume(selection); + if (gl_volume == nullptr) return nullptr; + const ModelObjectPtrs &objects = selection.get_model()->objects; + return get_model_volume(*gl_volume, objects); +} + +const GLVolume *get_selected_gl_volume(const Selection &selection) +{ + int object_idx = selection.get_object_idx(); + // is more object selected? + if (object_idx == -1) return nullptr; + + const auto &list = selection.get_volume_idxs(); + // is more volumes selected? + if (list.size() != 1) return nullptr; + + unsigned int volume_idx = *list.begin(); + return selection.get_volume(volume_idx); +} + +ModelVolume *get_selected_volume(const ObjectID &volume_id, const Selection &selection) +{ + const Selection::IndicesList &volume_ids = selection.get_volume_idxs(); + const ModelObjectPtrs & model_objects = selection.get_model()->objects; + for (auto id : volume_ids) { + const GLVolume * selected_volume = selection.get_volume(id); + const GLVolume::CompositeID &cid = selected_volume->composite_id; + ModelObject * obj = model_objects[cid.object_id]; + ModelVolume * volume = obj->volumes[cid.volume_id]; + if (volume_id == volume->id()) return volume; + } + return nullptr; +} + +ModelVolume *get_volume(const ObjectID &volume_id, const Selection &selection) +{ + const ModelObjectPtrs &objects = selection.get_model()->objects; + for (const ModelObject *object : objects) { + for (ModelVolume *volume : object->volumes) { + if (volume->id() == volume_id) return volume; + } + } + return nullptr; +} + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index d21bf0469..94c5d863e 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -14,6 +14,7 @@ class Model; class ModelObject; class ModelVolume; class ModelInstance; +class ObjectID; class GLVolume; class GLArrow; class GLCurvedArrow; @@ -291,7 +292,7 @@ public: void set_model(Model* model); EMode get_mode() const { return m_mode; } - void set_mode(EMode mode) { m_mode = mode; } + void set_mode(EMode mode); int query_real_volume_idx_from_other_view(unsigned int object_idx, unsigned int instance_idx, unsigned int model_volume_idx); void add(unsigned int volume_idx, bool as_single_selection = true, bool check_for_already_contained = false); @@ -376,6 +377,7 @@ public: const IndicesList& get_volume_idxs() const { return m_list; } const GLVolume* get_volume(unsigned int volume_idx) const; + GLVolume* get_volume(unsigned int volume_idx); const GLVolume* get_first_volume() const { return get_volume(*m_list.begin()); } const ObjectIdxsToInstanceIdxsMap& get_content() const { return m_cache.content; } @@ -526,7 +528,11 @@ private: void transform_volume_relative( GLVolume &volume, const VolumeCache &volume_data, TransformationType transformation_type, const Transform3d &transform, const Vec3d &world_pivot); }; +ModelVolume * get_selected_volume(const Selection &selection); +const GLVolume *get_selected_gl_volume(const Selection &selection); +ModelVolume *get_selected_volume(const ObjectID &volume_id, const Selection &selection); +ModelVolume *get_volume(const ObjectID &volume_id, const Selection &selection); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/SurfaceDrag.cpp b/src/slic3r/GUI/SurfaceDrag.cpp new file mode 100644 index 000000000..03006b46e --- /dev/null +++ b/src/slic3r/GUI/SurfaceDrag.cpp @@ -0,0 +1,718 @@ +#include "SurfaceDrag.hpp" + +#include // ModelVolume +#include + +#include "slic3r/Utils/RaycastManager.hpp" + +#include "GLCanvas3D.hpp" +#include "Camera.hpp" +#include "CameraUtils.hpp" +#include "I18N.hpp" +#include "GUI_App.hpp" +#include "Gizmos/GizmoObjectManipulation.hpp" + + +using namespace Slic3r; +using namespace Slic3r::GUI; + +namespace{ +// Distance of embossed volume from surface to be represented as distance surface +// Maximal distance is also enlarge by size of emboss depth +constexpr Slic3r::MinMax surface_distance_sq{1e-4, 10.}; // [in mm] + +/// +/// Extract position of mouse from mouse event +/// +/// Event +/// Position +Vec2d mouse_position(const wxMouseEvent &mouse_event); + +bool start_dragging(const Vec2d &mouse_pos, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + RaycastManager &raycast_manager, + const std::optional &up_limit); + +bool dragging(const Vec2d &mouse_pos, + const Camera &camera, + SurfaceDrag &surface_drag, // need to write whether exist hit + GLCanvas3D &canvas, + const RaycastManager &raycast_manager, + const std::optional &up_limit); + +Transform3d get_volume_transformation( + Transform3d world, // from volume + const Vec3d& world_dir, // wanted new direction + const Vec3d& world_position, // wanted new position + const std::optional& fix, // [optional] fix matrix + // Invers transformation of text volume instance + // Help convert world transformation to instance space + const Transform3d& instance_inv, + // initial rotation in Z axis + std::optional current_angle = {}, + const std::optional &up_limit = {}); + +// distinguish between transformation of volume inside object +// and object(single full instance with one volume) +bool is_embossed_object(const Selection &selection); + +/// +/// Get fix transformation for selected volume +/// Fix after store to 3mf +/// +/// Select only wanted volume +/// Pointer on fix transformation from ModelVolume when exists otherwise nullptr +const Transform3d *get_fix_transformation(const Selection &selection); +} + +namespace Slic3r::GUI { + // Calculate scale in world for check in debug +[[maybe_unused]] static std::optional calc_scale(const Matrix3d &from, const Matrix3d &to, const Vec3d &dir) +{ + Vec3d from_dir = from * dir; + Vec3d to_dir = to * dir; + double from_scale_sq = from_dir.squaredNorm(); + double to_scale_sq = to_dir.squaredNorm(); + if (is_approx(from_scale_sq, to_scale_sq, 1e-3)) + return {}; // no scale + return sqrt(from_scale_sq / to_scale_sq); +} + +bool on_mouse_surface_drag(const wxMouseEvent &mouse_event, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + RaycastManager &raycast_manager, + const std::optional&up_limit) +{ + // Fix when leave window during dragging + // Fix when click right button + if (surface_drag.has_value() && !mouse_event.Dragging()) { + // write transformation from UI into model + canvas.do_move(L("Move over surface")); + wxGetApp().obj_manipul()->set_dirty(); + + // allow moving with object again + //canvas.enable_moving(true); + //canvas.enable_picking(true); + surface_drag.reset(); + + // only left up is correct + // otherwise it is fix state and return false + return mouse_event.LeftUp(); + } + + if (mouse_event.Moving()) + return false; + + if (mouse_event.LeftDown()) + return start_dragging(mouse_position(mouse_event), camera, surface_drag, canvas, raycast_manager, up_limit); + + // Dragging starts out of window + if (!surface_drag.has_value()) + return false; + + if (mouse_event.Dragging()) + return dragging(mouse_position(mouse_event), camera, *surface_drag, canvas, raycast_manager, up_limit); + + return false; +} + +std::optional calc_surface_offset(const Selection &selection, RaycastManager &raycast_manager) { + const GLVolume *gl_volume_ptr = get_selected_gl_volume(selection); + if (gl_volume_ptr == nullptr) + return {}; + const GLVolume& gl_volume = *gl_volume_ptr; + + const ModelObjectPtrs &objects = selection.get_model()->objects; + const ModelVolume* volume = get_model_volume(gl_volume, objects); + if (volume == nullptr) + return {}; + + const ModelInstance* instance = get_model_instance(gl_volume, objects); + if (instance == nullptr) + return {}; + + // Move object on surface + auto cond = RaycastManager::SkipVolume(volume->id().id); + raycast_manager.actualize(*instance, &cond); + + Transform3d to_world = world_matrix_fixed(gl_volume, objects); + Vec3d point = to_world.translation(); + Vec3d dir = -get_z_base(to_world); + // ray in direction of text projection(from volume zero to z-dir) + std::optional hit_opt = raycast_manager.closest_hit(point, dir, &cond); + + // Try to find closest point when no hit object in emboss direction + if (!hit_opt.has_value()) { + std::optional close_point_opt = raycast_manager.closest(point); + + // It should NOT appear. Closest point always exists. + assert(close_point_opt.has_value()); + if (!close_point_opt.has_value()) + return {}; + + // It is no neccesary to move with origin by very small value + if (close_point_opt->squared_distance < EPSILON) + return {}; + + const RaycastManager::ClosePoint &close_point = *close_point_opt; + Transform3d hit_tr = raycast_manager.get_transformation(close_point.tr_key); + Vec3d hit_world = hit_tr * close_point.point; + Vec3d offset_world = hit_world - point; // vector in world + Vec3d offset_volume = to_world.inverse().linear() * offset_world; + return offset_volume; + } + + // It is no neccesary to move with origin by very small value + const RaycastManager::Hit &hit = *hit_opt; + if (hit.squared_distance < EPSILON) + return {}; + Transform3d hit_tr = raycast_manager.get_transformation(hit.tr_key); + Vec3d hit_world = hit_tr * hit.position; + Vec3d offset_world = hit_world - point; // vector in world + // TIP: It should be close to only z move + Vec3d offset_volume = to_world.inverse().linear() * offset_world; + return offset_volume; +} + +std::optional calc_distance(const GLVolume &gl_volume, RaycastManager &raycaster, GLCanvas3D &canvas) +{ + const ModelObject *object = get_model_object(gl_volume, canvas.get_model()->objects); + assert(object != nullptr); + if (object == nullptr) + return {}; + + const ModelInstance *instance = get_model_instance(gl_volume, *object); + const ModelVolume *volume = get_model_volume(gl_volume, *object); + assert(instance != nullptr && volume != nullptr); + if (object == nullptr || instance == nullptr || volume == nullptr) + return {}; + + if (volume->is_the_only_one_part()) + return {}; + + if (!volume->emboss_shape.has_value()) + return {}; + + RaycastManager::AllowVolumes condition = create_condition(object->volumes, volume->id()); + //RaycastManager::Meshes meshes = create_meshes(canvas, condition); + //raycaster.actualize(*instance, &condition, &meshes); + return calc_distance(gl_volume, raycaster, &condition, volume->emboss_shape->fix_3mf_tr); +} + +std::optional calc_distance(const GLVolume &gl_volume, const RaycastManager &raycaster, + const RaycastManager::ISkip *condition, const std::optional& fix) { + Transform3d w = gl_volume.world_matrix(); + if (fix.has_value()) + w = w * fix->inverse(); + Vec3d p = w.translation(); + Vec3d dir = -get_z_base(w); + auto hit_opt = raycaster.closest_hit(p, dir, condition); + if (!hit_opt.has_value()) + return {}; + + const RaycastManager::Hit &hit = *hit_opt; + // NOTE: hit.squared_distance is in volume space not world + + const Transform3d &tr = raycaster.get_transformation(hit.tr_key); + Vec3d hit_world = tr * hit.position; + Vec3d p_to_hit = hit_world - p; + double distance_sq = p_to_hit.squaredNorm(); + + // too small distance is calculated as zero distance + if (distance_sq < ::surface_distance_sq.min) + return {}; + + // check maximal distance + const BoundingBoxf3& bb = gl_volume.bounding_box(); + double max_squared_distance = std::max(std::pow(2 * bb.size().z(), 2), ::surface_distance_sq.max); + if (distance_sq > max_squared_distance) + return {}; + + // calculate sign + float sign = (p_to_hit.dot(dir) > 0)? 1.f : -1.f; + + // distiguish sign + return sign * static_cast(sqrt(distance_sq)); +} + +std::optional calc_angle(const Selection &selection) +{ + const GLVolume *gl_volume = selection.get_first_volume(); + assert(gl_volume != nullptr); + if (gl_volume == nullptr) + return {}; + + Transform3d to_world = gl_volume->world_matrix(); + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + assert(volume != nullptr); + assert(volume->emboss_shape.has_value()); + if (volume == nullptr || !volume->emboss_shape.has_value() || !volume->emboss_shape->fix_3mf_tr) + return Emboss::calc_up(to_world, UP_LIMIT); + + // exist fix matrix and must be applied before calculation + to_world = to_world * volume->emboss_shape->fix_3mf_tr->inverse(); + return Emboss::calc_up(to_world, UP_LIMIT); +} + +Transform3d world_matrix_fixed(const GLVolume &gl_volume, const ModelObjectPtrs &objects) +{ + Transform3d res = gl_volume.world_matrix(); + + const ModelVolume *mv = get_model_volume(gl_volume, objects); + if (!mv) + return res; + + const std::optional &es = mv->emboss_shape; + if (!es.has_value()) + return res; + + const std::optional &fix = es->fix_3mf_tr; + if (!fix.has_value()) + return res; + + return res * fix->inverse(); +} + +Transform3d world_matrix_fixed(const Selection &selection) +{ + const GLVolume *gl_volume = get_selected_gl_volume(selection); + assert(gl_volume != nullptr); + if (gl_volume == nullptr) + return Transform3d::Identity(); + + return world_matrix_fixed(*gl_volume, selection.get_model()->objects); +} + +void selection_transform(Selection &selection, const std::function &selection_transformation_fnc) +{ + if (const Transform3d *fix = get_fix_transformation(selection); fix != nullptr) { + // NOTE: need editable gl volume .. can't use selection.get_first_volume() + GLVolume *gl_volume = selection.get_volume(*selection.get_volume_idxs().begin()); + Transform3d volume_tr = gl_volume->get_volume_transformation().get_matrix(); + gl_volume->set_volume_transformation(volume_tr * fix->inverse()); + selection.setup_cache(); + + selection_transformation_fnc(); + + volume_tr = gl_volume->get_volume_transformation().get_matrix(); + gl_volume->set_volume_transformation(volume_tr * (*fix)); + selection.setup_cache(); + } else { + selection_transformation_fnc(); + } + + if (selection.is_single_full_instance()) + selection.synchronize_unselected_instances(Selection::SyncRotationType::GENERAL); +} + +bool face_selected_volume_to_camera(const Camera &camera, GLCanvas3D &canvas, const std::optional &wanted_up_limit) +{ + GLVolume *gl_volume_ptr = get_selected_gl_volume(canvas); + if (gl_volume_ptr == nullptr) + return false; + GLVolume &gl_volume = *gl_volume_ptr; + + const ModelObjectPtrs &objects = canvas.get_model()->objects; + ModelObject *object_ptr = get_model_object(gl_volume, objects); + assert(object_ptr != nullptr); + if (object_ptr == nullptr) + return false; + ModelObject &object = *object_ptr; + + const ModelInstance *instance_ptr = get_model_instance(gl_volume, object); + assert(instance_ptr != nullptr); + if (instance_ptr == nullptr) + return false; + const ModelInstance &instance = *instance_ptr; + + ModelVolume *volume_ptr = get_model_volume(gl_volume, object); + assert(volume_ptr != nullptr); + if (volume_ptr == nullptr) + return false; + ModelVolume &volume = *volume_ptr; + + // Calculate new volume transformation + Transform3d volume_tr = volume.get_matrix(); + std::optional fix; + if (volume.emboss_shape.has_value()) { + fix = volume.emboss_shape->fix_3mf_tr; + if (fix.has_value()) + volume_tr = volume_tr * fix->inverse(); + } + + Transform3d instance_tr = instance.get_matrix(); + Transform3d instance_tr_inv = instance_tr.inverse(); + Transform3d world_tr = instance_tr * volume_tr; // without sla !!! + std::optional current_angle; + if (wanted_up_limit.has_value()) + current_angle = Emboss::calc_up(world_tr, *wanted_up_limit); + + Vec3d world_position = gl_volume.world_matrix()*Vec3d::Zero(); + + assert(camera.get_type() == Camera::EType::Perspective || + camera.get_type() == Camera::EType::Ortho); + Vec3d wanted_direction = (camera.get_type() == Camera::EType::Perspective) ? + Vec3d(camera.get_position() - world_position).normalized() : + (-camera.get_dir_forward()); + + Transform3d new_volume_tr = get_volume_transformation(world_tr, wanted_direction, world_position, + fix, instance_tr_inv, current_angle, wanted_up_limit); + + Selection &selection = canvas.get_selection(); + if (is_embossed_object(selection)) { + // transform instance instead of volume + Transform3d new_instance_tr = instance_tr * new_volume_tr * volume.get_matrix().inverse(); + gl_volume.set_instance_transformation(new_instance_tr); + + // set same transformation to other instances when instance is embossed object + if (selection.is_single_full_instance()) + selection.synchronize_unselected_instances(Selection::SyncRotationType::GENERAL); + } else { + // write result transformation + gl_volume.set_volume_transformation(new_volume_tr); + } + + if (volume.type() == ModelVolumeType::MODEL_PART) { + object.invalidate_bounding_box(); + object.ensure_on_bed(); + } + + canvas.do_rotate(L("Face the camera")); + wxGetApp().obj_manipul()->set_dirty(); + return true; +} + +void do_local_z_rotate(Selection &selection, double relative_angle) { + assert(!selection.is_empty()); + if(selection.is_empty()) return; + + bool is_single_volume = selection.volumes_count() == 1; + assert(is_single_volume); + if (!is_single_volume) return; + + // Fix angle for mirrored volume + bool is_mirrored = false; + const GLVolume* gl_volume = selection.get_first_volume(); + if (gl_volume != nullptr) { + const ModelInstance *instance = get_model_instance(*gl_volume, selection.get_model()->objects); + bool is_instance_mirrored = (instance != nullptr)? has_reflection(instance->get_matrix()) : false; + if (is_embossed_object(selection)) { + is_mirrored = is_instance_mirrored; + } else { + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + if (volume != nullptr) + is_mirrored = is_instance_mirrored != has_reflection(volume->get_matrix()); + } + } + if (is_mirrored) + relative_angle *= -1; + + selection.setup_cache(); + auto selection_rotate_fnc = [&selection, &relative_angle](){ + selection.rotate(Vec3d(0., 0., relative_angle), get_drag_transformation_type(selection)); + }; + selection_transform(selection, selection_rotate_fnc); +} + +void do_local_z_move(Selection &selection, double relative_move) { + assert(!selection.is_empty()); + if (selection.is_empty()) return; + + selection.setup_cache(); + auto selection_translate_fnc = [&selection, relative_move]() { + Vec3d translate = Vec3d::UnitZ() * relative_move; + selection.translate(translate, TransformationType::Local); + }; + selection_transform(selection, selection_translate_fnc); +} + +TransformationType get_drag_transformation_type(const Selection &selection) +{ + return is_embossed_object(selection) ? + TransformationType::Instance_Relative_Joint : + TransformationType::Local_Relative_Joint; +} + +void dragging_rotate_gizmo(double gizmo_angle, std::optional& current_angle, std::optional &start_angle, Selection &selection) +{ + if (!start_angle.has_value()) + // create cache for initial angle + start_angle = current_angle.value_or(0.f); + + gizmo_angle -= PI / 2; // Grabber is upward + + double new_angle = gizmo_angle + *start_angle; + + const GLVolume *gl_volume = selection.get_first_volume(); + assert(gl_volume != nullptr); + if (gl_volume == nullptr) + return; + + bool is_volume_mirrored = has_reflection(gl_volume->get_volume_transformation().get_matrix()); + bool is_instance_mirrored = has_reflection(gl_volume->get_instance_transformation().get_matrix()); + if (is_volume_mirrored != is_instance_mirrored) + new_angle = -gizmo_angle + *start_angle; + + // move to range <-M_PI, M_PI> + Geometry::to_range_pi_pi(new_angle); + + const Transform3d* fix = get_fix_transformation(selection); + double z_rotation = (fix!=nullptr) ? (new_angle - current_angle.value_or(0.f)) : // relative angle + gizmo_angle; // relativity is keep by selection cache + + auto selection_rotate_fnc = [z_rotation, &selection]() { + selection.rotate(Vec3d(0., 0., z_rotation), get_drag_transformation_type(selection)); + }; + selection_transform(selection, selection_rotate_fnc); + + // propagate angle into property + current_angle = static_cast(new_angle); + + // do not store zero + if (is_approx(*current_angle, 0.f)) + current_angle.reset(); +} + +} // namespace Slic3r::GUI + +// private implementation +namespace { + +Vec2d mouse_position(const wxMouseEvent &mouse_event){ + // wxCoord == int --> wx/types.h + Vec2i32 mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + return mouse_coord.cast(); +} + +bool start_dragging(const Vec2d &mouse_pos, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + RaycastManager &raycast_manager, + const std::optional&up_limit) +{ + // selected volume + GLVolume *gl_volume_ptr = get_selected_gl_volume(canvas);//is svg_volume or text volume + if (gl_volume_ptr == nullptr) + return false; + const GLVolume &gl_volume = *gl_volume_ptr; + + // is selected volume closest hovered?//modify by bbs + //const GLVolumePtrs &gl_volumes = canvas.get_volumes().volumes; + /*if (int hovered_idx = canvas.get_first_hover_volume_idx(); hovered_idx < 0) + return false; + else if (auto hovered_idx_ = static_cast(hovered_idx); + hovered_idx_ >= gl_volumes.size() || gl_volumes[hovered_idx_] != gl_volume_ptr) + return false;*/ + + const ModelObjectPtrs &objects = canvas.get_model()->objects; + const ModelObject *mo = get_model_object(gl_volume, objects); + assert(object != nullptr); + if (mo == nullptr) + return false; + + const ModelInstance *instance = get_model_instance(gl_volume, *mo); + const ModelVolume * volume = get_model_volume(gl_volume, *mo); + assert(instance != nullptr && volume != nullptr); + if (mo == nullptr || instance == nullptr || volume == nullptr) + return false; + + // allowed drag&drop by canvas for object + if (volume->is_the_only_one_part()) + return false; + if (raycast_manager.get_meshes().size() == 0) { + return false; + } + RaycastManager::AllowVolumes condition = create_condition(mo->volumes, volume->id()); + //RaycastManager::Meshes meshes = create_meshes(canvas, condition); + // initialize raycasters + // INFO: It could slows down for big objects + // (may be move to thread and do not show drag until it finish) + //raycast_manager.actualize(*instance, &condition, &meshes); + + // world_matrix_fixed() without sla shift + Transform3d to_world = world_matrix_fixed(gl_volume, objects); + + // zero point of volume in world coordinate system + Vec3d volume_center = to_world.translation(); + // screen coordinate of volume center + auto coor = CameraUtils::project(camera, volume_center); + Vec2d mouse_offset = coor.cast() - mouse_pos; + Vec2d mouse_offset_without_sla_shift = mouse_offset; + if (double sla_shift = gl_volume.get_sla_shift_z(); !is_approx(sla_shift, 0.)) { + Transform3d to_world_without_sla_move = instance->get_matrix() * volume->get_matrix(); + if (volume->emboss_shape.has_value() && volume->emboss_shape->fix_3mf_tr.has_value()) + to_world_without_sla_move = to_world_without_sla_move * (*volume->emboss_shape->fix_3mf_tr); + // zero point of volume in world coordinate system + volume_center = to_world_without_sla_move.translation(); + // screen coordinate of volume center + coor = CameraUtils::project(camera, volume_center); + mouse_offset_without_sla_shift = coor.cast() - mouse_pos; + } + + Transform3d volume_tr = gl_volume.get_volume_transformation().get_matrix(); + + // fix baked transformation from .3mf store process + if (const std::optional &es_opt = volume->emboss_shape; es_opt.has_value()) { + const std::optional &fix = es_opt->fix_3mf_tr; + if (fix.has_value()) + volume_tr = volume_tr * fix->inverse(); + } + + Transform3d instance_tr = instance->get_matrix(); + Transform3d instance_tr_inv = instance_tr.inverse(); + Transform3d world_tr = instance_tr * volume_tr; + std::optional start_angle; + if (up_limit.has_value()) { + start_angle = Emboss::calc_up(world_tr, *up_limit); + if (start_angle.has_value() && has_reflection(world_tr)) + start_angle = -(*start_angle); + } + + std::optional start_distance; + if (!volume->emboss_shape->projection.use_surface) + start_distance = calc_distance(gl_volume, raycast_manager, &condition, volume->emboss_shape->fix_3mf_tr); + surface_drag = SurfaceDrag{mouse_offset, world_tr, instance_tr_inv, + gl_volume_ptr, condition, start_angle, + start_distance, true, mouse_offset_without_sla_shift}; + + // disable moving with object by mouse + //canvas.enable_moving(false);//modify by bbs + //canvas.enable_picking(false); + return true; +} + +Transform3d get_volume_transformation( + Transform3d world, // from volume + const Vec3d& world_dir, // wanted new direction + const Vec3d& world_position, // wanted new position + const std::optional& fix, // [optional] fix matrix + // Invers transformation of text volume instance + // Help convert world transformation to instance space + const Transform3d& instance_inv, + // initial rotation in Z axis + std::optional current_angle, + const std::optional &up_limit) +{ + auto world_linear = world.linear(); + // Calculate offset: transformation to wanted position + { + // Reset skew of the text Z axis: + // Project the old Z axis into a new Z axis, which is perpendicular to the old XY plane. + Vec3d old_z = world_linear.col(2); + Vec3d new_z = world_linear.col(0).cross(world_linear.col(1)); + world_linear.col(2) = new_z * (old_z.dot(new_z) / new_z.squaredNorm()); + } + + Vec3d text_z_world = world_linear.col(2); // world_linear * Vec3d::UnitZ() + auto z_rotation = Eigen::Quaternion::FromTwoVectors(text_z_world, world_dir); + Transform3d world_new = z_rotation * world; + auto world_new_linear = world_new.linear(); + + // Fix direction of up vector to zero initial rotation + if(up_limit.has_value()){ + Vec3d z_world = world_new_linear.col(2); + z_world.normalize(); + Vec3d wanted_up = Emboss::suggest_up(z_world, *up_limit); + + Vec3d y_world = world_new_linear.col(1); + auto y_rotation = Eigen::Quaternion::FromTwoVectors(y_world, wanted_up); + + world_new = y_rotation * world_new; + world_new_linear = world_new.linear(); + } + + // Edit position from right + Transform3d volume_new{Eigen::Translation(instance_inv * world_position)}; + volume_new.linear() = instance_inv.linear() * world_new_linear; + + // Check that transformation matrix is valid transformation + assert(volume_new.matrix()(0, 0) == volume_new.matrix()(0, 0)); // Check valid transformation not a NAN + if (volume_new.matrix()(0, 0) != volume_new.matrix()(0, 0)) + return Transform3d::Identity(); + + // Check that scale in world did not changed + assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitY()).has_value()); + assert(!calc_scale(world_linear, world_new_linear, Vec3d::UnitZ()).has_value()); + + // fix baked transformation from .3mf store process + if (fix.has_value()) + volume_new = volume_new * (*fix); + + // apply move in Z direction and rotation by up vector + Emboss::apply_transformation(current_angle, {}, volume_new); + + return volume_new; +} + +bool dragging(const Vec2d &mouse_pos, + const Camera &camera, + SurfaceDrag &surface_drag, + GLCanvas3D &canvas, + const RaycastManager &raycast_manager, + const std::optional &up_limit) +{ + Vec2d offseted_mouse = mouse_pos + surface_drag.mouse_offset_without_sla_shift; + std::optional hit = ray_from_camera( + raycast_manager, offseted_mouse, camera, &surface_drag.condition); + + surface_drag.exist_hit = hit.has_value(); + if (!hit.has_value()) { + // cross hair need redraw + canvas.set_as_dirty(); + return true; + } + + const ModelVolume *volume = get_model_volume(*surface_drag.gl_volume, canvas.get_model()->objects); + std::optional fix; + if (volume !=nullptr && + volume->emboss_shape.has_value() && + volume->emboss_shape->fix_3mf_tr.has_value()) + fix = volume->emboss_shape->fix_3mf_tr; + Transform3d volume_new = get_volume_transformation(surface_drag.world, hit->normal, hit->position, + fix, surface_drag.instance_inv, surface_drag.start_angle, up_limit); + + // Update transformation for all instances + for (GLVolume *vol : canvas.get_volumes().volumes) { + if (vol->object_idx() != surface_drag.gl_volume->object_idx() || + vol->volume_idx() != surface_drag.gl_volume->volume_idx()) + continue; + vol->set_volume_transformation(volume_new); + } + + canvas.set_as_dirty(); + // Show current position in manipulation panel + wxGetApp().obj_manipul()->set_dirty(); + return true; +} + +bool is_embossed_object(const Selection &selection) +{ + assert(selection.volumes_count() == 1); + return selection.is_single_full_object() || selection.is_single_full_instance(); +} + +const Transform3d *get_fix_transformation(const Selection &selection) { + const GLVolume *gl_volume = get_selected_gl_volume(selection); + assert(gl_volume != nullptr); + if (gl_volume == nullptr) + return nullptr; + + const ModelVolume *volume = get_model_volume(*gl_volume, selection.get_model()->objects); + assert(volume != nullptr); + if (volume == nullptr) + return nullptr; + + const std::optional &es = volume->emboss_shape; + if (!volume->emboss_shape.has_value()) + return nullptr; + if (!es->fix_3mf_tr.has_value()) + return nullptr; + return &(*es->fix_3mf_tr); +} + +} // namespace diff --git a/src/slic3r/GUI/SurfaceDrag.hpp b/src/slic3r/GUI/SurfaceDrag.hpp new file mode 100644 index 000000000..a57142afc --- /dev/null +++ b/src/slic3r/GUI/SurfaceDrag.hpp @@ -0,0 +1,170 @@ +#ifndef slic3r_SurfaceDrag_hpp_ +#define slic3r_SurfaceDrag_hpp_ + +#include +#include "libslic3r/Point.hpp" // Vec2d, Transform3d +#include "slic3r/Utils/RaycastManager.hpp" +#include "wx/event.h" // wxMouseEvent +#include + +namespace Slic3r { +class GLVolume; +class ModelVolume; +} // namespace Slic3r + +namespace Slic3r::GUI { +class GLCanvas3D; +class Selection; +class TransformationType; +struct Camera; + +// Data for drag&drop over surface with mouse +struct SurfaceDrag +{ + // hold screen coor offset of cursor from object center + Vec2d mouse_offset; + + // Start dragging text transformations to world + Transform3d world; + + // Invers transformation of text volume instance + // Help convert world transformation to instance space + Transform3d instance_inv; + + // Dragged gl volume + GLVolume *gl_volume; + + // condition for raycaster + RaycastManager::AllowVolumes condition; + + // initial rotation in Z axis of volume + std::optional start_angle; + + // initial Z distance from surface + std::optional start_distance; + + // Flag whether coordinate hit some volume + bool exist_hit = true; + + // hold screen coor offset of cursor from object center without SLA shift + Vec2d mouse_offset_without_sla_shift; +}; + +// Limit direction of up vector on model +// Between side and top surface +constexpr double UP_LIMIT = 0.9; + +/// +/// Mouse event handler, when move(drag&drop) volume over model surface +/// NOTE: Dragged volume has to be selected. And also has to be hovered on start of dragging. +/// +/// Contain type of event and mouse position +/// Actual viewport of camera +/// Structure which keep information about dragging +/// Contain gl_volumes and selection +/// AABB trees for raycast in object +/// Refresh state inside of function +/// When set than use correction of up vector +/// True when event is processed otherwise false +bool on_mouse_surface_drag(const wxMouseEvent &mouse_event, + const Camera &camera, + std::optional &surface_drag, + GLCanvas3D &canvas, + RaycastManager &raycast_manager, + const std::optional&up_limit = {}); + +/// +/// Calculate translation of volume onto surface of model +/// +/// Must contain only one selected volume, Transformation of current instance +/// AABB trees of object. Actualize object +/// Offset of volume in volume coordinate +std::optional calc_surface_offset(const Selection &selection, RaycastManager &raycast_manager); + +/// +/// Calculate distance by ray to surface of object in emboss direction +/// +/// Define embossed volume +/// Way to cast rays to object +/// Contain model +/// Calculated distance from surface +std::optional calc_distance(const GLVolume &gl_volume, RaycastManager &raycaster, GLCanvas3D &canvas); +std::optional calc_distance(const GLVolume &gl_volume, const RaycastManager &raycaster, + const RaycastManager::ISkip *condition, const std::optional& fix); + +/// +/// Calculate up vector angle +/// +/// Calculation of angle is for selected one volume +/// +std::optional calc_angle(const Selection &selection); + +/// +/// Get transformation to world +/// - use fix after store to 3mf when exists +/// +/// Scene volume +/// To identify Model volume with fix transformation +/// Fixed Transformation of gl_volume +Transform3d world_matrix_fixed(const GLVolume &gl_volume, const ModelObjectPtrs& objects); + +/// +/// Get transformation to world +/// - use fix after store to 3mf when exists +/// NOTE: when not one volume selected return identity +/// +/// Selected volume +/// Fixed Transformation of selected volume in selection +Transform3d world_matrix_fixed(const Selection &selection); + +/// +/// Wrap function around selection transformation to apply fix transformation +/// Fix transformation is needed because of (store/load) volume (to/from) 3mf +/// +/// Selected gl volume will be modified +/// Function modified Selection transformation +void selection_transform(Selection &selection, const std::function& selection_transformation_fnc); + +/// +/// Apply camera direction for emboss direction +/// +/// Define view vector +/// Containe Selected ModelVolume to modify orientation +/// [Optional]Limit for direction of up vector +/// True when apply change otherwise false +bool face_selected_volume_to_camera(const Camera &camera, GLCanvas3D &canvas, const std::optional &wanted_up_limit = {}); + +/// +/// Rotation around z Axis(emboss direction) +/// +/// Selected volume for rotation +/// Relative angle to rotate around emboss direction +void do_local_z_rotate(Selection &selection, double relative_angle); + +/// +/// Translation along local z Axis (emboss direction) +/// +/// Selected volume for translate +/// Relative move along emboss direction +void do_local_z_move(Selection &selection, double relative_move); + +/// +/// Distiguish between object and volume +/// Differ in possible transformation type +/// +/// Contain selected volume/object +/// Transformation to use +TransformationType get_drag_transformation_type(const Selection &selection); + +/// +/// On dragging rotate gizmo func +/// Transform GLVolume from selection +/// +/// GLGizmoRotate::get_angle() +/// In/Out current angle visible in UI +/// Cache for start dragging angle +/// Selected only Actual embossed volume +void dragging_rotate_gizmo(double gizmo_angle, std::optional& current_angle, std::optional &start_angle, Selection &selection); + +} // namespace Slic3r::GUI +#endif // slic3r_SurfaceDrag_hpp_ \ No newline at end of file diff --git a/src/slic3r/Utils/FontUtils.cpp b/src/slic3r/Utils/FontUtils.cpp index 733038c2c..82863eb14 100644 --- a/src/slic3r/Utils/FontUtils.cpp +++ b/src/slic3r/Utils/FontUtils.cpp @@ -1,4 +1,5 @@ #include "FontUtils.hpp" +//#define STB_TRUETYPE_IMPLEMENTATION #include "imgui/imstb_truetype.h" #include "libslic3r/Utils.hpp" #include @@ -58,9 +59,6 @@ std::string get_file_path(const wxFont &font) return path_str; } #endif // __APPLE__ - -using fontinfo_opt = std::optional; - std::string get_human_readable_name(const wxFont &font) { if (!font.IsOk()) return "Font is NOT ok."; @@ -72,6 +70,8 @@ std::string get_human_readable_name(const wxFont &font) } } +using fontinfo_opt = std::optional; + fontinfo_opt load_font_info(const unsigned char *data, unsigned int index) { int font_offset = stbtt_GetFontOffsetForIndex(data, index); @@ -275,7 +275,7 @@ bool can_generate_text_shape_from_font(const stbtt_fontinfo &font_info) int num_verts = stbtt_GetGlyphShape(&font_info, glyph_index, &vertices); if (num_verts <= 0) return false; - + return true; } @@ -288,7 +288,7 @@ bool can_generate_text_shape(const std::string& font_name) { fontinfo_opt font_info_opt = load_font_info(font->data->data(), 0); if (!font_info_opt.has_value()) return false; - + return can_generate_text_shape_from_font(*font_info_opt); } diff --git a/src/slic3r/Utils/RaycastManager.cpp b/src/slic3r/Utils/RaycastManager.cpp new file mode 100644 index 000000000..1297ddeb2 --- /dev/null +++ b/src/slic3r/Utils/RaycastManager.cpp @@ -0,0 +1,391 @@ +#include "RaycastManager.hpp" +#include + +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/Camera.hpp" +#include "slic3r/GUI/CameraUtils.hpp" + +using namespace Slic3r::GUI; + +namespace{ +using namespace Slic3r; +void actualize(RaycastManager::Meshes &meshes, const ModelVolumePtrs &volumes, const RaycastManager::ISkip *skip, RaycastManager::Meshes *input = nullptr); +const sla::IndexedMesh * get_mesh(const RaycastManager::Meshes &meshes, size_t volume_id); +RaycastManager::TrKey create_key(const ModelVolume& volume, const ModelInstance& instance){ + return std::make_pair(instance.id().id, volume.id().id); } +RaycastManager::TrItems::iterator find(RaycastManager::TrItems &items, const RaycastManager::TrKey &key); +RaycastManager::TrItems::const_iterator find(const RaycastManager::TrItems &items, const RaycastManager::TrKey &key); +bool is_lower_key(const RaycastManager::TrKey &k1, const RaycastManager::TrKey &k2) { + return k1.first < k2.first || (k1.first == k2.first && k1.second < k2.second); } +bool is_lower(const RaycastManager::TrItem &i1, const RaycastManager::TrItem &i2) { + return is_lower_key(i1.first, i2.first); }; +template inline void erase(std::vector &vec, const std::vector &flags); +} + +void RaycastManager::actualize(const ModelObject &object, const ISkip *skip, Meshes *meshes) +{ + // actualize MeshRaycaster + ::actualize(m_meshes, object.volumes, skip, meshes); + + // check if inscance was removed + std::vector removed_transf(m_transformations.size(), {true}); + + bool need_sort = false; + // actualize transformation matrices + for (const ModelVolume *volume : object.volumes) { + if (skip != nullptr && skip->skip(volume->id().id)) continue; + const Transform3d &volume_tr = volume->get_matrix(); + for (const ModelInstance *instance : object.instances) { + const Transform3d &instrance_tr = instance->get_matrix(); + Transform3d transformation = instrance_tr * volume_tr; + TrKey key = ::create_key(*volume, *instance); + auto item = ::find(m_transformations, key); + if (item != m_transformations.end()) { + // actualize transformation all the time + item->second = transformation; + size_t index = item - m_transformations.begin(); + removed_transf[index] = false; + } else { + // add new transformation + m_transformations.emplace_back(key, transformation); + need_sort = true; + } + } + } + + // clean other transformation + ::erase(m_transformations, removed_transf); + + if (need_sort) + std::sort(m_transformations.begin(), m_transformations.end(), ::is_lower); +} + +void RaycastManager::actualize(const ModelInstance &instance, const ISkip *skip, Meshes *meshes) +{ + const ModelVolumePtrs &volumes = instance.get_object()->volumes; + + // actualize MeshRaycaster + ::actualize(m_meshes, volumes, skip, meshes); + + // check if inscance was removed + std::vector removed_transf(m_transformations.size(), {true}); + + bool need_sort = false; + // actualize transformation matrices + for (const ModelVolume *volume : volumes) { + if (skip != nullptr && skip->skip(volume->id().id)) + continue; + const Transform3d &volume_tr = volume->get_matrix(); + const Transform3d &instrance_tr = instance.get_matrix(); + Transform3d transformation = instrance_tr * volume_tr; + TrKey key = ::create_key(*volume, instance); + auto item = ::find(m_transformations, key); + if (item != m_transformations.end()) { + // actualize transformation all the time + item->second = transformation; + size_t index = item - m_transformations.begin(); + removed_transf[index] = false; + } else { + // add new transformation + m_transformations.emplace_back(key, transformation); + need_sort = true; + } + } + + // clean other transformation + ::erase(m_transformations, removed_transf); + + if (need_sort) + std::sort(m_transformations.begin(), m_transformations.end(), ::is_lower); +} + +std::optional RaycastManager::first_hit(const Vec3d& point, const Vec3d& direction, const ISkip *skip) const +{ + // Improve: it is not neccessaru to use AABBMesh and calc normal for every hit + + // Results + const sla::IndexedMesh *hit_mesh = nullptr; + double hit_squared_distance = 0.; + int hit_face = -1; + Vec3d hit_world; + const Transform3d *hit_tramsformation = nullptr; + const TrKey *hit_key = nullptr; + + for (const auto &[key, transformation]: m_transformations) { + size_t volume_id = key.second; + if (skip != nullptr && skip->skip(volume_id)) + continue; + const sla::IndexedMesh *mesh = ::get_mesh(m_meshes, volume_id); + if (mesh == nullptr) continue; + Transform3d inv = transformation.inverse(); + + // transform input into mesh world + Vec3d point_ = inv * point; + Vec3d direction_= inv.linear() * direction; + + std::vector hits = mesh->query_ray_hits(point_, direction_); + if (hits.empty()) continue; // no intersection found + + const sla::IndexedMesh::hit_result &hit = hits.front(); + + // convert to world + Vec3d world = transformation * hit.position(); + double squared_distance = (point - world).squaredNorm(); + if (hit_mesh != nullptr && + hit_squared_distance < squared_distance) + continue; // exist closer one + + hit_mesh = mesh; + hit_squared_distance = squared_distance; + hit_face = hit.face(); + hit_world = world; + hit_tramsformation = &transformation; + hit_key = &key; + } + + if (hit_mesh == nullptr) + return {}; + + // Calculate normal from transformed triangle + // NOTE: Anisotropic transformation of normal is not perpendiculat to triangle + const Vec3i32 tri = hit_mesh->indices(hit_face); + std::array pts; + auto tr = hit_tramsformation->linear(); + for (int i = 0; i < 3; ++i) + pts[i] = tr * hit_mesh->vertices(tri[i]).cast(); + Vec3d normal_world = (pts[1] - pts[0]).cross(pts[2] - pts[1]); + if (has_reflection(*hit_tramsformation)) + normal_world *= -1; + normal_world.normalize(); + + SurfacePoint point_world{hit_world, normal_world}; + return RaycastManager::Hit{point_world, *hit_key, hit_squared_distance}; +} + +std::optional RaycastManager::closest_hit(const Vec3d &point, const Vec3d &direction, const ISkip *skip) const +{ + std::optional closest; + for (const auto &[key, transformation] : m_transformations) { + size_t volume_id = key.second; + if (skip != nullptr && skip->skip(volume_id)) continue; + const sla::IndexedMesh *mesh = ::get_mesh(m_meshes, volume_id); + if (mesh == nullptr) continue; + Transform3d tr_inv = transformation.inverse(); + Vec3d mesh_point = tr_inv * point; + Vec3d mesh_direction = tr_inv.linear() * direction; + + // Need for detect that actual point position is on correct place + Vec3d point_positive = mesh_point - mesh_direction; + Vec3d point_negative = mesh_point + mesh_direction; + + // Throw ray to both directions of ray + std::vector hits = mesh->query_ray_hits(point_positive, mesh_direction); + std::vector hits_neg = mesh->query_ray_hits(point_negative, -mesh_direction); + hits.insert(hits.end(), std::make_move_iterator(hits_neg.begin()), std::make_move_iterator(hits_neg.end())); + for (const sla::IndexedMesh::hit_result &hit : hits) { + Vec3d diff = mesh_point - hit.position(); + double squared_distance = diff.squaredNorm(); + if (closest.has_value() && + closest->squared_distance < squared_distance) + continue; + closest = Hit{{hit.position(), hit.normal()}, key, squared_distance}; + } + } + return closest; +} + +std::optional RaycastManager::closest(const Vec3d &point, const ISkip *skip) const +{ + std::optional closest; + for (const auto &[key, transformation] : m_transformations) { + size_t volume_id = key.second; + if (skip != nullptr && skip->skip(volume_id)) + continue; + const sla::IndexedMesh *mesh = ::get_mesh(m_meshes, volume_id); + if (mesh == nullptr) continue; + Transform3d tr_inv = transformation.inverse(); + Vec3d mesh_point = tr_inv * point; + + int face_idx = 0; + Vec3d closest_point; + Vec3d pointd = point.cast(); + mesh->squared_distance(pointd, face_idx, closest_point); + + double squared_distance = (mesh_point - closest_point).squaredNorm(); + if (closest.has_value() && closest->squared_distance < squared_distance) + continue; + + closest = ClosePoint{key, closest_point, squared_distance}; + } + return closest; +} + +Slic3r::Transform3d RaycastManager::get_transformation(const TrKey &tr_key) const { + auto tr = ::find(m_transformations, tr_key); + if (tr == m_transformations.end()) + return Transform3d::Identity(); + return tr->second; +} + +void Slic3r::GUI::RaycastManager::clear() { + m_meshes.clear(); + m_transformations.clear(); +} + +namespace { +void actualize(RaycastManager::Meshes &meshes, const ModelVolumePtrs &volumes, const RaycastManager::ISkip *skip, RaycastManager::Meshes* inputs) +{ + // check if volume was removed + std::vector removed_meshes(meshes.size(), {true}); + bool need_sort = false; + // actualize MeshRaycaster + for (const ModelVolume *volume : volumes) { + size_t oid = volume->id().id; + if (skip != nullptr && skip->skip(oid)) + continue; + auto is_oid = [oid](const RaycastManager::Mesh &it) { return oid == it.first; }; + if (auto item = std::find_if(meshes.begin(), meshes.end(), is_oid); + item != meshes.end()) { + size_t index = item - meshes.begin(); + removed_meshes[index] = false; + continue; + } + + // exist AABB in inputs ? + if (inputs != nullptr) { + auto input = std::find_if(inputs->begin(), inputs->end(), is_oid); + if (input != inputs->end()) { + meshes.emplace_back(std::move(*input)); + need_sort = true; + continue; + } + } + + // add new raycaster + bool calculate_epsilon = true; + auto mesh = std::make_unique(volume->mesh(), calculate_epsilon); + meshes.emplace_back(std::make_pair(oid, std::move(mesh))); + need_sort = true; + } + + // clean other raycasters + erase(meshes, removed_meshes); + + // All the time meshes must be sorted by volume id - for faster search + if (need_sort) { + auto is_lower = [](const RaycastManager::Mesh &m1, const RaycastManager::Mesh &m2) { return m1.first < m2.first; }; + std::sort(meshes.begin(), meshes.end(), is_lower); + } +} + +const Slic3r::sla::IndexedMesh *get_mesh(const RaycastManager::Meshes &meshes, size_t volume_id) +{ + auto is_lower_index = [](const RaycastManager::Mesh &m, size_t i) { return m.first < i; }; + auto it = std::lower_bound(meshes.begin(), meshes.end(), volume_id, is_lower_index); + if (it == meshes.end() || it->first != volume_id) + return nullptr; + return &(*(it->second)); +} + +RaycastManager::TrItems::iterator find(RaycastManager::TrItems &items, const RaycastManager::TrKey &key) { + auto fnc = [](const RaycastManager::TrItem &it, const RaycastManager::TrKey &l_key) { return is_lower_key(it.first, l_key); }; + auto it = std::lower_bound(items.begin(), items.end(), key, fnc); + if (it != items.end() && it->first != key) + return items.end(); + return it; +} + +RaycastManager::TrItems::const_iterator find(const RaycastManager::TrItems &items, const RaycastManager::TrKey &key) +{ + auto fnc = [](const RaycastManager::TrItem &it, const RaycastManager::TrKey &l_key) { return is_lower_key(it.first, l_key); }; + auto it = std::lower_bound(items.begin(), items.end(), key, fnc); + if (it != items.end() && it->first != key) + return items.end(); + return it; +} + +template inline void erase(std::vector &vec, const std::vector &flags) +{ + if (vec.size() < flags.size() || flags.empty()) + return; + + // reverse iteration over flags to erase indices from back to front. + for (int i = static_cast(flags.size()) - 1; i >= 0; --i) + if (flags[i]) + vec.erase(vec.begin() + i); +} + +} // namespace + +namespace Slic3r::GUI{ + +RaycastManager::Meshes create_meshes(GLCanvas3D &canvas, const RaycastManager::AllowVolumes &condition) +{ + RaycastManager::Meshes meshes;//from + throw; + //std::map> m_mesh_raycaster_map;//for text + /* + m_mesh_raycaster_map[v] = std::make_shared(mesh, -1); + SceneRaycaster::EType type = SceneRaycaster::EType::Volume; + auto scene_casters = canvas.get_raycasters_for_picking(type); + if (scene_casters == nullptr) + return {}; + const std::vector> &casters = *scene_casters; + + const GLVolumePtrs &gl_volumes = canvas.get_volumes().volumes; + const ModelObjectPtrs &objects = canvas.get_model()->objects; + + for (const std::shared_ptr &caster : casters) { + int index = SceneRaycaster::decode_id(type, caster->get_id()); + if (index < 0) + continue; + auto index_ = static_cast(index); + if(index_ >= gl_volumes.size()) + continue; + const GLVolume *gl_volume = gl_volumes[index_]; + if (gl_volume == nullptr) + continue; + const ModelVolume *volume = get_model_volume(*gl_volume, objects); + if (volume == nullptr) + continue; + size_t id = volume->id().id; + if (condition.skip(id)) + continue; + auto mesh = std::make_unique(caster->get_raycaster()->get_aabb_mesh()); + meshes.emplace_back(std::make_pair(id, std::move(mesh))); + }*/ + return meshes; +} + + +std::optional ray_from_camera(const RaycastManager &raycaster, + const Vec2d &mouse_pos, + const Camera &camera, + const RaycastManager::ISkip *skip) +{ + Vec3d point; + Vec3d direction; + CameraUtils::ray_from_screen_pos(camera, mouse_pos, point, direction); + return raycaster.first_hit(point, direction, skip); +} + +RaycastManager::AllowVolumes create_condition(const ModelVolumePtrs &volumes, const ObjectID &disallowed_volume_id) { + std::vector allowed_volumes_id; + if (volumes.size() > 1) { + allowed_volumes_id.reserve(volumes.size() - 1); + for (const ModelVolume *v : volumes) { + // drag only above part not modifiers or negative surface + if (!v->is_model_part()) + continue; + + // skip actual selected object + if (v->id() == disallowed_volume_id) + continue; + + allowed_volumes_id.emplace_back(v->id().id); + } + } + return RaycastManager::AllowVolumes(allowed_volumes_id); +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/Utils/RaycastManager.hpp b/src/slic3r/Utils/RaycastManager.hpp new file mode 100644 index 000000000..3ac2c220a --- /dev/null +++ b/src/slic3r/Utils/RaycastManager.hpp @@ -0,0 +1,188 @@ +#ifndef slic3r_RaycastManager_hpp_ +#define slic3r_RaycastManager_hpp_ + +#include // unique_ptr +#include +#include "libslic3r/SLA/IndexedMesh.hpp" //#include "libslic3r/AABBMesh.hpp" // Structure to cast rays +#include "libslic3r/Point.hpp" // Transform3d +#include "libslic3r/ObjectID.hpp" +#include "libslic3r/Model.hpp" // ModelObjectPtrs, ModelObject, ModelInstance, ModelVolume + +namespace Slic3r::GUI{ + +/// +/// Cast rays from camera to scene +/// Used for find hit point on model volume under mouse cursor +/// +class RaycastManager +{ +// Public structures used by RaycastManager +public: + + // ModelVolume.id + using Mesh = std::pair >;//AABBMesh + using Meshes = std::vector; + + // Key for transformation consist of unique volume and instance id ... ObjectId() + // ModelInstance, ModelVolume + using TrKey = std::pair; + using TrItem = std::pair; + using TrItems = std::vector; + + /// + /// Interface for identify allowed volumes to cast rays. + /// + class ISkip{ + public: + virtual ~ISkip() = default; + + /// + /// Condition to not process model volume + /// + /// ObjectID of model volume to not process + /// True on skip otherwise false + virtual bool skip(const size_t &model_volume_id) const { return false; } + }; + + // TODO: it is more general object move outside of this class + template + struct SurfacePoint { + using Vec3 = Eigen::Matrix; + Vec3 position = Vec3::Zero(); + Vec3 normal = Vec3::UnitZ(); + }; + + struct Hit : public SurfacePoint + { + TrKey tr_key; + double squared_distance; + }; + + struct ClosePoint + { + TrKey tr_key; + Vec3d point; + double squared_distance; + }; + const Meshes& get_meshes() { return m_meshes; } + // Members +private: + // Keep structure to fast cast rays + // meshes are sorted by volume_id for faster search + Meshes m_meshes; + + // Keep transformation of meshes + TrItems m_transformations; + // Note: one mesh could have more transformations ... instances + +public: + + /// + /// Actualize raycasters + transformation + /// Detection of removed object + /// Detection of removed instance + /// Detection of removed volume + /// + /// Model representation + /// Condifiton for skip actualization + /// Speed up for already created AABBtrees + void actualize(const ModelObject &object, const ISkip *skip = nullptr, Meshes *meshes = nullptr); + void actualize(const ModelInstance &instance, const ISkip *skip = nullptr, Meshes* meshes = nullptr); + + class SkipVolume: public ISkip + { + size_t volume_id; + public: + SkipVolume(size_t volume_id) : volume_id(volume_id) {} + bool skip(const size_t &model_volume_id) const override { return model_volume_id == volume_id; } + }; + + class AllowVolumes: public ISkip + { + std::vector allowed_id; + public: + AllowVolumes() = default; + AllowVolumes(std::vector allowed_id) : allowed_id(allowed_id) {} + void clear() { + allowed_id.clear(); + } + bool skip(const size_t &model_volume_id) const override { + if (allowed_id.size() == 0) { + return false; + } + auto it = std::find(allowed_id.begin(), allowed_id.end(), model_volume_id); + return it == allowed_id.end(); + } + }; + + /// + /// Unproject on mesh and return closest hit to point in given direction + /// + /// Position in space + /// Casted ray direction + /// Define which caster will be skipped, null mean no skip + /// Position on surface, normal direction in world coorinate + /// + key, to know hitted instance and volume + std::optional first_hit(const Vec3d &point, const Vec3d &direction, const ISkip *skip = nullptr) const; + + /// + /// Unproject Ray(point direction) on mesh to find closest hit of surface in given direction + /// NOTE: It inspect also oposit direction of ray !! + /// + /// Start point for ray + /// Direction of ray, orientation doesn't matter, both are used + /// Define which caster will be skipped, null mean no skip + /// Position on surface, normal direction and transformation key, which define hitted object instance + std::optional closest_hit(const Vec3d &point, const Vec3d &direction, const ISkip *skip = nullptr) const; + + /// + /// Search of closest point + /// + /// Point + /// Define which caster will be skipped, null mean no skip + /// + std::optional closest(const Vec3d &point, const ISkip *skip = nullptr) const; + + /// + /// Getter on transformation from hitted volume to world + /// + /// Define transformation + /// Transformation for key + Transform3d get_transformation(const TrKey &tr_key) const; + void clear(); +}; + +class GLCanvas3D; +/// +/// Use scene Raycasters and prepare data for actualize RaycasterManager +/// +/// contain Scene raycasters +/// Limit for scene casters +/// Meshes +RaycastManager::Meshes create_meshes(GLCanvas3D &canvas, const RaycastManager::AllowVolumes &condition); + +struct Camera; +/// +/// Unproject on mesh by Mesh raycasters +/// +/// Position of mouse on screen +/// Projection params +/// Define which caster will be skipped, null mean no skip +/// Position on surface, normal direction in world coorinate +/// + key, to know hitted instance and volume +std::optional ray_from_camera(const RaycastManager &raycaster, + const Vec2d &mouse_pos, + const Camera &camera, + const RaycastManager::ISkip *skip); + +/// +/// Create condition to allowe only parts from volumes without one given +/// +/// List of allowed volumes included one which is dissalowed and non parts +/// Disallowed volume +/// Condition +RaycastManager::AllowVolumes create_condition(const ModelVolumePtrs &volumes, const ObjectID &disallowed_volume_id); + +} // namespace Slic3r::GUI + +#endif // slic3r_RaycastManager_hpp_