From 933aa3050bf5297b3714fb59575b3ec86e017112 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 9 Dec 2023 22:46:18 +0800 Subject: [PATCH] Port Emboss & SVG gizmo from PrusaSlicer (#2819) * Rework UI jobs to make them more understandable and flexible. * Update Orca specific jobs * Fix progress issue * Fix dark mode and window radius * Update cereal version from 1.2.2 to 1.3.0 (cherry picked from commit prusa3d/PrusaSlicer@057232a2756dad131aba2b989e558bb1560eed93) * Initial port of Emboss gizmo * Bump up CGAL version to 5.4 (cherry picked from commit prusa3d/PrusaSlicer@1bf9dee3e7d36a66588bfccb7beda10add154b8a) * Fix text rotation * Fix test dragging * Add text gizmo to right click menu * Initial port of SVG gizmo * Fix text rotation * Fix Linux build * Fix "from surface" * Fix -90 rotation * Fix icon path * Fix loading font with non-ascii name * Fix storing non-utf8 font descriptor in 3mf file * Fix filtering with non-utf8 characters * Emboss: Use Orca style input dialog * Fix build on macOS * Fix tooltip color in light mode * InputText: fixed incorrect padding when FrameBorder > 0. (ocornut/imgui#4794, ocornut/imgui#3781) InputTextMultiline: fixed vertical tracking with large values of FramePadding.y. (ocornut/imgui#3781, ocornut/imgui#4794) (cherry picked from commit ocornut/imgui@072caa4a9068b93070567879afc5fbcbb772c5ae) (cherry picked from commit ocornut/imgui@bdd2a943150628fdc827c285eafaced6005a2f64) * SVG: Use Orca style input dialog * Fix job progress update * Fix crash when select editing text in preview screen * Use Orca checkbox style * Fix issue that toolbar icons are kept regenerated * Emboss: Fix text & icon alignment * SVG: Fix text & icon alignment * Emboss: fix toolbar icon mouse hover state * Add a simple subtle outline effect by drawing back faces using wireframe mode * Disable selection outlines * Show outline in white if the model color is too dark * Make the outline algorithm more reliable * Enable cull face, which fix render on Linux * Fix `disable_cullface` * Post merge fix * Optimize selection rendering * Fix scale gizmo * Emboss: Fix text rotation if base object is scaled * Fix volume synchronize * Fix emboss rotation * Emboss: Fix advance toggle * Fix text position after reopened the project * Make font style preview darker * Make font style preview selector height shorter --------- Co-authored-by: tamasmeszaros Co-authored-by: ocornut Co-authored-by: SoftFever --- deps/CGAL/CGAL.cmake | 28 +- deps/Cereal/Cereal.cmake | 12 +- resources/images/add_copies.svg | 4 +- resources/images/add_text_modifier.svg | 4 + resources/images/add_text_negative.svg | 4 + resources/images/add_text_part.svg | 4 + resources/images/align_horizontal_center.svg | 7 + resources/images/align_horizontal_left.svg | 7 + resources/images/align_horizontal_right.svg | 7 + resources/images/align_vertical_bottom.svg | 60 + resources/images/align_vertical_center.svg | 60 + resources/images/align_vertical_top.svg | 60 + resources/images/burn.svg | 4 + resources/images/delete.svg | 22 + resources/images/exclamation.svg | 17 + resources/images/lock_closed.svg | 10 + resources/images/lock_closed_f.svg | 10 + resources/images/lock_open.svg | 11 + resources/images/lock_open_f.svg | 11 + resources/images/make_bold.svg | 4 + resources/images/make_italic.svg | 4 + resources/images/make_unbold.svg | 4 + resources/images/make_unitalic.svg | 4 + resources/images/open.svg | 2 +- resources/images/reflection_x.svg | 4 + resources/images/reflection_y.svg | 4 + resources/images/refresh.svg | 4 + resources/images/svg_modifier.svg | 4 + resources/images/svg_negative.svg | 4 + resources/images/svg_part.svg | 4 + resources/images/text_B.svg | 3 - resources/images/text_B_dark.svg | 3 - resources/images/text_T.svg | 4 - resources/images/text_T_dark.svg | 5 - src/imgui/CMakeLists.txt | 13 +- src/imgui/imgui_stdlib.cpp | 76 + src/imgui/imgui_stdlib.h | 22 + src/imgui/imgui_widgets.cpp | 5 +- src/libslic3r/AppConfig.cpp | 5 + src/libslic3r/AppConfig.hpp | 5 + src/libslic3r/BoundingBox.hpp | 7 + src/libslic3r/CMakeLists.txt | 20 +- src/libslic3r/ClipperUtils.cpp | 29 +- src/libslic3r/ClipperUtils.hpp | 12 +- src/libslic3r/Color.hpp | 1 + src/libslic3r/CutSurface.cpp | 4086 +++++++++++++++++ src/libslic3r/CutSurface.hpp | 78 + src/libslic3r/CutUtils.cpp | 4 +- src/libslic3r/Emboss.cpp | 2185 +++++++++ src/libslic3r/Emboss.hpp | 477 ++ src/libslic3r/EmbossShape.hpp | 143 + src/libslic3r/ExPolygon.cpp | 30 + src/libslic3r/ExPolygon.hpp | 46 +- src/libslic3r/ExPolygonSerialize.hpp | 28 + src/libslic3r/ExPolygonsIndex.cpp | 82 + src/libslic3r/ExPolygonsIndex.hpp | 74 + src/libslic3r/Format/SL1.cpp | 12 +- src/libslic3r/Format/bbs_3mf.cpp | 609 ++- src/libslic3r/Geometry.cpp | 350 +- src/libslic3r/Geometry.hpp | 130 +- src/libslic3r/IntersectionPoints.cpp | 49 + src/libslic3r/IntersectionPoints.hpp | 27 + src/libslic3r/Model.cpp | 177 +- src/libslic3r/Model.hpp | 120 +- src/libslic3r/NSVGUtils.cpp | 547 +++ src/libslic3r/NSVGUtils.hpp | 86 + src/libslic3r/Point.cpp | 137 +- src/libslic3r/Point.hpp | 75 +- src/libslic3r/Polygon.cpp | 43 + src/libslic3r/Polygon.hpp | 46 +- src/libslic3r/Polyline.cpp | 27 + src/libslic3r/Polyline.hpp | 4 + src/libslic3r/PrintApply.cpp | 4 +- src/libslic3r/SLA/AGGRaster.hpp | 33 +- src/libslic3r/SLA/RasterBase.cpp | 20 +- src/libslic3r/SLA/RasterBase.hpp | 68 +- src/libslic3r/SLAPrint.hpp | 1 + src/libslic3r/TextConfiguration.hpp | 191 + src/libslic3r/Timer.cpp | 25 + src/libslic3r/Timer.hpp | 96 + src/libslic3r/Triangulation.cpp | 332 ++ src/libslic3r/Triangulation.hpp | 76 + src/libslic3r/Utils.hpp | 1 + src/libslic3r/utils.cpp | 28 + src/slic3r/CMakeLists.txt | 42 +- src/slic3r/GUI/3DScene.cpp | 6 +- src/slic3r/GUI/3DScene.hpp | 16 +- src/slic3r/GUI/BindDialog.cpp | 13 +- src/slic3r/GUI/BindDialog.hpp | 3 +- .../GUI/CalibrationWizardPresetPage.cpp | 10 +- src/slic3r/GUI/CalibrationWizardSavePage.cpp | 10 +- src/slic3r/GUI/DownloadProgressDialog.cpp | 23 +- src/slic3r/GUI/DownloadProgressDialog.hpp | 5 +- src/slic3r/GUI/GCodeViewer.cpp | 2 +- src/slic3r/GUI/GLCanvas3D.cpp | 252 +- src/slic3r/GUI/GLCanvas3D.hpp | 23 + src/slic3r/GUI/GUI_Factories.cpp | 267 +- src/slic3r/GUI/GUI_Factories.hpp | 20 +- src/slic3r/GUI/GUI_ObjectList.cpp | 79 +- src/slic3r/GUI/GUI_ObjectList.hpp | 2 - src/slic3r/GUI/GUI_Preview.cpp | 7 + src/slic3r/GUI/Gizmos/GLGizmoBase.cpp | 13 +- src/slic3r/GUI/Gizmos/GLGizmoBase.hpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 8 +- src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp | 3664 +++++++++++++++ src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp | 240 + src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 4 +- src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp | 2 +- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 4 +- src/slic3r/GUI/Gizmos/GLGizmoMove.cpp | 10 +- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 22 +- src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp | 104 +- src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp | 38 +- src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp | 2239 +++++++++ src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp | 200 + src/slic3r/GUI/Gizmos/GLGizmoScale.cpp | 35 +- src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 49 +- src/slic3r/GUI/Gizmos/GLGizmosManager.hpp | 8 +- .../GUI/Gizmos/GizmoObjectManipulation.cpp | 115 +- .../GUI/Gizmos/GizmoObjectManipulation.hpp | 14 +- src/slic3r/GUI/IconManager.cpp | 410 ++ src/slic3r/GUI/IconManager.hpp | 134 + src/slic3r/GUI/ImGuiWrapper.cpp | 417 +- src/slic3r/GUI/ImGuiWrapper.hpp | 127 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 53 +- src/slic3r/GUI/Jobs/ArrangeJob.hpp | 38 +- src/slic3r/GUI/Jobs/BindJob.cpp | 61 +- src/slic3r/GUI/Jobs/BindJob.hpp | 16 +- src/slic3r/GUI/Jobs/BoostThreadWorker.cpp | 186 + src/slic3r/GUI/Jobs/BoostThreadWorker.hpp | 159 + src/slic3r/GUI/Jobs/BusyCursorJob.hpp | 57 + .../GUI/Jobs/CreateFontNameImageJob.cpp | 170 + .../GUI/Jobs/CreateFontNameImageJob.hpp | 85 + .../GUI/Jobs/CreateFontStyleImagesJob.cpp | 157 + .../GUI/Jobs/CreateFontStyleImagesJob.hpp | 40 + src/slic3r/GUI/Jobs/EmbossJob.cpp | 1568 +++++++ src/slic3r/GUI/Jobs/EmbossJob.hpp | 265 ++ src/slic3r/GUI/Jobs/FillBedJob.cpp | 33 +- src/slic3r/GUI/Jobs/FillBedJob.hpp | 24 +- src/slic3r/GUI/Jobs/Job.cpp | 167 - src/slic3r/GUI/Jobs/Job.hpp | 156 +- .../Jobs/NotificationProgressIndicator.cpp | 10 +- .../Jobs/NotificationProgressIndicator.hpp | 5 + src/slic3r/GUI/Jobs/OrientJob.cpp | 52 +- src/slic3r/GUI/Jobs/OrientJob.hpp | 21 +- src/slic3r/GUI/Jobs/PlaterJob.cpp | 17 - src/slic3r/GUI/Jobs/PlaterJob.hpp | 26 - src/slic3r/GUI/Jobs/PlaterWorker.hpp | 165 + src/slic3r/GUI/Jobs/PrintJob.cpp | 155 +- src/slic3r/GUI/Jobs/PrintJob.hpp | 22 +- src/slic3r/GUI/Jobs/ProgressIndicator.hpp | 4 + src/slic3r/GUI/Jobs/RotoptimizeJob.cpp | 34 +- src/slic3r/GUI/Jobs/RotoptimizeJob.hpp | 26 +- src/slic3r/GUI/Jobs/SLAImportDialog.hpp | 104 + src/slic3r/GUI/Jobs/SLAImportJob.cpp | 153 +- src/slic3r/GUI/Jobs/SLAImportJob.hpp | 34 +- src/slic3r/GUI/Jobs/SendJob.cpp | 132 +- src/slic3r/GUI/Jobs/SendJob.hpp | 21 +- src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp | 128 + src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp | 62 +- src/slic3r/GUI/Jobs/UpgradeNetworkJob.hpp | 11 +- src/slic3r/GUI/Jobs/Worker.hpp | 123 + src/slic3r/GUI/MainFrame.cpp | 13 +- src/slic3r/GUI/MediaPlayCtrl.cpp | 6 +- src/slic3r/GUI/MeshUtils.cpp | 2 +- src/slic3r/GUI/NotificationManager.cpp | 22 +- src/slic3r/GUI/NotificationManager.hpp | 10 + src/slic3r/GUI/ObjectDataViewModel.cpp | 117 +- src/slic3r/GUI/ObjectDataViewModel.hpp | 31 +- src/slic3r/GUI/Plater.cpp | 476 +- src/slic3r/GUI/Plater.hpp | 46 +- src/slic3r/GUI/ReleaseNote.cpp | 26 +- src/slic3r/GUI/ReleaseNote.hpp | 6 +- src/slic3r/GUI/SelectMachine.cpp | 27 +- src/slic3r/GUI/SelectMachine.hpp | 4 +- src/slic3r/GUI/Selection.cpp | 783 +++- src/slic3r/GUI/Selection.hpp | 111 +- src/slic3r/GUI/SendToPrinter.cpp | 31 +- src/slic3r/GUI/SendToPrinter.hpp | 2 +- src/slic3r/GUI/SurfaceDrag.cpp | 732 +++ src/slic3r/GUI/SurfaceDrag.hpp | 169 + src/slic3r/GUI/TextLines.cpp | 354 ++ src/slic3r/GUI/TextLines.hpp | 49 + src/slic3r/Utils/CalibUtils.cpp | 10 +- src/slic3r/Utils/CalibUtils.hpp | 5 +- src/slic3r/Utils/EmbossStyleManager.cpp | 788 ++++ src/slic3r/Utils/EmbossStyleManager.hpp | 325 ++ src/slic3r/Utils/FontConfigHelp.cpp | 144 + src/slic3r/Utils/FontConfigHelp.hpp | 29 + src/slic3r/Utils/RaycastManager.cpp | 386 ++ src/slic3r/Utils/RaycastManager.hpp | 184 + src/slic3r/Utils/WxFontUtils.cpp | 354 ++ src/slic3r/Utils/WxFontUtils.hpp | 75 + 197 files changed, 27190 insertions(+), 2454 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/align_horizontal_center.svg create mode 100644 resources/images/align_horizontal_left.svg create mode 100644 resources/images/align_horizontal_right.svg create mode 100644 resources/images/align_vertical_bottom.svg create mode 100644 resources/images/align_vertical_center.svg create mode 100644 resources/images/align_vertical_top.svg create mode 100644 resources/images/burn.svg create mode 100644 resources/images/delete.svg create mode 100644 resources/images/exclamation.svg create mode 100644 resources/images/lock_closed.svg create mode 100644 resources/images/lock_closed_f.svg create mode 100644 resources/images/lock_open.svg create mode 100644 resources/images/lock_open_f.svg create mode 100644 resources/images/make_bold.svg create mode 100644 resources/images/make_italic.svg create mode 100644 resources/images/make_unbold.svg create mode 100644 resources/images/make_unitalic.svg create mode 100644 resources/images/reflection_x.svg create mode 100644 resources/images/reflection_y.svg create mode 100644 resources/images/refresh.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 delete mode 100644 resources/images/text_B.svg delete mode 100644 resources/images/text_B_dark.svg delete mode 100644 resources/images/text_T.svg delete mode 100644 resources/images/text_T_dark.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/GLGizmoEmboss.cpp create mode 100644 src/slic3r/GUI/Gizmos/GLGizmoEmboss.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/CreateFontNameImageJob.cpp create mode 100644 src/slic3r/GUI/Jobs/CreateFontNameImageJob.hpp create mode 100644 src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp create mode 100644 src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp create mode 100644 src/slic3r/GUI/Jobs/EmbossJob.cpp create mode 100644 src/slic3r/GUI/Jobs/EmbossJob.hpp delete mode 100644 src/slic3r/GUI/Jobs/Job.cpp delete mode 100644 src/slic3r/GUI/Jobs/PlaterJob.cpp delete mode 100644 src/slic3r/GUI/Jobs/PlaterJob.hpp create mode 100644 src/slic3r/GUI/Jobs/PlaterWorker.hpp create mode 100644 src/slic3r/GUI/Jobs/SLAImportDialog.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/GUI/TextLines.cpp create mode 100644 src/slic3r/GUI/TextLines.hpp create mode 100644 src/slic3r/Utils/EmbossStyleManager.cpp create mode 100644 src/slic3r/Utils/EmbossStyleManager.hpp create mode 100644 src/slic3r/Utils/FontConfigHelp.cpp create mode 100644 src/slic3r/Utils/FontConfigHelp.hpp create mode 100644 src/slic3r/Utils/RaycastManager.cpp create mode 100644 src/slic3r/Utils/RaycastManager.hpp create mode 100644 src/slic3r/Utils/WxFontUtils.cpp create mode 100644 src/slic3r/Utils/WxFontUtils.hpp diff --git a/deps/CGAL/CGAL.cmake b/deps/CGAL/CGAL.cmake index 39514e465..18fee7c15 100644 --- a/deps/CGAL/CGAL.cmake +++ b/deps/CGAL/CGAL.cmake @@ -1,31 +1,11 @@ orcaslicer_add_cmake_project( CGAL - GIT_REPOSITORY https://github.com/CGAL/cgal.git - GIT_TAG caacd806dc55c61cc68adaad99f2240f00493b29 # releases/CGAL-5.3 + # GIT_REPOSITORY https://github.com/CGAL/cgal.git + # GIT_TAG bec70a6d52d8aacb0b3d82a7b4edc3caa899184b # releases/CGAL-5.0 # For whatever reason, this keeps downloading forever (repeats downloads if finished) - #URL https://github.com/CGAL/cgal/archive/releases/CGAL-5.0.zip - #URL_HASH SHA256=c2b035bd078687b6d8c0fb6371a7443adcdb647856af9969532c4050cd5f48e5 + URL https://github.com/CGAL/cgal/archive/refs/tags/v5.4.zip + URL_HASH SHA256=d7605e0a5a5ca17da7547592f6f6e4a59430a0bc861948974254d0de43eab4c0 DEPENDS dep_Boost dep_GMP dep_MPFR ) include(GNUInstallDirs) - -# CGAL, for whatever reason, makes itself non-relocatable by writing the build directory into -# CGALConfig-installation-dirs.cmake and including it in configure time. -# If this file is not present, it will not consider the stored absolute path -ExternalProject_Add_Step(dep_CGAL dep_CGAL_relocation_fix - DEPENDEES install - - COMMAND ${CMAKE_COMMAND} -E remove CGALConfig-installation-dirs.cmake - WORKING_DIRECTORY "${DESTDIR}/usr/local/${CMAKE_INSTALL_LIBDIR}/cmake/CGAL" -) - -# Again, for whatever reason, CGAL thinks that its version is not relevant if -# configured as a header only library. Fixing it by placing a cmake version file -# besides the installed config file. -ExternalProject_Add_Step(dep_CGAL dep_CGAL_version_fix - DEPENDEES install - - COMMAND ${CMAKE_COMMAND} -E copy cgal/CGALConfigVersion.cmake "${DESTDIR}/usr/local/${CMAKE_INSTALL_LIBDIR}/cmake/CGAL/CGALConfigVersion.cmake" - WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" -) diff --git a/deps/Cereal/Cereal.cmake b/deps/Cereal/Cereal.cmake index 6f4c9a1d9..7f87121f5 100644 --- a/deps/Cereal/Cereal.cmake +++ b/deps/Cereal/Cereal.cmake @@ -1,6 +1,12 @@ +#/|/ Copyright (c) Prusa Research 2021 - 2022 Tomáš Mészáros @tamasmeszaros, Filip Sykala @Jony01 +#/|/ +#/|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +#/|/ orcaslicer_add_cmake_project(Cereal - URL "https://github.com/USCiLab/cereal/archive/v1.2.2.tar.gz" - URL_HASH SHA256=1921f26d2e1daf9132da3c432e2fd02093ecaedf846e65d7679ddf868c7289c4 + URL "https://github.com/USCiLab/cereal/archive/refs/tags/v1.3.0.zip" + URL_HASH SHA256=71642cb54658e98c8f07a0f0d08bf9766f1c3771496936f6014169d3726d9657 CMAKE_ARGS - -DJUST_INSTALL_CEREAL=on + -DJUST_INSTALL_CEREAL=ON + -DSKIP_PERFORMANCE_COMPARISON=ON + -DBUILD_TESTS=OFF ) \ No newline at end of file diff --git a/resources/images/add_copies.svg b/resources/images/add_copies.svg index 45b1d27cf..7eb75471e 100644 --- a/resources/images/add_copies.svg +++ b/resources/images/add_copies.svg @@ -8,11 +8,11 @@ S11.87,1,8,1L8,1z"/> - - diff --git a/resources/images/add_text_modifier.svg b/resources/images/add_text_modifier.svg new file mode 100644 index 000000000..d79499eff --- /dev/null +++ b/resources/images/add_text_modifier.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/add_text_negative.svg b/resources/images/add_text_negative.svg new file mode 100644 index 000000000..2cf445692 --- /dev/null +++ b/resources/images/add_text_negative.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/add_text_part.svg b/resources/images/add_text_part.svg new file mode 100644 index 000000000..c4972e6cb --- /dev/null +++ b/resources/images/add_text_part.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_horizontal_center.svg b/resources/images/align_horizontal_center.svg new file mode 100644 index 000000000..a91ab70ac --- /dev/null +++ b/resources/images/align_horizontal_center.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/images/align_horizontal_left.svg b/resources/images/align_horizontal_left.svg new file mode 100644 index 000000000..fc72e75cf --- /dev/null +++ b/resources/images/align_horizontal_left.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/images/align_horizontal_right.svg b/resources/images/align_horizontal_right.svg new file mode 100644 index 000000000..2e83ee635 --- /dev/null +++ b/resources/images/align_horizontal_right.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/images/align_vertical_bottom.svg b/resources/images/align_vertical_bottom.svg new file mode 100644 index 000000000..9f65196ed --- /dev/null +++ b/resources/images/align_vertical_bottom.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/resources/images/align_vertical_center.svg b/resources/images/align_vertical_center.svg new file mode 100644 index 000000000..348c08a98 --- /dev/null +++ b/resources/images/align_vertical_center.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/resources/images/align_vertical_top.svg b/resources/images/align_vertical_top.svg new file mode 100644 index 000000000..0bcb4b9e0 --- /dev/null +++ b/resources/images/align_vertical_top.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + diff --git a/resources/images/burn.svg b/resources/images/burn.svg new file mode 100644 index 000000000..685999cd1 --- /dev/null +++ b/resources/images/burn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/delete.svg b/resources/images/delete.svg new file mode 100644 index 000000000..91d56e91d --- /dev/null +++ b/resources/images/delete.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/images/exclamation.svg b/resources/images/exclamation.svg new file mode 100644 index 000000000..5a5b631a4 --- /dev/null +++ b/resources/images/exclamation.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/resources/images/lock_closed.svg b/resources/images/lock_closed.svg new file mode 100644 index 000000000..549cdba23 --- /dev/null +++ b/resources/images/lock_closed.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/resources/images/lock_closed_f.svg b/resources/images/lock_closed_f.svg new file mode 100644 index 000000000..2920ea0aa --- /dev/null +++ b/resources/images/lock_closed_f.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/resources/images/lock_open.svg b/resources/images/lock_open.svg new file mode 100644 index 000000000..3f0da9ae0 --- /dev/null +++ b/resources/images/lock_open.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/resources/images/lock_open_f.svg b/resources/images/lock_open_f.svg new file mode 100644 index 000000000..3d12d7874 --- /dev/null +++ b/resources/images/lock_open_f.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/resources/images/make_bold.svg b/resources/images/make_bold.svg new file mode 100644 index 000000000..1c39a3d23 --- /dev/null +++ b/resources/images/make_bold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/make_italic.svg b/resources/images/make_italic.svg new file mode 100644 index 000000000..5c128e630 --- /dev/null +++ b/resources/images/make_italic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/make_unbold.svg b/resources/images/make_unbold.svg new file mode 100644 index 000000000..7a99cc227 --- /dev/null +++ b/resources/images/make_unbold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/make_unitalic.svg b/resources/images/make_unitalic.svg new file mode 100644 index 000000000..f3c4d9068 --- /dev/null +++ b/resources/images/make_unitalic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/open.svg b/resources/images/open.svg index 8fcddc2a6..5c7fa81be 100644 --- a/resources/images/open.svg +++ b/resources/images/open.svg @@ -5,7 +5,7 @@ - diff --git a/resources/images/reflection_x.svg b/resources/images/reflection_x.svg new file mode 100644 index 000000000..fa391e6cd --- /dev/null +++ b/resources/images/reflection_x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/reflection_y.svg b/resources/images/reflection_y.svg new file mode 100644 index 000000000..0de6a5971 --- /dev/null +++ b/resources/images/reflection_y.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/refresh.svg b/resources/images/refresh.svg new file mode 100644 index 000000000..af9b33685 --- /dev/null +++ b/resources/images/refresh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/svg_modifier.svg b/resources/images/svg_modifier.svg new file mode 100644 index 000000000..8b1ff317b --- /dev/null +++ b/resources/images/svg_modifier.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/svg_negative.svg b/resources/images/svg_negative.svg new file mode 100644 index 000000000..c47a8fe58 --- /dev/null +++ b/resources/images/svg_negative.svg @@ -0,0 +1,4 @@ + + + + \ 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..40f907bd9 --- /dev/null +++ b/resources/images/svg_part.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/images/text_B.svg b/resources/images/text_B.svg deleted file mode 100644 index c792b4be4..000000000 --- a/resources/images/text_B.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/images/text_B_dark.svg b/resources/images/text_B_dark.svg deleted file mode 100644 index 5fa0f1bbd..000000000 --- a/resources/images/text_B_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/images/text_T.svg b/resources/images/text_T.svg deleted file mode 100644 index fa5cb783b..000000000 --- a/resources/images/text_T.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/resources/images/text_T_dark.svg b/resources/images/text_T_dark.svg deleted file mode 100644 index af85448da..000000000 --- a/resources/images/text_T_dark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/imgui/CMakeLists.txt b/src/imgui/CMakeLists.txt index 2db32b306..da31d7695 100644 --- a/src/imgui/CMakeLists.txt +++ b/src/imgui/CMakeLists.txt @@ -3,16 +3,19 @@ project(imgui) add_library(imgui STATIC imconfig.h + imgui.cpp imgui.h + imgui_demo.cpp + imgui_draw.cpp imgui_internal.h + imgui_stdlib.cpp + imgui_stdlib.h + imgui_tables.cpp + imgui_widgets.cpp + # imgui STB imstb_rectpack.h imstb_textedit.h imstb_truetype.h - imgui_tables.cpp - imgui.cpp - imgui_demo.cpp - imgui_draw.cpp - imgui_widgets.cpp ) if(Boost_FOUND) 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 b09b09abc..ded72f417 100644 --- a/src/imgui/imgui_widgets.cpp +++ b/src/imgui/imgui_widgets.cpp @@ -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/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 665fb0771..ba0d363f7 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2017 - 2023 Oleksandra Iushchenko @YuSanka, Vojtěch Bubník @bubnikv, Pavel Mikuš @Godrak, David Kocík @kocikdav, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Lukáš Hejl @hejllukas, Filip Sykala @Jony01, Vojtěch Král @vojtechkral +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "libslic3r/libslic3r.h" #include "libslic3r/Utils.hpp" #include "AppConfig.hpp" @@ -43,6 +47,7 @@ static const std::string MODELS_STR = "models"; const std::string AppConfig::SECTION_FILAMENTS = "filaments"; const std::string AppConfig::SECTION_MATERIALS = "sla_materials"; +const std::string AppConfig::SECTION_EMBOSS_STYLE = "font"; std::string AppConfig::get_language_code() { diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index dfcc24658..1219adec9 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2017 - 2023 Vojtěch Bubník @bubnikv, David Kocík @kocikdav, Lukáš Matěna @lukasmatena, Filip Sykala @Jony01, Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka, Vojtěch Král @vojtechkral +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_AppConfig_hpp_ #define slic3r_AppConfig_hpp_ @@ -287,6 +291,7 @@ public: static const std::string SECTION_FILAMENTS; static const std::string SECTION_MATERIALS; + static const std::string SECTION_EMBOSS_STYLE; private: template diff --git a/src/libslic3r/BoundingBox.hpp b/src/libslic3r/BoundingBox.hpp index a7d092ae2..8654669eb 100644 --- a/src/libslic3r/BoundingBox.hpp +++ b/src/libslic3r/BoundingBox.hpp @@ -1,3 +1,8 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Tomáš Mészáros @tamasmeszaros, Vojtěch Bubník @bubnikv, Filip Sykala @Jony01, Enrico Turri @enricoturri1966 +///|/ Copyright (c) Slic3r 2014 - 2015 Alessandro Ranellucci @alranel +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_BoundingBox_hpp_ #define slic3r_BoundingBox_hpp_ @@ -222,6 +227,8 @@ public: friend BoundingBox get_extents_rotated(const Points &points, double angle); }; +using BoundingBoxes = std::vector; + class BoundingBox3 : public BoundingBox3Base { public: diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index d5a020e05..37cb8ea0d 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -66,9 +66,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 MultiPoint.cpp MultiPoint.hpp MutablePriorityQueue.hpp + NSVGUtils.cpp + NSVGUtils.hpp ObjectID.cpp ObjectID.hpp PerimeterGenerator.cpp @@ -318,6 +326,7 @@ set(lisbslic3r_sources Technologies.hpp Tesselate.cpp Tesselate.hpp + TextConfiguration.hpp TriangleMesh.cpp TriangleMesh.hpp TriangleMeshSlicer.cpp @@ -329,6 +338,8 @@ set(lisbslic3r_sources Utils.hpp Time.cpp Time.hpp + Timer.cpp + Timer.hpp Thread.cpp Thread.hpp TriangleSelector.cpp @@ -457,8 +468,13 @@ cmake_policy(SET CMP0011 NEW) find_package(CGAL REQUIRED) 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 ec16193fd..eeb43f6f0 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -272,8 +272,8 @@ bool has_duplicate_points(const ClipperLib::PolyTree &polytree) // Offset CCW contours outside, CW contours (holes) inside. // Don't calculate union of the output paths. -template -static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) +template +static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType endType = ClipperLib::etClosedPolygon) { ClipperLib::ClipperOffset co; ClipperLib::Paths out; @@ -354,16 +354,16 @@ TResult clipper_union( // Perform union of input polygons using the positive rule, convert to ExPolygons. //FIXME is there any benefit of not doing the boolean / using pftEvenOdd? -ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input, bool do_union) +inline ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input, bool do_union) { return PolyTreeToExPolygons(clipper_union(input, do_union ? ClipperLib::pftNonZero : ClipperLib::pftEvenOdd)); } -template -static ClipperLib::Paths raw_offset_polyline(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) +template +static ClipperLib::Paths raw_offset_polyline(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type = ClipperLib::etOpenButt) { assert(offset > 0); - return raw_offset(std::forward(paths), offset, joinType, miterLimit); + return raw_offset(std::forward(paths), offset, joinType, miterLimit, end_type); } template @@ -418,10 +418,17 @@ Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, Cli Slic3r::ExPolygons offset_ex(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType, double miterLimit) { return PolyTreeToExPolygons(offset_paths(ClipperUtils::PolygonsProvider(polygons), delta, joinType, miterLimit)); } -Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::SinglePathProvider(polyline.points), delta, joinType, miterLimit))); } -Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType, double miterLimit) - { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::PolylinesProvider(polylines), delta, joinType, miterLimit))); } +Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::EndType end_type) + { assert(delta > 0); return to_polygons(clipper_union(raw_offset_polyline(ClipperUtils::SinglePathProvider(polyline.points), delta, joinType, miterLimit, end_type))); } +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))); } + +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) @@ -795,6 +802,8 @@ 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::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 diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 1428b6bd0..3607b0a69 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -22,6 +22,9 @@ namespace Slic3r { static constexpr const float ClipperSafetyOffset = 10.f; static constexpr const Slic3r::ClipperLib::JoinType DefaultJoinType = Slic3r::ClipperLib::jtMiter; + +static constexpr const Slic3r::ClipperLib::EndType DefaultEndType = Slic3r::ClipperLib::etOpenButt; + //FIXME evaluate the default miter limit. 3 seems to be extreme, Cura uses 1.2. // Mitter Limit 3 is useful for perimeter generator, where sharp corners are extruded without needing a gap fill. // However such a high limit causes issues with large positive or negative offsets, where a sharp corner @@ -335,8 +338,8 @@ Slic3r::Polygons offset(const Slic3r::Polygon &polygon, const float delta, Clipp // offset Polylines // Wherever applicable, please use the expand() / shrink() 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. -Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit); -Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit); +Slic3r::Polygons offset(const Slic3r::Polyline &polyline, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); +Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, ClipperLib::JoinType joinType = DefaultLineJoinType, double miterLimit = DefaultLineMiterLimit, ClipperLib::EndType end_type = DefaultEndType); Slic3r::Polygons offset(const Slic3r::Polygons &polygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::Polygons offset(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); Slic3r::Polygons offset(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit); @@ -355,6 +358,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); } @@ -539,6 +546,7 @@ Slic3r::Polygons union_(const Slic3r::Polygons &subject, const Slic3r::Polygons // 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); diff --git a/src/libslic3r/Color.hpp b/src/libslic3r/Color.hpp index ea17328be..a11174639 100644 --- a/src/libslic3r/Color.hpp +++ b/src/libslic3r/Color.hpp @@ -61,6 +61,7 @@ public: static const ColorRGB REDISH() { return { 1.0f, 0.5f, 0.5f }; } static const ColorRGB YELLOW() { return { 1.0f, 1.0f, 0.0f }; } static const ColorRGB WHITE() { return { 1.0f, 1.0f, 1.0f }; } + static const ColorRGB ORCA() { return {0.0f, 150.f / 255.0f, 136.0f / 255}; } static const ColorRGB X() { return { 0.75f, 0.0f, 0.0f }; } static const ColorRGB Y() { return { 0.0f, 0.75f, 0.0f }; } diff --git a/src/libslic3r/CutSurface.cpp b/src/libslic3r/CutSurface.cpp new file mode 100644 index 000000000..b2cf3d965 --- /dev/null +++ b/src/libslic3r/CutSurface.cpp @@ -0,0 +1,4086 @@ +///|/ Copyright (c) Prusa Research 2022 - 2023 Vojtěch Bubník @bubnikv, Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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 + +// 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 Vec3i &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 Vec3i &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]; + Vec2i 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 + Vec3i 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; + Vec3i 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(Vec3i(ai, ai2, bi)); + its.indices.push_back(Vec3i(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(Vec3i(ai, bi, ai2)); + result.indices.push_back(Vec3i(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..f5b41cdae --- /dev/null +++ b/src/libslic3r/CutSurface.hpp @@ -0,0 +1,78 @@ +///|/ Copyright (c) Prusa Research 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index f847c7ad7..9f4dbdaac 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -217,7 +217,7 @@ static void reset_instance_transformation(ModelObject* object, size_t src_instan auto& obj_instance = object->instances[i]; const double rot_z = obj_instance->get_rotation().z(); - Transformation inst_trafo = Transformation(obj_instance->get_transformation().get_matrix(false, false, true)); + Transformation inst_trafo = Transformation(obj_instance->get_transformation().get_matrix_no_scaling_factor()); // add respect to mirroring if (obj_instance->is_left_handed()) inst_trafo = inst_trafo * Transformation(scale_transform(Vec3d(-1, 1, 1))); @@ -320,7 +320,7 @@ const ModelObjectPtrs& Cut::perform_with_plane() // except for translation and Z-rotation on instances, which are preserved // in the transformation matrix and not applied to the mesh transform. - const auto instance_matrix = mo->instances[m_instance]->get_transformation().get_matrix(true); + const auto instance_matrix = mo->instances[m_instance]->get_transformation().get_matrix_no_offset(); const Transformation cut_transformation = Transformation(m_cut_matrix); const Transform3d inverse_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * translation_transform(-1. * cut_transformation.get_offset()); diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp new file mode 100644 index 000000000..6af848f46 --- /dev/null +++ b/src/libslic3r/Emboss.cpp @@ -0,0 +1,2185 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Lukáš Matěna @lukasmatena, Oleksandra Iushchenko @YuSanka, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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 Vec3i &t : shape_triangles) + result.indices.emplace_back(t.x(), t.z(), t.y()); + // bottom triangles - use CW + for (const Vec3i &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 Vec3i &t : shape_triangles) + result.indices.emplace_back(t.x(), t.z(), t.y()); + // bottom triangles - use CW + for (const Vec3i &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..ded2196a7 --- /dev/null +++ b/src/libslic3r/Emboss.hpp @@ -0,0 +1,477 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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..9094d2ad5 --- /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 + assert(file_data != 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 77222dc36..dee115194 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -1,3 +1,15 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Lukáš Hejl @hejllukas +///|/ Copyright (c) Slic3r 2013 - 2016 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2015 Maksim Derbasov @ntfshard +///|/ Copyright (c) 2014 Petr Ledvina @ledvinap +///|/ +///|/ ported from lib/Slic3r/ExPolygon.pm: +///|/ Copyright (c) Prusa Research 2017 - 2022 Vojtěch Bubník @bubnikv +///|/ Copyright (c) Slic3r 2011 - 2014 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2012 Mark Hindess +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "BoundingBox.hpp" #include "ExPolygon.hpp" #include "Exception.hpp" @@ -475,6 +487,24 @@ bool has_duplicate_points(const ExPolygons &expolys) #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); diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index c8f5a5f45..39974ab7a 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -1,3 +1,14 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Pavel Mikuš @Godrak, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Filip Sykala @Jony01, Lukáš Hejl @hejllukas, Tomáš Mészáros @tamasmeszaros +///|/ Copyright (c) 2016 Sakari Kapanen @Flannelhead +///|/ Copyright (c) Slic3r 2013 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ ported from lib/Slic3r/ExPolygon.pm: +///|/ Copyright (c) Prusa Research 2017 - 2022 Vojtěch Bubník @bubnikv +///|/ Copyright (c) Slic3r 2011 - 2014 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2012 Mark Hindess +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_ExPolygon_hpp_ #define slic3r_ExPolygon_hpp_ @@ -142,19 +153,6 @@ inline Lines to_lines(const ExPolygons &src) return lines; } -inline Points to_points(const ExPolygons& src) -{ - Points points; - size_t count = count_points(src); - points.reserve(count); - for (const ExPolygon& expolygon : src) { - append(points, expolygon.contour.points); - for (const Polygon& hole : expolygon.holes) - append(points, hole.points); - } - return points; -} - // Line is from point index(see to_points) to next point. // Next point of last point in polygon is first polygon point. inline Linesf to_linesf(const ExPolygons &src, uint32_t count_lines = 0) @@ -205,6 +203,20 @@ inline Linesf to_unscaled_linesf(const ExPolygons &src) return lines; } + +inline Points to_points(const ExPolygons &src) +{ + Points points; + size_t count = count_points(src); + points.reserve(count); + for (const ExPolygon &expolygon : src) { + append(points, expolygon.contour.points); + for (const Polygon &hole : expolygon.holes) + append(points, hole.points); + } + return points; +} + inline Polylines to_polylines(const ExPolygon &src) { Polylines polylines; @@ -371,6 +383,11 @@ inline Points to_points(const ExPolygon &expoly) return out; } +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); @@ -464,6 +481,9 @@ std::vector get_extents_vector(const ExPolygons &polygons); 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); diff --git a/src/libslic3r/ExPolygonSerialize.hpp b/src/libslic3r/ExPolygonSerialize.hpp new file mode 100644 index 000000000..712d4706d --- /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..976531799 --- /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..b46fd5089 --- /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/SL1.cpp b/src/libslic3r/Format/SL1.cpp index 9604a43dd..91aa7bbfc 100644 --- a/src/libslic3r/Format/SL1.cpp +++ b/src/libslic3r/Format/SL1.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "SL1.hpp" #include "GCode/ThumbnailData.hpp" #include "libslic3r/Time.hpp" @@ -442,8 +446,8 @@ void fill_slicerconf(ConfMap &m, const SLAPrint &print) std::unique_ptr SL1Archive::create_raster() const { - sla::RasterBase::Resolution res; - sla::RasterBase::PixelDim pxdim; + sla::Resolution res; + sla::PixelDim pxdim; std::array mirror; double w = m_cfg.display_width.getFloat(); @@ -464,8 +468,8 @@ std::unique_ptr SL1Archive::create_raster() const std::swap(pw, ph); } - res = sla::RasterBase::Resolution{pw, ph}; - pxdim = sla::RasterBase::PixelDim{w / pw, h / ph}; + res = sla::Resolution{pw, ph}; + pxdim = sla::PixelDim{w / pw, h / ph}; sla::RasterBase::Trafo tr{orientation, mirror}; double gamma = m_cfg.gamma_correction.getFloat(); diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 3e35e5e07..17bb4972d 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -23,6 +23,9 @@ #include #include +#include +#include + #include #include #include @@ -48,6 +51,13 @@ namespace pt = boost::property_tree; #include #include "miniz_extension.hpp" #include "nlohmann/json.hpp" + +#include "TextConfiguration.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, @@ -211,7 +221,7 @@ static constexpr const char* ASSEMBLE_ITEM_TAG = "assemble_item"; static constexpr const char* SLICE_HEADER_TAG = "header"; static constexpr const char* SLICE_HEADER_ITEM_TAG = "header_item"; -// text_info +// Deprecated: text_info static constexpr const char* TEXT_INFO_TAG = "text_info"; static constexpr const char* TEXT_ATTR = "text"; static constexpr const char* FONT_NAME_ATTR = "font_name"; @@ -325,6 +335,42 @@ 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_TAG = "slic3rpe:text"; +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[] = @@ -718,8 +764,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) MetadataList metadata; RepairedMeshErrors mesh_stats; ModelVolumeType part_type; - TextInfo text_info; - + std::optional text_configuration; + 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) @@ -770,6 +816,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) typedef std::map IdToCutObjectInfoMap; /*typedef std::map> IdToSlaSupportPointsMap; typedef std::map> IdToSlaDrainHolesMap;*/ + using PathToEmbossShapeFileMap = std::map>; struct ObjectImporter { @@ -962,6 +1009,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; @@ -1015,6 +1063,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); @@ -1035,6 +1084,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); @@ -1090,6 +1140,9 @@ 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_text_configuration(const char** attributes, unsigned int num_attributes); + 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); @@ -1141,7 +1194,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) void _generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects); bool _generate_volumes_new(ModelObject& object, const std::vector &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); - bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); + //bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); // callbacks to parse the .model file static void XMLCALL _handle_start_model_xml_element(void* userData, const char* name, const char** attributes); @@ -1757,6 +1810,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return false; } } + else if (_is_svg_shape_file(name)) { + _extract_embossed_svg_shape_file(name, archive, stat); + } else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) { m_parsing_slice_info = true; //extract slice info from archive @@ -2156,6 +2212,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); @@ -2870,6 +2930,32 @@ 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 @@ -3066,6 +3152,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_start_config_volume_mesh(attributes, num_attributes); else if (::strcmp(METADATA_TAG, name) == 0) res = _handle_start_config_metadata(attributes, num_attributes); + else if (::strcmp(SHAPE_TAG, name) == 0) + res = _handle_start_shape_configuration(attributes, num_attributes); + else if (::strcmp(TEXT_TAG, name) == 0) + res = _handle_start_text_configuration(attributes, num_attributes); else if (::strcmp(PLATE_TAG, name) == 0) res = _handle_start_config_plater(attributes, num_attributes); else if (::strcmp(INSTANCE_TAG, name) == 0) @@ -3632,6 +3722,104 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + struct TextConfigurationSerialization + { + public: + TextConfigurationSerialization() = delete; + + using TypeToName = boost::bimap; + static const TypeToName type_to_name; + + using HorizontalAlignToName = boost::bimap; + static const HorizontalAlignToName horizontal_align_to_name; + + using VerticalAlignToName = boost::bimap; + static const VerticalAlignToName vertical_align_to_name; + + static EmbossStyle::Type get_type(std::string_view type) { + const auto& to_type = TextConfigurationSerialization::type_to_name.right; + auto type_item = to_type.find(type); + assert(type_item != to_type.end()); + if (type_item == to_type.end()) return EmbossStyle::Type::undefined; + return type_item->second; + } + + static std::string_view get_name(EmbossStyle::Type type) { + const auto& to_name = TextConfigurationSerialization::type_to_name.left; + auto type_name = to_name.find(type); + assert(type_name != to_name.end()); + if (type_name == to_name.end()) return "unknown type"; + return type_name->second; + } + + static void to_xml(std::stringstream &stream, const TextConfiguration &tc); + static std::optional read(const char **attributes, unsigned int num_attributes); + static EmbossShape read_old(const char **attributes, unsigned int num_attributes); + }; + + bool _BBS_3MF_Importer::_handle_start_text_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; + } + if (object->second.volumes.empty()) { + add_error("Can not assign mesh to a valid volume"); + return false; + } + ObjectMetadata::VolumeMetadata& volume = object->second.volumes.back(); + volume.text_configuration = TextConfigurationSerialization::read(attributes, num_attributes); + if (!volume.text_configuration.has_value()) + return false; + + // Is 3mf version with shapes? + if (volume.shape_configuration.has_value()) + return true; + + // Back compatibility for 3mf version without shapes + volume.shape_configuration = TextConfigurationSerialization::read_old(attributes, num_attributes); + 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; @@ -4169,6 +4357,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) ObjectMetadata::VolumeMetadata &volume = object->second.volumes[m_curr_config.volume_id]; + if (volume.text_configuration.has_value()) { + add_error("Both text_info and text_configuration found, ignore legacy text_info"); + return true; + } + + // TODO: Orca: support legacy text info + /* 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); @@ -4196,7 +4391,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (!hit_normal.empty()) text_info.m_rr.normal = get_vec3_from_string(hit_normal); - volume.text_info = text_info; + volume.text_info = text_info;*/ return true; } @@ -4473,9 +4668,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } volume->set_type(volume_data->part_type); - - if (!volume_data->text_info.m_text.empty()) - volume->set_text_info(volume_data->text_info); + + if (auto &es = volume_data->shape_configuration; es.has_value()) + volume->emboss_shape = std::move(es); + if (auto &tc = volume_data->text_configuration; tc.has_value()) + volume->text_configuration = std::move(tc); // apply the remaining volume's metadata for (const Metadata& metadata : volume_data->metadata) { @@ -4519,7 +4716,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } - + /* bool _BBS_3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions) { if (!object.volumes.empty()) { @@ -4667,7 +4864,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } - + */ void XMLCALL _BBS_3MF_Importer::_handle_start_model_xml_element(void* userData, const char* name, const char** attributes) { _BBS_3MF_Importer* importer = (_BBS_3MF_Importer*)userData; @@ -5242,6 +5439,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 @@ -6687,6 +6885,16 @@ 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 @@ -6707,13 +6915,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"; } @@ -7041,38 +7243,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } - void _add_text_info_to_archive(std::stringstream& stream, const TextInfo& text_info) { - stream << " <" << TEXT_INFO_TAG << " "; - - stream << TEXT_ATTR << "=\"" << xml_escape(text_info.m_text) << "\" "; - stream << FONT_NAME_ATTR << "=\"" << text_info.m_font_name << "\" "; - - stream << FONT_INDEX_ATTR << "=\"" << text_info.m_curr_font_idx << "\" "; - - stream << FONT_SIZE_ATTR << "=\"" << text_info.m_font_size << "\" "; - stream << THICKNESS_ATTR << "=\"" << text_info.m_thickness << "\" "; - stream << EMBEDED_DEPTH_ATTR << "=\"" << text_info.m_embeded_depth << "\" "; - stream << ROTATE_ANGLE_ATTR << "=\"" << text_info.m_rotate_angle << "\" "; - stream << TEXT_GAP_ATTR << "=\"" << text_info.m_text_gap << "\" "; - - stream << BOLD_ATTR << "=\"" << (text_info.m_bold ? 1 : 0) << "\" "; - stream << ITALIC_ATTR << "=\"" << (text_info.m_italic ? 1 : 0) << "\" "; - stream << SURFACE_TEXT_ATTR << "=\"" << (text_info.m_is_surface_text ? 1 : 0) << "\" "; - stream << KEEP_HORIZONTAL_ATTR << "=\"" << (text_info.m_keep_horizontal ? 1 : 0) << "\" "; - - stream << HIT_MESH_ATTR << "=\"" << text_info.m_rr.mesh_id << "\" "; - - stream << HIT_POSITION_ATTR << "=\""; - add_vec3(stream, text_info.m_rr.hit); - stream << "\" "; - - stream << HIT_NORMAL_ATTR << "=\""; - add_vec3(stream, text_info.m_rr.normal); - stream << "\" "; - - stream << "/>\n"; - } - bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, int export_plate_idx, bool save_gcode, bool use_loaded_id) { std::stringstream stream; @@ -7170,9 +7340,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " <" << METADATA_TAG << " "<< KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; } - const TextInfo &text_info = volume->get_text_info(); - if (!text_info.m_text.empty()) - _add_text_info_to_archive(stream, text_info); + if (const std::optional &es = volume->emboss_shape; + es.has_value()) + to_xml(stream, *es, *volume, archive); + + if (const std::optional &tc = volume->text_configuration; + tc.has_value()) + TextConfigurationSerialization::to_xml(stream, *tc); // stores mesh's statistics const RepairedMeshErrors& stats = volume->mesh().stats().repaired_errors; @@ -8163,4 +8337,335 @@ 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 + +/// +/// TextConfiguration serialization +/// +const TextConfigurationSerialization::TypeToName TextConfigurationSerialization::type_to_name = + boost::assign::list_of + (EmbossStyle::Type::file_path, "file_name") + (EmbossStyle::Type::wx_win_font_descr, "wxFontDescriptor_Windows") + (EmbossStyle::Type::wx_lin_font_descr, "wxFontDescriptor_Linux") + (EmbossStyle::Type::wx_mac_font_descr, "wxFontDescriptor_MacOsX"); + +const TextConfigurationSerialization::HorizontalAlignToName TextConfigurationSerialization::horizontal_align_to_name = + boost::assign::list_of + (FontProp::HorizontalAlign::left, "left") + (FontProp::HorizontalAlign::center, "center") + (FontProp::HorizontalAlign::right, "right"); + +const TextConfigurationSerialization::VerticalAlignToName TextConfigurationSerialization::vertical_align_to_name = + boost::assign::list_of + (FontProp::VerticalAlign::top, "top") + (FontProp::VerticalAlign::center, "middle") + (FontProp::VerticalAlign::bottom, "bottom"); + + +void TextConfigurationSerialization::to_xml(std::stringstream &stream, const TextConfiguration &tc) +{ + stream << " <" << TEXT_TAG << " "; + + stream << TEXT_DATA_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(tc.text) << "\" "; + // font item + const EmbossStyle &style = tc.style; + stream << STYLE_NAME_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(style.name) << "\" "; + stream << FONT_DESCRIPTOR_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(style.path) << "\" "; + constexpr std::string_view dafault_type{"undefined"}; + std::string_view style_type = bimap_cvt(type_to_name, style.type, dafault_type); + stream << FONT_DESCRIPTOR_TYPE_ATTR << "=\"" << style_type << "\" "; + + // font property + const FontProp &fp = tc.style.prop; + if (fp.char_gap.has_value()) + stream << CHAR_GAP_ATTR << "=\"" << *fp.char_gap << "\" "; + if (fp.line_gap.has_value()) + stream << LINE_GAP_ATTR << "=\"" << *fp.line_gap << "\" "; + + stream << LINE_HEIGHT_ATTR << "=\"" << fp.size_in_mm << "\" "; + if (fp.boldness.has_value()) + stream << BOLDNESS_ATTR << "=\"" << *fp.boldness << "\" "; + if (fp.skew.has_value()) + stream << SKEW_ATTR << "=\"" << *fp.skew << "\" "; + if (fp.per_glyph) + stream << PER_GLYPH_ATTR << "=\"" << 1 << "\" "; + stream << HORIZONTAL_ALIGN_ATTR << "=\"" << bimap_cvt(horizontal_align_to_name, fp.align.first, dafault_type) << "\" "; + stream << VERTICAL_ALIGN_ATTR << "=\"" << bimap_cvt(vertical_align_to_name, fp.align.second, dafault_type) << "\" "; + if (fp.collection_number.has_value()) + stream << COLLECTION_NUMBER_ATTR << "=\"" << *fp.collection_number << "\" "; + // font descriptor + if (fp.family.has_value()) + stream << FONT_FAMILY_ATTR << "=\"" << *fp.family << "\" "; + if (fp.face_name.has_value()) + stream << FONT_FACE_NAME_ATTR << "=\"" << *fp.face_name << "\" "; + if (fp.style.has_value()) + stream << FONT_STYLE_ATTR << "=\"" << *fp.style << "\" "; + if (fp.weight.has_value()) + stream << FONT_WEIGHT_ATTR << "=\"" << *fp.weight << "\" "; + + stream << "/>\n"; // end TEXT_TAG +} +namespace { + +FontProp::HorizontalAlign read_horizontal_align(const char **attributes, unsigned int num_attributes, const TextConfigurationSerialization::HorizontalAlignToName& horizontal_align_to_name){ + std::string horizontal_align_str = bbs_get_attribute_value_string(attributes, num_attributes, HORIZONTAL_ALIGN_ATTR); + + // Back compatibility + // PS 2.6.0 do not have align + if (horizontal_align_str.empty()) + return FontProp::HorizontalAlign::center; + + // Back compatibility + // PS 2.6.1 store indices(0|1|2) instead of text for align + if (horizontal_align_str.length() == 1) { + int horizontal_align_int = 0; + if(boost::spirit::qi::parse(horizontal_align_str.c_str(), horizontal_align_str.c_str() + 1, boost::spirit::qi::int_, horizontal_align_int)) + return static_cast(horizontal_align_int); + } + + return bimap_cvt(horizontal_align_to_name, std::string_view(horizontal_align_str), FontProp::HorizontalAlign::center); +} + + +FontProp::VerticalAlign read_vertical_align(const char **attributes, unsigned int num_attributes, const TextConfigurationSerialization::VerticalAlignToName& vertical_align_to_name){ + std::string vertical_align_str = bbs_get_attribute_value_string(attributes, num_attributes, VERTICAL_ALIGN_ATTR); + + // Back compatibility + // PS 2.6.0 do not have align + if (vertical_align_str.empty()) + return FontProp::VerticalAlign::center; + + // Back compatibility + // PS 2.6.1 store indices(0|1|2) instead of text for align + if (vertical_align_str.length() == 1) { + int vertical_align_int = 0; + if(boost::spirit::qi::parse(vertical_align_str.c_str(), vertical_align_str.c_str() + 1, boost::spirit::qi::int_, vertical_align_int)) + return static_cast(vertical_align_int); + } + + return bimap_cvt(vertical_align_to_name, std::string_view(vertical_align_str), FontProp::VerticalAlign::center); +} + +} // namespace + +std::optional TextConfigurationSerialization::read(const char **attributes, unsigned int num_attributes) +{ + FontProp fp; + int char_gap = bbs_get_attribute_value_int(attributes, num_attributes, CHAR_GAP_ATTR); + if (char_gap != 0) fp.char_gap = char_gap; + int line_gap = bbs_get_attribute_value_int(attributes, num_attributes, LINE_GAP_ATTR); + if (line_gap != 0) fp.line_gap = line_gap; + float boldness = bbs_get_attribute_value_float(attributes, num_attributes, BOLDNESS_ATTR); + if (std::fabs(boldness) > std::numeric_limits::epsilon()) + fp.boldness = boldness; + float skew = bbs_get_attribute_value_float(attributes, num_attributes, SKEW_ATTR); + if (std::fabs(skew) > std::numeric_limits::epsilon()) + fp.skew = skew; + int per_glyph = bbs_get_attribute_value_int(attributes, num_attributes, PER_GLYPH_ATTR); + if (per_glyph == 1) fp.per_glyph = true; + + fp.align = FontProp::Align( + read_horizontal_align(attributes, num_attributes, horizontal_align_to_name), + read_vertical_align(attributes, num_attributes, vertical_align_to_name)); + + int collection_number = bbs_get_attribute_value_int(attributes, num_attributes, COLLECTION_NUMBER_ATTR); + if (collection_number > 0) fp.collection_number = static_cast(collection_number); + + fp.size_in_mm = bbs_get_attribute_value_float(attributes, num_attributes, LINE_HEIGHT_ATTR); + + std::string family = bbs_get_attribute_value_string(attributes, num_attributes, FONT_FAMILY_ATTR); + if (!family.empty()) fp.family = family; + std::string face_name = bbs_get_attribute_value_string(attributes, num_attributes, FONT_FACE_NAME_ATTR); + if (!face_name.empty()) fp.face_name = face_name; + std::string style = bbs_get_attribute_value_string(attributes, num_attributes, FONT_STYLE_ATTR); + if (!style.empty()) fp.style = style; + std::string weight = bbs_get_attribute_value_string(attributes, num_attributes, FONT_WEIGHT_ATTR); + if (!weight.empty()) fp.weight = weight; + + std::string style_name = bbs_get_attribute_value_string(attributes, num_attributes, STYLE_NAME_ATTR); + std::string font_descriptor = bbs_get_attribute_value_string(attributes, num_attributes, FONT_DESCRIPTOR_ATTR); + std::string type_str = bbs_get_attribute_value_string(attributes, num_attributes, FONT_DESCRIPTOR_TYPE_ATTR); + EmbossStyle::Type type = bimap_cvt(type_to_name, std::string_view{type_str}, EmbossStyle::Type::undefined); + + std::string text = bbs_get_attribute_value_string(attributes, num_attributes, TEXT_DATA_ATTR); + EmbossStyle es{style_name, std::move(font_descriptor), type, std::move(fp)}; + return TextConfiguration{std::move(es), std::move(text)}; +} + +EmbossShape TextConfigurationSerialization::read_old(const char **attributes, unsigned int num_attributes) +{ + EmbossShape es; + std::string fix_tr_mat_str = bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR); + if (!fix_tr_mat_str.empty()) + es.fix_3mf_tr = bbs_get_transform_from_3mf_specs_string(fix_tr_mat_str); + + + if (bbs_get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR) == 1) + es.projection.use_surface = true; + + es.projection.depth = bbs_get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); + + int use_surface = bbs_get_attribute_value_int(attributes, num_attributes, USE_SURFACE_ATTR); + if (use_surface == 1) + es.projection.use_surface = true; + + return es; +} + +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.cpp b/src/libslic3r/Geometry.cpp index e57b774f3..b19445b79 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -397,42 +397,12 @@ Transform3d scale_transform(const Vec3d& scale) Vec3d extract_euler_angles(const Eigen::Matrix& rotation_matrix) { - // reference: http://www.gregslabaugh.net/publications/euler.pdf - Vec3d angles1 = Vec3d::Zero(); - Vec3d angles2 = Vec3d::Zero(); - // BBS: rotation_matrix(2, 0) may be slighterly larger than 1 due to numerical accuracy - if (std::abs(std::abs(rotation_matrix(2, 0)) - 1.0) < 1e-5 || std::abs(rotation_matrix(2, 0))>1) { - angles1.z() = 0.0; - if (rotation_matrix(2, 0) < 0.0) { // == -1.0 - angles1.y() = 0.5 * double(PI); - angles1.x() = angles1.z() + ::atan2(rotation_matrix(0, 1), rotation_matrix(0, 2)); - } - else { // == 1.0 - angles1.y() = - 0.5 * double(PI); - angles1.x() = - angles1.y() + ::atan2(- rotation_matrix(0, 1), - rotation_matrix(0, 2)); - } - angles2 = angles1; - } - else { - angles1.y() = -::asin(rotation_matrix(2, 0)); - const double inv_cos1 = 1.0 / ::cos(angles1.y()); - angles1.x() = ::atan2(rotation_matrix(2, 1) * inv_cos1, rotation_matrix(2, 2) * inv_cos1); - angles1.z() = ::atan2(rotation_matrix(1, 0) * inv_cos1, rotation_matrix(0, 0) * inv_cos1); - - angles2.y() = double(PI) - angles1.y(); - const double inv_cos2 = 1.0 / ::cos(angles2.y()); - angles2.x() = ::atan2(rotation_matrix(2, 1) * inv_cos2, rotation_matrix(2, 2) * inv_cos2); - angles2.z() = ::atan2(rotation_matrix(1, 0) * inv_cos2, rotation_matrix(0, 0) * inv_cos2); - } - - // The following euristic is the best found up to now (in the sense that it works fine with the greatest number of edge use-cases) - // but there are other use-cases were it does not - // We need to improve it - const double min_1 = angles1.cwiseAbs().minCoeff(); - const double min_2 = angles2.cwiseAbs().minCoeff(); - const bool use_1 = (min_1 < min_2) || (is_approx(min_1, min_2) && (angles1.norm() <= angles2.norm())); - - return use_1 ? angles1 : angles2; + // The extracted "rotation" is a triplet of numbers such that Geometry::rotation_transform + // returns the original transform. Because of the chosen order of rotations, the triplet + // is not equivalent to Euler angles in the usual sense. + Vec3d angles = rotation_matrix.eulerAngles(2,1,0); + std::swap(angles(0), angles(2)); + return angles; } Vec3d extract_euler_angles(const Transform3d& transform) @@ -446,14 +416,6 @@ Vec3d extract_euler_angles(const Transform3d& transform) return extract_euler_angles(m); } -static Transform3d extract_rotation_matrix(const Transform3d& trafo) -{ - Matrix3d rotation; - Matrix3d scale; - trafo.computeRotationScaling(&rotation, &scale); - return Transform3d(rotation); -} - void rotation_from_two_vectors(Vec3d from, Vec3d to, Vec3d& rotation_axis, double& phi, Matrix3d* rotation_matrix) { double epsilon = 1e-5; @@ -488,42 +450,65 @@ void rotation_from_two_vectors(Vec3d from, Vec3d to, Vec3d& rotation_axis, doubl } } -bool Transformation::Flags::needs_update(bool dont_translate, bool dont_rotate, bool dont_scale, bool dont_mirror) const +Transform3d Transformation::get_offset_matrix() const { - return (this->dont_translate != dont_translate) || (this->dont_rotate != dont_rotate) || (this->dont_scale != dont_scale) || (this->dont_mirror != dont_mirror); + return translation_transform(get_offset()); } -void Transformation::Flags::set(bool dont_translate, bool dont_rotate, bool dont_scale, bool dont_mirror) +static Transform3d extract_rotation_matrix(const Transform3d& trafo) { - this->dont_translate = dont_translate; - this->dont_rotate = dont_rotate; - this->dont_scale = dont_scale; - this->dont_mirror = dont_mirror; + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + return Transform3d(rotation); } -Transformation::Transformation() +static Transform3d extract_scale(const Transform3d& trafo) { - reset(); + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + return Transform3d(scale); } -Transformation::Transformation(const Transform3d& transform) +static std::pair extract_rotation_scale(const Transform3d& trafo) { - set_from_transform(transform); + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + return { Transform3d(rotation), Transform3d(scale) }; } -void Transformation::set_offset(const Vec3d& offset) +static bool contains_skew(const Transform3d& trafo) { - set_offset(X, offset.x()); - set_offset(Y, offset.y()); - set_offset(Z, offset.z()); + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + + if (scale.isDiagonal()) + return false; + + if (scale.determinant() >= 0.0) + return true; + + // the matrix contains mirror + const Matrix3d ratio = scale.cwiseQuotient(trafo.matrix().block<3,3>(0,0)); + + auto check_skew = [&ratio](int i, int j, bool& skew) { + if (!std::isnan(ratio(i, j)) && !std::isnan(ratio(j, i))) + skew |= std::abs(ratio(i, j) * ratio(j, i) - 1.0) > EPSILON; + }; + + bool has_skew = false; + check_skew(0, 1, has_skew); + check_skew(0, 2, has_skew); + check_skew(1, 2, has_skew); + return has_skew; } -void Transformation::set_offset(Axis axis, double offset) +Vec3d Transformation::get_rotation() const { - if (m_offset(axis) != offset) { - m_offset(axis) = offset; - m_dirty = true; - } + return extract_euler_angles(extract_rotation_matrix(m_matrix)); } Transform3d Transformation::get_rotation_matrix() const @@ -533,9 +518,9 @@ Transform3d Transformation::get_rotation_matrix() const void Transformation::set_rotation(const Vec3d& rotation) { - set_rotation(X, rotation.x()); - set_rotation(Y, rotation.y()); - set_rotation(Z, rotation.z()); + const Vec3d offset = get_offset(); + m_matrix = rotation_transform(rotation) * extract_scale(m_matrix); + m_matrix.translation() = offset; } void Transformation::set_rotation(Axis axis, double rotation) @@ -544,32 +529,88 @@ void Transformation::set_rotation(Axis axis, double rotation) if (is_approx(std::abs(rotation), 2.0 * double(PI))) rotation = 0.0; - if (m_rotation(axis) != rotation) { - m_rotation(axis) = rotation; - m_dirty = true; - } + auto [curr_rotation, scale] = extract_rotation_scale(m_matrix); + Vec3d angles = extract_euler_angles(curr_rotation); + angles[axis] = rotation; + + const Vec3d offset = get_offset(); + m_matrix = rotation_transform(angles) * scale; + m_matrix.translation() = offset; +} + +Vec3d Transformation::get_scaling_factor() const +{ + const Transform3d scale = extract_scale(m_matrix); + return { std::abs(scale(0, 0)), std::abs(scale(1, 1)), std::abs(scale(2, 2)) }; +} + +Transform3d Transformation::get_scaling_factor_matrix() const +{ + Transform3d scale = extract_scale(m_matrix); + scale(0, 0) = std::abs(scale(0, 0)); + scale(1, 1) = std::abs(scale(1, 1)); + scale(2, 2) = std::abs(scale(2, 2)); + return scale; } void Transformation::set_scaling_factor(const Vec3d& scaling_factor) { - set_scaling_factor(X, scaling_factor.x()); - set_scaling_factor(Y, scaling_factor.y()); - set_scaling_factor(Z, scaling_factor.z()); + assert(scaling_factor.x() > 0.0 && scaling_factor.y() > 0.0 && scaling_factor.z() > 0.0); + + const Vec3d offset = get_offset(); + m_matrix = extract_rotation_matrix(m_matrix) * scale_transform(scaling_factor); + m_matrix.translation() = offset; } void Transformation::set_scaling_factor(Axis axis, double scaling_factor) { - if (m_scaling_factor(axis) != std::abs(scaling_factor)) { - m_scaling_factor(axis) = std::abs(scaling_factor); - m_dirty = true; - } + assert(scaling_factor > 0.0); + + auto [rotation, scale] = extract_rotation_scale(m_matrix); + scale(axis, axis) = scaling_factor; + + const Vec3d offset = get_offset(); + m_matrix = rotation * scale; + m_matrix.translation() = offset; +} + +Vec3d Transformation::get_mirror() const +{ + const Transform3d scale = extract_scale(m_matrix); + return { scale(0, 0) / std::abs(scale(0, 0)), scale(1, 1) / std::abs(scale(1, 1)), scale(2, 2) / std::abs(scale(2, 2)) }; +} + +Transform3d Transformation::get_mirror_matrix() const +{ + Transform3d scale = extract_scale(m_matrix); + scale(0, 0) = scale(0, 0) / std::abs(scale(0, 0)); + scale(1, 1) = scale(1, 1) / std::abs(scale(1, 1)); + scale(2, 2) = scale(2, 2) / std::abs(scale(2, 2)); + return scale; } void Transformation::set_mirror(const Vec3d& mirror) { - set_mirror(X, mirror.x()); - set_mirror(Y, mirror.y()); - set_mirror(Z, mirror.z()); + Vec3d copy(mirror); + const Vec3d abs_mirror = copy.cwiseAbs(); + for (int i = 0; i < 3; ++i) { + if (abs_mirror(i) == 0.0) + copy(i) = 1.0; + else if (abs_mirror(i) != 1.0) + copy(i) /= abs_mirror(i); + } + + auto [rotation, scale] = extract_rotation_scale(m_matrix); + const Vec3d curr_scales = { scale(0, 0), scale(1, 1), scale(2, 2) }; + const Vec3d signs = curr_scales.cwiseProduct(copy); + + if (signs[0] < 0.0) scale(0, 0) = -scale(0, 0); + if (signs[1] < 0.0) scale(1, 1) = -scale(1, 1); + if (signs[2] < 0.0) scale(2, 2) = -scale(2, 2); + + const Vec3d offset = get_offset(); + m_matrix = rotation * scale; + m_matrix.translation() = offset; } void Transformation::set_mirror(Axis axis, double mirror) @@ -580,75 +621,61 @@ void Transformation::set_mirror(Axis axis, double mirror) else if (abs_mirror != 1.0) mirror /= abs_mirror; - if (m_mirror(axis) != mirror) { - m_mirror(axis) = mirror; - m_dirty = true; - } + auto [rotation, scale] = extract_rotation_scale(m_matrix); + const double curr_scale = scale(axis, axis); + const double sign = curr_scale * mirror; + + if (sign < 0.0) scale(axis, axis) = -scale(axis, axis); + + const Vec3d offset = get_offset(); + m_matrix = rotation * scale; + m_matrix.translation() = offset; } -void Transformation::set_from_transform(const Transform3d& transform) +bool Transformation::has_skew() const { - // offset - set_offset(transform.matrix().block(0, 3, 3, 1)); - - Eigen::Matrix m3x3 = transform.matrix().block(0, 0, 3, 3); - - // mirror - // it is impossible to reconstruct the original mirroring factors from a matrix, - // we can only detect if the matrix contains a left handed reference system - // in which case we reorient it back to right handed by mirroring the x axis - Vec3d mirror = Vec3d::Ones(); - if (m3x3.col(0).dot(m3x3.col(1).cross(m3x3.col(2))) < 0.0) { - mirror.x() = -1.0; - // remove mirror - m3x3.col(0) *= -1.0; - } - set_mirror(mirror); - - // scale - set_scaling_factor(Vec3d(m3x3.col(0).norm(), m3x3.col(1).norm(), m3x3.col(2).norm())); - - // remove scale - m3x3.col(0).normalize(); - m3x3.col(1).normalize(); - m3x3.col(2).normalize(); - - // rotation - set_rotation(extract_euler_angles(m3x3)); - - // forces matrix recalculation matrix - m_matrix = get_matrix(); - -// // debug check -// if (!m_matrix.isApprox(transform)) -// std::cout << "something went wrong in extracting data from matrix" << std::endl; + return contains_skew(m_matrix); } void Transformation::reset() { - m_offset = Vec3d::Zero(); - m_rotation = Vec3d::Zero(); - m_scaling_factor = Vec3d::Ones(); - m_mirror = Vec3d::Ones(); m_matrix = Transform3d::Identity(); - m_dirty = false; } -const Transform3d& Transformation::get_matrix(bool dont_translate, bool dont_rotate, bool dont_scale, bool dont_mirror) const +void Transformation::reset_rotation() { - if (m_dirty || m_flags.needs_update(dont_translate, dont_rotate, dont_scale, dont_mirror)) { - m_matrix = Geometry::assemble_transform( - dont_translate ? Vec3d::Zero() : m_offset, - dont_rotate ? Vec3d::Zero() : m_rotation, - dont_scale ? Vec3d::Ones() : m_scaling_factor, - dont_mirror ? Vec3d::Ones() : m_mirror - ); + const Geometry::TransformationSVD svd(*this); + m_matrix = get_offset_matrix() * Transform3d(svd.v * svd.s * svd.v.transpose()) * svd.mirror_matrix(); +} - m_flags.set(dont_translate, dont_rotate, dont_scale, dont_mirror); - m_dirty = false; - } +void Transformation::reset_scaling_factor() +{ + const Geometry::TransformationSVD svd(*this); + m_matrix = get_offset_matrix() * Transform3d(svd.u) * Transform3d(svd.v.transpose()) * svd.mirror_matrix(); +} - return m_matrix; +void Transformation::reset_skew() +{ + auto new_scale_factor = [](const Matrix3d& s) { + return pow(s(0, 0) * s(1, 1) * s(2, 2), 1. / 3.); // scale average + }; + + const Geometry::TransformationSVD svd(*this); + m_matrix = get_offset_matrix() * Transform3d(svd.u) * scale_transform(new_scale_factor(svd.s)) * Transform3d(svd.v.transpose()) * svd.mirror_matrix(); +} + +Transform3d Transformation::get_matrix_no_offset() const +{ + Transformation copy(*this); + copy.reset_offset(); + return copy.get_matrix(); +} + +Transform3d Transformation::get_matrix_no_scaling_factor() const +{ + Transformation copy(*this); + copy.reset_scaling_factor(); + return copy.get_matrix(); } Transformation Transformation::operator * (const Transformation& other) const @@ -663,7 +690,7 @@ Transformation Transformation::volume_to_bed_transformation(const Transformation if (instance_transformation.is_scaling_uniform()) { // No need to run the non-linear least squares fitting for uniform scaling. // Just set the inverse. - out.set_from_transform(instance_transformation.get_matrix(true).inverse()); + out.set_matrix(instance_transformation.get_matrix_no_offset().inverse()); } else if (is_rotation_ninety_degrees(instance_transformation.get_rotation())) { // Anisotropic scaling, rotation by multiples of ninety degrees. @@ -712,6 +739,53 @@ Transformation Transformation::volume_to_bed_transformation(const Transformation return out; } +TransformationSVD::TransformationSVD(const Transform3d& trafo) +{ + const auto &m0 = trafo.matrix().block<3, 3>(0, 0); + mirror = m0.determinant() < 0.0; + + Matrix3d m; + if (mirror) + m = m0 * Eigen::DiagonalMatrix(-1.0, 1.0, 1.0); + else + m = m0; + const Eigen::JacobiSVD svd(m, Eigen::ComputeFullU | Eigen::ComputeFullV); + u = svd.matrixU(); + v = svd.matrixV(); + s = svd.singularValues().asDiagonal(); + + scale = !s.isApprox(Matrix3d::Identity()); + anisotropic_scale = ! is_approx(s(0, 0), s(1, 1)) || ! is_approx(s(1, 1), s(2, 2)); + rotation = !v.isApprox(u); + + if (anisotropic_scale) { + rotation_90_degrees = true; + for (int i = 0; i < 3; ++i) { + const Vec3d row = v.row(i).cwiseAbs(); + const size_t num_zeros = is_approx(row[0], 0.) + is_approx(row[1], 0.) + is_approx(row[2], 0.); + const size_t num_ones = is_approx(row[0], 1.) + is_approx(row[1], 1.) + is_approx(row[2], 1.); + if (num_zeros != 2 || num_ones != 1) { + rotation_90_degrees = false; + break; + } + } + // Detect skew by brute force: check if the axes are still orthogonal after transformation + const Matrix3d trafo_linear = trafo.linear(); + const std::array axes = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() }; + std::array transformed_axes; + for (int i = 0; i < 3; ++i) { + transformed_axes[i] = trafo_linear * axes[i]; + } + skew = std::abs(transformed_axes[0].dot(transformed_axes[1])) > EPSILON || + std::abs(transformed_axes[1].dot(transformed_axes[2])) > EPSILON || + std::abs(transformed_axes[2].dot(transformed_axes[0])) > EPSILON; + + // This following old code does not work under all conditions. The v matrix can become non diagonal (see SPE-1492) +// skew = ! rotation_90_degrees; + } else + skew = false; +} + // For parsing a transformation matrix from 3MF / AMF. Transform3d transform3d_from_string(const std::string& transform_str) { diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index 4f6511ff6..616fb5649 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -51,9 +51,9 @@ enum Orientation static inline Orientation orient(const Point &a, const Point &b, const Point &c) { static_assert(sizeof(coord_t) * 2 == sizeof(int64_t), "orient works with 32 bit coordinates"); - int64_t u = int64_t(b(0)) * int64_t(c(1)) - int64_t(b(1)) * int64_t(c(0)); - int64_t v = int64_t(a(0)) * int64_t(c(1)) - int64_t(a(1)) * int64_t(c(0)); - int64_t w = int64_t(a(0)) * int64_t(b(1)) - int64_t(a(1)) * int64_t(b(0)); + int64_t u = int64_t(b.x()) * int64_t(c.y()) - int64_t(b.y()) * int64_t(c.x()); + int64_t v = int64_t(a.x()) * int64_t(c.y()) - int64_t(a.y()) * int64_t(c.x()); + int64_t w = int64_t(a.x()) * int64_t(b.y()) - int64_t(a.y()) * int64_t(b.x()); int64_t d = u - v + w; return (d > 0) ? ORIENTATION_CCW : ((d == 0) ? ORIENTATION_COLINEAR : ORIENTATION_CW); } @@ -322,6 +322,13 @@ 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); @@ -404,66 +411,67 @@ void rotation_from_two_vectors(Vec3d from, Vec3d to, Vec3d &rotation_axis, doubl class Transformation { - struct Flags - { - bool dont_translate{ true }; - bool dont_rotate{ true }; - bool dont_scale{ true }; - bool dont_mirror{ true }; - - bool needs_update(bool dont_translate, bool dont_rotate, bool dont_scale, bool dont_mirror) const; - void set(bool dont_translate, bool dont_rotate, bool dont_scale, bool dont_mirror); - }; - - Vec3d m_offset{ Vec3d::Zero() }; // In unscaled coordinates - Vec3d m_rotation{ Vec3d::Zero() }; // Rotation around the three axes, in radians around mesh center point - Vec3d m_scaling_factor{ Vec3d::Ones() }; // Scaling factors along the three axes - Vec3d m_mirror{ Vec3d::Ones() }; // Mirroring along the three axes - - mutable Transform3d m_matrix{ Transform3d::Identity() }; - mutable Flags m_flags; - mutable bool m_dirty{ false }; + Transform3d m_matrix{ Transform3d::Identity() }; public: - Transformation(); - explicit Transformation(const Transform3d& transform); + Transformation() = default; + explicit Transformation(const Transform3d& transform) : m_matrix(transform) {} - //BBS: add get dirty function - bool is_dirty() { return m_dirty; } + Vec3d get_offset() const { return m_matrix.translation(); } + double get_offset(Axis axis) const { return get_offset()[axis]; } - const Vec3d& get_offset() const { return m_offset; } - double get_offset(Axis axis) const { return m_offset(axis); } + Transform3d get_offset_matrix() const; - void set_offset(const Vec3d& offset); - void set_offset(Axis axis, double offset); + void set_offset(const Vec3d& offset) { m_matrix.translation() = offset; } + void set_offset(Axis axis, double offset) { m_matrix.translation()[axis] = offset; } - const Vec3d& get_rotation() const { return m_rotation; } - double get_rotation(Axis axis) const { return m_rotation(axis); } + Vec3d get_rotation() const; + double get_rotation(Axis axis) const { return get_rotation()[axis]; } Transform3d get_rotation_matrix() const; void set_rotation(const Vec3d& rotation); void set_rotation(Axis axis, double rotation); - const Vec3d& get_scaling_factor() const { return m_scaling_factor; } - double get_scaling_factor(Axis axis) const { return m_scaling_factor(axis); } + Vec3d get_scaling_factor() const; + double get_scaling_factor(Axis axis) const { return get_scaling_factor()[axis]; } + + Transform3d get_scaling_factor_matrix() const; + + bool is_scaling_uniform() const { + const Vec3d scale = get_scaling_factor(); + return std::abs(scale.x() - scale.y()) < 1e-8 && std::abs(scale.x() - scale.z()) < 1e-8; + } void set_scaling_factor(const Vec3d& scaling_factor); void set_scaling_factor(Axis axis, double scaling_factor); - bool is_scaling_uniform() const { return std::abs(m_scaling_factor.x() - m_scaling_factor.y()) < 1e-8 && std::abs(m_scaling_factor.x() - m_scaling_factor.z()) < 1e-8; } - const Vec3d& get_mirror() const { return m_mirror; } - double get_mirror(Axis axis) const { return m_mirror(axis); } - bool is_left_handed() const { return m_mirror.x() * m_mirror.y() * m_mirror.z() < 0.; } + Vec3d get_mirror() const; + double get_mirror(Axis axis) const { return get_mirror()[axis]; } + + Transform3d get_mirror_matrix() const; + + bool is_left_handed() const { + return m_matrix.linear().determinant() < 0; + } void set_mirror(const Vec3d& mirror); void set_mirror(Axis axis, double mirror); - void set_from_transform(const Transform3d& transform); + bool has_skew() const; void reset(); + void reset_offset() { set_offset(Vec3d::Zero()); } + void reset_rotation(); + void reset_scaling_factor(); + void reset_mirror() { set_mirror(Vec3d::Ones()); } + void reset_skew(); - const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const; + const Transform3d& get_matrix() const { return m_matrix; } + Transform3d get_matrix_no_offset() const; + Transform3d get_matrix_no_scaling_factor() const; + + void set_matrix(const Transform3d& transform) { m_matrix = transform; } Transformation operator * (const Transformation& other) const; @@ -474,19 +482,43 @@ public: // BBS: backup use this compare friend bool operator==(Transformation const& l, Transformation const& r) { - return l.m_offset == r.m_offset && l.m_rotation == r.m_rotation && l.m_scaling_factor == r.m_scaling_factor && l.m_mirror == r.m_mirror; + return l.m_matrix.isApprox(r.m_matrix); + } + + friend bool operator!=(Transformation const &l, Transformation const &r) + { + return !(l == r); } private: friend class cereal::access; - template void serialize(Archive & ar) { ar(m_offset, m_rotation, m_scaling_factor, m_mirror); } - explicit Transformation(int) : m_dirty(true) {} - template static void load_and_construct(Archive &ar, cereal::construct &construct) - { - // Calling a private constructor with special "int" parameter to indicate that no construction is necessary. - construct(1); - ar(construct.ptr()->m_offset, construct.ptr()->m_rotation, construct.ptr()->m_scaling_factor, construct.ptr()->m_mirror); - } + template void serialize(Archive& ar) { ar(m_matrix); } + explicit Transformation(int) {} + template static void load_and_construct(Archive& ar, cereal::construct& construct) + { + // Calling a private constructor with special "int" parameter to indicate that no construction is necessary. + construct(1); + ar(construct.ptr()->m_matrix); + } +}; + +struct TransformationSVD +{ + Matrix3d u{ Matrix3d::Identity() }; + Matrix3d s{ Matrix3d::Identity() }; + Matrix3d v{ Matrix3d::Identity() }; + + bool mirror{ false }; + bool scale{ false }; + bool anisotropic_scale{ false }; + bool rotation{ false }; + bool rotation_90_degrees{ false }; + bool skew{ false }; + + explicit TransformationSVD(const Transformation& trafo) : TransformationSVD(trafo.get_matrix()) {} + explicit TransformationSVD(const Transform3d& trafo); + + Eigen::DiagonalMatrix mirror_matrix() const { return Eigen::DiagonalMatrix(this->mirror ? -1. : 1., 1., 1.); } }; // For parsing a transformation matrix from 3MF / AMF. diff --git a/src/libslic3r/IntersectionPoints.cpp b/src/libslic3r/IntersectionPoints.cpp new file mode 100644 index 000000000..bdf66d590 --- /dev/null +++ b/src/libslic3r/IntersectionPoints.cpp @@ -0,0 +1,49 @@ +///|/ Copyright (c) Prusa Research 2023 Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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..cf06c718a --- /dev/null +++ b/src/libslic3r/IntersectionPoints.hpp @@ -0,0 +1,27 @@ +///|/ Copyright (c) Prusa Research 2023 Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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 3fc6693a7..a8aecf178 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1368,7 +1368,7 @@ indexed_triangle_set ModelObject::raw_indexed_triangle_set() const size_t j = out.indices.size(); append(out.vertices, v->mesh().its.vertices); append(out.indices, v->mesh().its.indices); - auto m = v->get_matrix(); + const Transform3d& m = v->get_matrix(); for (; i < out.vertices.size(); ++ i) out.vertices[i] = (m * out.vertices[i].cast()).cast().eval(); if (v->is_left_handed()) { @@ -1410,7 +1410,7 @@ const BoundingBoxf3& ModelObject::raw_bounding_box() const if (this->instances.empty()) throw Slic3r::InvalidArgument("Can't call raw_bounding_box() with no instances"); - const Transform3d& inst_matrix = this->instances.front()->get_transformation().get_matrix(true); + const Transform3d inst_matrix = this->instances.front()->get_transformation().get_matrix_no_offset(); for (const ModelVolume *v : this->volumes) if (v->is_model_part()) m_raw_bounding_box.merge(v->mesh().transformed_bounding_box(inst_matrix * v->get_matrix())); @@ -1423,14 +1423,18 @@ BoundingBoxf3 ModelObject::instance_bounding_box(size_t instance_idx, bool dont_ return instance_bounding_box(*this->instances[instance_idx], dont_translate); } -BoundingBoxf3 ModelObject::instance_bounding_box(const ModelInstance &instance, bool dont_translate) const { - BoundingBoxf3 bbox; - const auto& inst_mat = instance.get_transformation().get_matrix(dont_translate); - for (auto vol : this->volumes) { - if (vol->is_model_part()) - bbox.merge(vol->mesh().transformed_bounding_box(inst_mat * vol->get_matrix())); +BoundingBoxf3 ModelObject::instance_bounding_box(const ModelInstance &instance, bool dont_translate) const +{ + BoundingBoxf3 bb; + const Transform3d inst_matrix = dont_translate ? + instance.get_transformation().get_matrix_no_offset() : + instance.get_transformation().get_matrix(); + + for (ModelVolume *v : this->volumes) { + if (v->is_model_part()) + bb.merge(v->mesh().transformed_bounding_box(inst_matrix * v->get_matrix())); } - return bbox; + return bb; } @@ -1438,7 +1442,9 @@ BoundingBoxf3 ModelObject::instance_bounding_box(const ModelInstance &instance, BoundingBoxf3 ModelObject::instance_convex_hull_bounding_box(size_t instance_idx, bool dont_translate) const { BoundingBoxf3 bb; - const Transform3d& inst_matrix = this->instances[instance_idx]->get_transformation().get_matrix(dont_translate); + const Transform3d& inst_matrix = dont_translate ? + this->instances[instance_idx]->get_transformation().get_matrix_no_offset() : + this->instances[instance_idx]->get_transformation().get_matrix(); for (ModelVolume *v : this->volumes) { if (v->is_model_part()) @@ -1739,6 +1745,25 @@ 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; +} + void ModelVolume::reset_extra_facets() { this->supported_facets.reset(); @@ -1759,67 +1784,6 @@ static void invalidate_translations(ModelObject* object, const ModelInstance* sr } } -static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, const Transform3d& cut_matrix, - bool place_on_cut = false, bool flip = false, Vec3d local_displace = Vec3d::Zero()) -{ - using namespace Geometry; - - // Reset instance transformation except offset and Z-rotation - - for (size_t i = 0; i < object->instances.size(); ++i) { - auto& obj_instance = object->instances[i]; - - Geometry::Transformation instance_transformation_copy = obj_instance->get_transformation(); - instance_transformation_copy.set_offset(Vec3d(0, 0, 0)); - if (object->volumes.size() == 1) { - instance_transformation_copy.set_offset(-object->volumes[0]->get_offset()); - } - - if (i == src_instance_idx && object->volumes.size() == 1) - invalidate_translations(object, obj_instance); - - const Vec3d offset = obj_instance->get_offset(); - const double rot_z = obj_instance->get_rotation().z(); - - obj_instance->set_transformation(Transformation()); - - const Vec3d displace = local_displace.isApprox(Vec3d::Zero()) ? Vec3d::Zero() : - rotation_transform(obj_instance->get_rotation()) * local_displace; - obj_instance->set_offset(offset + displace); - - Vec3d rotation = Vec3d::Zero(); - if (!flip && !place_on_cut) { - if ( i != src_instance_idx) - rotation[Z] = rot_z; - } - else { - Transform3d rotation_matrix = Transform3d::Identity(); - if (flip) - rotation_matrix = rotation_transform(PI * Vec3d::UnitX()); - - if (place_on_cut) - rotation_matrix = rotation_matrix * Transformation(cut_matrix).get_matrix(true, false, true, true).inverse(); - - if (i != src_instance_idx) - rotation_matrix = rotation_transform(rot_z * Vec3d::UnitZ()) * rotation_matrix; - - rotation = Transformation(rotation_matrix).get_rotation(); - } - - obj_instance->set_rotation(rotation); - - // update the assemble matrix - const Transform3d &assemble_matrix = obj_instance->get_assemble_transformation().get_matrix(); - const Transform3d &instance_inverse_matrix = instance_transformation_copy.get_matrix().inverse(); - Transform3d new_instance_inverse_matrix = instance_inverse_matrix * obj_instance->get_transformation().get_matrix(true).inverse(); - if (place_on_cut) { // reset the rotation of cut plane - new_instance_inverse_matrix = new_instance_inverse_matrix * Transformation(cut_matrix).get_matrix(true, false, true, true).inverse(); - } - Transform3d new_assemble_transform = assemble_matrix * new_instance_inverse_matrix; - obj_instance->set_assemble_from_transform(new_assemble_transform); - } -} - void ModelObject::split(ModelObjectPtrs* new_objects) { std::vector all_meshes; @@ -1833,6 +1797,10 @@ void ModelObject::split(ModelObjectPtrs* new_objects) if (volume->type() != ModelVolumeType::MODEL_PART) continue; + // splited volume should not be text object + if (volume->text_configuration.has_value()) + volume->text_configuration.reset(); + if (!is_multi_volume_object) { //BBS: not multi volume object, then split mesh. std::vector volume_meshes = volume->mesh().split(); @@ -1912,7 +1880,7 @@ void ModelObject::split(ModelObjectPtrs* new_objects) for (ModelInstance* model_instance : new_object->instances) { - Vec3d shift = model_instance->get_transformation().get_matrix(true) * new_vol->get_offset(); + const Vec3d shift = model_instance->get_transformation().get_matrix_no_offset() * new_vol->get_offset(); model_instance->set_offset(model_instance->get_offset() + shift); //BBS: add assemble_view related logic @@ -1920,7 +1888,7 @@ void ModelObject::split(ModelObjectPtrs* new_objects) instance_transformation_copy.set_offset(-new_vol->get_offset()); const Transform3d &assemble_matrix = model_instance->get_assemble_transformation().get_matrix(); const Transform3d &instance_inverse_matrix = instance_transformation_copy.get_matrix().inverse(); - Transform3d new_instance_inverse_matrix = instance_inverse_matrix * model_instance->get_transformation().get_matrix(true).inverse(); + Transform3d new_instance_inverse_matrix = instance_inverse_matrix * model_instance->get_transformation().get_matrix_no_offset().inverse(); Transform3d new_assemble_transform = assemble_matrix * new_instance_inverse_matrix; model_instance->set_assemble_from_transform(new_assemble_transform); model_instance->set_offset_to_assembly(new_vol->get_offset()); @@ -2037,8 +2005,14 @@ void ModelObject::bake_xy_rotation_into_meshes(size_t instance_idx) // Adjust the meshes. // Transformation to be applied to the meshes. - Eigen::Matrix3d mesh_trafo_3x3 = reference_trafo.get_matrix(true, false, uniform_scaling, ! has_mirrorring).matrix().block<3, 3>(0, 0); - Transform3d volume_offset_correction = this->instances[instance_idx]->get_transformation().get_matrix().inverse() * reference_trafo.get_matrix(); + Geometry::Transformation reference_trafo_mod = reference_trafo; + reference_trafo_mod.reset_offset(); + if (uniform_scaling) + reference_trafo_mod.reset_scaling_factor(); + if (!has_mirrorring) + reference_trafo_mod.reset_mirror(); + Eigen::Matrix3d mesh_trafo_3x3 = reference_trafo_mod.get_matrix().matrix().block<3, 3>(0, 0); + Transform3d volume_offset_correction = this->instances[instance_idx]->get_transformation().get_matrix().inverse() * reference_trafo.get_matrix(); for (ModelVolume *model_volume : this->volumes) { const Geometry::Transformation volume_trafo = model_volume->get_transformation(); bool volume_left_handed = volume_trafo.is_left_handed(); @@ -2047,9 +2021,15 @@ void ModelObject::bake_xy_rotation_into_meshes(size_t instance_idx) std::abs(volume_trafo.get_scaling_factor().x() - volume_trafo.get_scaling_factor().z()) < EPSILON; double volume_new_scaling_factor = volume_uniform_scaling ? volume_trafo.get_scaling_factor().x() : 1.; // Transform the mesh. - Matrix3d volume_trafo_3x3 = volume_trafo.get_matrix(true, false, volume_uniform_scaling, !volume_has_mirrorring).matrix().block<3, 3>(0, 0); + Geometry::Transformation volume_trafo_mod = volume_trafo; + volume_trafo_mod.reset_offset(); + if (volume_uniform_scaling) + volume_trafo_mod.reset_scaling_factor(); + if (!volume_has_mirrorring) + volume_trafo_mod.reset_mirror(); + Eigen::Matrix3d volume_trafo_3x3 = volume_trafo_mod.get_matrix().matrix().block<3, 3>(0, 0); // Following method creates a new shared_ptr - model_volume->transform_this_mesh(mesh_trafo_3x3 * volume_trafo_3x3, left_handed != volume_left_handed); + model_volume->transform_this_mesh(mesh_trafo_3x3 * volume_trafo_3x3, left_handed != volume_left_handed); // Reset the rotation, scaling and mirroring. model_volume->set_rotation(Vec3d(0., 0., 0.)); model_volume->set_scaling_factor(Vec3d(volume_new_scaling_factor, volume_new_scaling_factor, volume_new_scaling_factor)); @@ -2094,7 +2074,7 @@ double ModelObject::get_instance_min_z(size_t instance_idx) const double min_z = DBL_MAX; const ModelInstance* inst = instances[instance_idx]; - const Transform3d& mi = inst->get_matrix(true); + const Transform3d mi = inst->get_matrix_no_offset(); for (const ModelVolume* v : volumes) { if (!v->is_model_part()) @@ -2130,7 +2110,7 @@ double ModelObject::get_instance_max_z(size_t instance_idx) const double max_z = -DBL_MAX; const ModelInstance* inst = instances[instance_idx]; - const Transform3d& mi = inst->get_matrix(true); + const Transform3d mi = inst->get_matrix_no_offset(); for (const ModelVolume* v : volumes) { if (!v->is_model_part()) @@ -2553,6 +2533,10 @@ size_t ModelVolume::split(unsigned int max_extruders) if (meshes.size() <= 1) return 1; + // splited volume should not be text object + if (text_configuration.has_value()) + text_configuration.reset(); + 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; @@ -2723,44 +2707,17 @@ void ModelVolume::convert_from_meters() void ModelInstance::transform_mesh(TriangleMesh* mesh, bool dont_translate) const { - mesh->transform(get_matrix(dont_translate)); -} - -BoundingBoxf3 ModelInstance::transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate) const -{ - // Rotate around mesh origin. - TriangleMesh copy(mesh); - copy.transform(get_matrix(true, false, true, true)); - BoundingBoxf3 bbox = copy.bounding_box(); - - if (!empty(bbox)) { - // Scale the bounding box along the three axes. - for (unsigned int i = 0; i < 3; ++i) - { - if (std::abs(get_scaling_factor((Axis)i)-1.0) > EPSILON) - { - bbox.min(i) *= get_scaling_factor((Axis)i); - bbox.max(i) *= get_scaling_factor((Axis)i); - } - } - - // Translate the bounding box. - if (! dont_translate) { - bbox.min += get_offset(); - bbox.max += get_offset(); - } - } - return bbox; + mesh->transform(dont_translate ? get_matrix_no_offset() : get_matrix()); } BoundingBoxf3 ModelInstance::transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate) const { - return bbox.transformed(get_matrix(dont_translate)); + return bbox.transformed(dont_translate ? get_matrix_no_offset() : get_matrix()); } Vec3d ModelInstance::transform_vector(const Vec3d& v, bool dont_translate) const { - return get_matrix(dont_translate) * v; + return dont_translate ? get_matrix_no_offset() * v : get_matrix() * v; } void ModelInstance::transform_polygon(Polygon* polygon) const @@ -2952,7 +2909,7 @@ Polygon ModelInstance::convex_hull_2d() //BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": name %1%, is_valid %2%")% this->object->name.c_str()% convex_hull.is_valid(); //if (!convex_hull.is_valid()) { // this logic is not working right now, as moving instance doesn't update convex_hull - const Transform3d& trafo_instance = get_matrix(false); + const Transform3d& trafo_instance = get_matrix(); convex_hull = get_object()->convex_hull_2d(trafo_instance); } //int size = convex_hull.size(); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 219d5b19a..2b4fa14e4 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -26,6 +26,8 @@ #include "CustomGCode.hpp" #include "calib.hpp" #include "enum_bitmask.hpp" +#include "TextConfiguration.hpp" +#include "EmbossShape.hpp" //BBS: add bbs 3mf #include "Format/bbs_3mf.hpp" @@ -41,6 +43,7 @@ #include #include #include +#include namespace cereal { class BinaryInputArchive; @@ -779,38 +782,6 @@ private: friend class ModelVolume; }; -struct RaycastResult -{ - Vec2d mouse_position = Vec2d::Zero(); - int mesh_id = -1; - Vec3f hit = Vec3f::Zero(); - Vec3f normal = Vec3f::Zero(); - - template void serialize(Archive &ar) { ar(mouse_position, mesh_id, hit, normal); } -}; - -struct TextInfo -{ - std::string m_font_name; - float m_font_size = 16.f; - int m_curr_font_idx = 0; - bool m_bold = true; - bool m_italic = false; - float m_thickness = 2.f; - float m_embeded_depth = 0.f; - float m_rotate_angle = 0; - float m_text_gap = 0.f; - bool m_is_surface_text = false; - bool m_keep_horizontal = false; - std::string m_text; - - RaycastResult m_rr; - - template void serialize(Archive &ar) { - ar(m_font_name, m_font_size, m_curr_font_idx, m_bold, m_italic, m_thickness, m_embeded_depth, m_rotate_angle, m_text_gap, m_is_surface_text, m_keep_horizontal, m_text, m_rr); - } -}; - // An object STL, or a modifier volume, over which a different set of parameters shall be applied. // ModelVolume instances are owned by a ModelObject. class ModelVolume final : public ObjectBase @@ -829,7 +800,7 @@ public: bool is_converted_from_meters{ false }; bool is_from_builtin_objects{ false }; - template void serialize(Archive& ar) { + template void serialize(Archive& ar) { //FIXME Vojtech: Serialize / deserialize only if the Source is set. // likely testing input_file or object_idx would be sufficient. ar(input_file, object_idx, volume_idx, mesh_offset, transform, is_converted_from_inches, is_converted_from_meters, is_from_builtin_objects); @@ -883,7 +854,8 @@ 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(); } - // Configuration parameters specific to an object model geometry or a modifier volume, + 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; @@ -903,6 +875,14 @@ public: // List of exterior faces FacetsAnnotation exterior_facets; + // Is set only when volume is Embossed Text type + // Contain information how to re-create volume + std::optional text_configuration; + + // 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; } @@ -913,6 +893,9 @@ 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_text() const { return text_configuration.has_value(); } + bool is_svg() const { return emboss_shape.has_value() && !text_configuration.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(); @@ -968,15 +951,16 @@ public: const Geometry::Transformation& get_transformation() const { return m_transformation; } void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } - void set_transformation(const Transform3d &trafo) { m_transformation.set_from_transform(trafo); } + void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); } + + Vec3d get_offset() const { return m_transformation.get_offset(); } - const Vec3d& get_offset() const { return m_transformation.get_offset(); } double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } - const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } + Vec3d get_rotation() const { return m_transformation.get_rotation(); } double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } @@ -988,7 +972,7 @@ public: void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } - const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } + Vec3d get_mirror() const { return m_transformation.get_mirror(); } double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } bool is_left_handed() const { return m_transformation.is_left_handed(); } @@ -997,10 +981,8 @@ public: void convert_from_imperial_units(); void convert_from_meters(); - void set_text_info(const TextInfo& text_info) { m_text_info = text_info; } - const TextInfo& get_text_info() const { return m_text_info; } - - const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } + const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } + Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } void set_new_unique_id() { ObjectBase::set_new_unique_id(); @@ -1044,8 +1026,6 @@ private: mutable Polygon m_cached_2d_polygon; //BBS, used for convex_hell_2d acceleration Geometry::Transformation m_transformation; - TextInfo m_text_info; - //BBS: add convex_hell_2d related logic void calculate_convex_hull_2d(const Geometry::Transformation &transformation) const; @@ -1100,10 +1080,10 @@ 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) + cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { - assert(this->id().valid()); - assert(this->config.id().valid()); + assert(this->id().valid()); + assert(this->config.id().valid()); assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); @@ -1119,11 +1099,12 @@ private: this->set_material_id(other.material_id()); } // 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) + ModelVolume(ModelObject *object, const ModelVolume &other, TriangleMesh &&mesh) : + name(other.name), source(other.source), config(other.config), object(object), m_mesh(new TriangleMesh(std::move(mesh))), m_type(other.m_type), m_transformation(other.m_transformation), + cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape) { - assert(this->id().valid()); - assert(this->config.id().valid()); + assert(this->id().valid()); + assert(this->config.id().valid()); assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); assert(this->mmu_segmentation_facets.id().valid()); @@ -1135,10 +1116,10 @@ private: assert(this->config.id() == other.config.id()); this->set_material_id(other.material_id()); this->config.set_new_unique_id(); - if (mesh.facets_count() > 1) + if (m_mesh->facets_count() > 1) calculate_convex_hull(); - assert(this->config.id().valid()); - assert(this->config.id() != other.config.id()); + assert(this->config.id().valid()); + assert(this->config.id() != other.config.id()); assert(this->supported_facets.id() != other.supported_facets.id()); assert(this->seam_facets.id() != other.seam_facets.id()); assert(this->mmu_segmentation_facets.id() != other.mmu_segmentation_facets.id()); @@ -1165,9 +1146,8 @@ private: // BBS: add backup, check modify bool mesh_changed = false; 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); + ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull, 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(); @@ -1178,6 +1158,8 @@ 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, text_configuration); + cereal::load(ar, emboss_shape); assert(m_mesh); if (has_convex_hull) { cereal::load_optional(ar, m_convex_hull); @@ -1191,11 +1173,13 @@ private: } template void save(Archive &ar) const { bool has_convex_hull = m_convex_hull.get() != nullptr; - ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull, m_text_info, cut_info); + ar(name, source, m_mesh, m_type, m_material_id, m_transformation, m_is_splittable, has_convex_hull, cut_info); cereal::save_by_value(ar, supported_facets); cereal::save_by_value(ar, seam_facets); cereal::save_by_value(ar, mmu_segmentation_facets); cereal::save_by_value(ar, config); + cereal::save(ar, text_configuration); + cereal::save(ar, emboss_shape); if (has_convex_hull) cereal::save_optional(ar, m_convex_hull); } @@ -1259,7 +1243,7 @@ public: } void set_assemble_from_transform(Transform3d& transform) { m_assemble_initialized = true; - m_assemble_transformation.set_from_transform(transform); + m_assemble_transformation.set_matrix(transform); } void set_assemble_offset(const Vec3d& offset) { m_assemble_transformation.set_offset(offset); } void rotate_assemble(double angle, const Vec3d& axis) { @@ -1270,13 +1254,13 @@ public: void set_offset_to_assembly(const Vec3d& offset) { m_offset_to_assembly = offset; } Vec3d get_offset_to_assembly() const { return m_offset_to_assembly; } - const Vec3d& get_offset() const { return m_transformation.get_offset(); } + Vec3d get_offset() const { return m_transformation.get_offset(); } double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } - const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } + Vec3d get_rotation() const { return m_transformation.get_rotation(); } double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } @@ -1284,38 +1268,36 @@ 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); } - const Vec3d& get_scaling_factor() const { return m_transformation.get_scaling_factor(); } + Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } - const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } + Vec3d get_mirror() const { return m_transformation.get_mirror(); } double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } - bool is_left_handed() const { return m_transformation.is_left_handed(); } + bool is_left_handed() const { return m_transformation.is_left_handed(); } void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } // To be called on an external mesh void transform_mesh(TriangleMesh* mesh, bool dont_translate = false) const; - // Calculate a bounding box of a transformed mesh. To be called on an external mesh. - BoundingBoxf3 transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate = false) const; - // Transform an external bounding box. + // Transform an external bounding box, thus the resulting bounding box is no more snug. BoundingBoxf3 transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate = false) const; // Transform an external vector. Vec3d transform_vector(const Vec3d& v, bool dont_translate = false) const; // To be called on an external polygon. It does not translate the polygon, only rotates and scales. void transform_polygon(Polygon* polygon) const; - const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } + const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } + Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } bool is_printable() const { return object->printable && printable && (print_volume_state == ModelInstancePVS_Inside); } bool is_assemble_initialized() { return m_assemble_initialized; } diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp new file mode 100644 index 000000000..6bfb89b74 --- /dev/null +++ b/src/libslic3r/NSVGUtils.cpp @@ -0,0 +1,547 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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..fe76fa045 --- /dev/null +++ b/src/libslic3r/NSVGUtils.hpp @@ -0,0 +1,86 @@ +///|/ Copyright (c) Prusa Research 2021 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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 09a7398ae..38c813d09 100644 --- a/src/libslic3r/Point.cpp +++ b/src/libslic3r/Point.cpp @@ -59,65 +59,12 @@ Pointf3s transform(const Pointf3s& points, const Transform3d& t) void Point::rotate(double angle, const Point ¢er) { - double cur_x = (double)(*this)(0); - double cur_y = (double)(*this)(1); - double s = ::sin(angle); - double c = ::cos(angle); - double dx = cur_x - (double)center(0); - double dy = cur_y - (double)center(1); - (*this)(0) = (coord_t)round( (double)center(0) + c * dx - s * dy ); - (*this)(1) = (coord_t)round( (double)center(1) + c * dy + s * dx ); -} - -int Point::nearest_point_index(const Points &points) const -{ - PointConstPtrs p; - p.reserve(points.size()); - for (Points::const_iterator it = points.begin(); it != points.end(); ++it) - p.push_back(&*it); - return this->nearest_point_index(p); -} - -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; -} - -int Point::nearest_point_index(const PointPtrs &points) const -{ - PointConstPtrs p; - p.reserve(points.size()); - for (PointPtrs::const_iterator it = points.begin(); it != points.end(); ++it) - p.push_back(*it); - return this->nearest_point_index(p); -} - -bool Point::nearest_point(const Points &points, Point* point) const -{ - int idx = this->nearest_point_index(points); - if (idx == -1) return false; - *point = points.at(idx); - return true; + Vec2d cur = this->cast(); + double s = ::sin(angle); + double c = ::cos(angle); + auto d = cur - center.cast(); + this->x() = fast_round_up(center.x() + c * d.x() - s * d.y()); + this->y() = fast_round_up(center.y() + s * d.x() + c * d.y()); } /* Three points are a counter-clockwise turn if ccw > 0, clockwise if @@ -191,7 +138,7 @@ Point Point::projection_onto(const Line &line) const return ((line.a - *this).cast().squaredNorm() < (line.b - *this).cast().squaredNorm()) ? line.a : line.b; } -bool has_duplicate_points(std::vector &&pts) +bool has_duplicate_points(Points &&pts) { std::sort(pts.begin(), pts.end()); for (size_t i = 1; i < pts.size(); ++ i) @@ -200,6 +147,24 @@ 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) { @@ -231,6 +196,58 @@ BoundingBoxf get_extents(const std::vector &pts) return bbox; } + +int Point::nearest_point_index(const Points &points) const +{ + PointConstPtrs p; + p.reserve(points.size()); + for (Points::const_iterator it = points.begin(); it != points.end(); ++it) + p.push_back(&*it); + return this->nearest_point_index(p); +} + +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; +} + +int Point::nearest_point_index(const PointPtrs &points) const +{ + PointConstPtrs p; + p.reserve(points.size()); + for (PointPtrs::const_iterator it = points.begin(); it != points.end(); ++it) + p.push_back(*it); + return this->nearest_point_index(p); +} + +bool Point::nearest_point(const Points &points, Point* point) const +{ + int idx = this->nearest_point_index(points); + if (idx == -1) return false; + *point = points.at(idx); + return true; +} + std::ostream& operator<<(std::ostream &stm, const Vec2d &pointf) { return stm << pointf(0) << "," << pointf(1); diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 8bb0a6a41..b3498e308 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -53,9 +53,9 @@ using Vec3i64 = Eigen::Matrix; // Vector types with a double coordinate base type. using Vec2f = Eigen::Matrix; using Vec3f = Eigen::Matrix; +using Vec4f = Eigen::Matrix; using Vec2d = Eigen::Matrix; using Vec3d = Eigen::Matrix; -using Vec4f = Eigen::Matrix; using Vec4d = Eigen::Matrix; using Points = std::vector; @@ -116,9 +116,8 @@ inline Eigen::Matrix perp(cons } // Angle from v1 to v2, returning double atan2(y, x) normalized to <-PI, PI>. -template -inline double angle(const Eigen::MatrixBase& v1, const Eigen::MatrixBase& v2) -{ +template +inline double angle(const Eigen::MatrixBase &v1, const Eigen::MatrixBase &v2) { static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "angle(): first parameter is not a 2D vector"); static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "angle(): second parameter is not a 2D vector"); auto v1d = v1.template cast(); @@ -153,6 +152,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 @@ -163,15 +185,16 @@ public: Point() : Vec2crd(0, 0) {} Point(int32_t x, int32_t y) : Vec2crd(coord_t(x), coord_t(y)) {} Point(int64_t x, int64_t y) : Vec2crd(coord_t(x), coord_t(y)) {} - Point(double x, double y) : Vec2crd(coord_t(lrint(x)), coord_t(lrint(y))) {} + Point(double x, double y) : Vec2crd(coord_t(std::round(x)), coord_t(std::round(y))) {} Point(const Point &rhs) { *this = rhs; } - explicit Point(const Vec2d& rhs) : Vec2crd(coord_t(lrint(rhs.x())), coord_t(lrint(rhs.y()))) {} + explicit Point(const Vec2d& rhs) : Vec2crd(coord_t(std::round(rhs.x())), coord_t(std::round(rhs.y()))) {} // This constructor allows you to construct Point from Eigen expressions + // This constructor has to be implicit (non-explicit) to allow implicit conversion from Eigen expressions. template Point(const Eigen::MatrixBase &other) : Vec2crd(other) {} static Point new_scale(coordf_t x, coordf_t y) { return Point(coord_t(scale_(x)), coord_t(scale_(y))); } - static Point new_scale(const Vec2d &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } - static Point new_scale(const Vec2f &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } + template + static Point new_scale(const Eigen::MatrixBase &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } // This method allows you to assign Eigen expressions to MyVectorType template @@ -297,16 +320,16 @@ BoundingBoxf get_extents(const std::vector &pts); // Test for duplicate points in a vector of points. // The points are copied, sorted and checked for duplicates globally. -bool has_duplicate_points(std::vector &&pts); -inline bool has_duplicate_points(const std::vector &pts) +bool has_duplicate_points(Points &&pts); +inline bool has_duplicate_points(const Points &pts) { - std::vector cpy = pts; + Points cpy = pts; return has_duplicate_points(std::move(cpy)); } // Test for duplicate points in a vector of points. // Only successive points are checked for equality. -inline bool has_duplicate_successive_points(const std::vector &pts) +inline bool has_duplicate_successive_points(const Points &pts) { for (size_t i = 1; i < pts.size(); ++ i) if (pts[i - 1] == pts[i]) @@ -316,11 +339,14 @@ inline bool has_duplicate_successive_points(const std::vector &pts) // Test for duplicate points in a vector of points. // Only successive points are checked for equality. Additionally, first and last points are compared for equality. -inline bool has_duplicate_successive_points_closed(const std::vector &pts) +inline bool has_duplicate_successive_points_closed(const Points &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) @@ -341,7 +367,7 @@ namespace int128 { // To be used by std::unordered_map, std::unordered_multimap and friends. struct PointHash { - size_t operator()(const Vec2crd &pt) const { + size_t operator()(const Vec2crd &pt) const noexcept { return coord_t((89 * 31 + int64_t(pt.x())) * 31 + pt.y()); } }; @@ -570,6 +596,27 @@ inline coord_t align_to_grid(coord_t coord, coord_t spacing, coord_t base) 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 diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index 18a20586a..697def1f5 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -1,3 +1,14 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Vojtěch Bubník @bubnikv, Filip Sykala @Jony01, Lukáš Matěna @lukasmatena, Tomáš Mészáros @tamasmeszaros, Enrico Turri @enricoturri1966 +///|/ Copyright (c) Slic3r 2013 - 2015 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2014 Petr Ledvina @ledvinap +///|/ +///|/ ported from lib/Slic3r/Polygon.pm: +///|/ Copyright (c) Prusa Research 2017 - 2022 Vojtěch Bubník @bubnikv +///|/ Copyright (c) Slic3r 2011 - 2014 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2012 Mark Hindess +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "Exception.hpp" @@ -454,6 +465,38 @@ 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; diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index d09d7bae7..848e37c0a 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -1,3 +1,13 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Tomáš Mészáros @tamasmeszaros, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Lukáš Hejl @hejllukas, Filip Sykala @Jony01, Oleksandra Iushchenko @YuSanka +///|/ Copyright (c) Slic3r 2013 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ ported from lib/Slic3r/Polygon.pm: +///|/ Copyright (c) Prusa Research 2017 - 2022 Vojtěch Bubník @bubnikv +///|/ Copyright (c) Slic3r 2011 - 2014 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2012 Mark Hindess +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_Polygon_hpp_ #define slic3r_Polygon_hpp_ @@ -5,6 +15,7 @@ #include #include #include "Line.hpp" +#include "Point.hpp" #include "MultiPoint.hpp" #include "Polyline.hpp" @@ -112,6 +123,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) @@ -245,7 +260,19 @@ inline Polylines to_polylines(Polygons &&polys) return polylines; } -inline Polygons to_polygons(const std::vector &paths) +// 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 VecOfPoints &paths) { Polygons out; out.reserve(paths.size()); @@ -254,7 +281,7 @@ inline Polygons to_polygons(const std::vector &paths) return out; } -inline Polygons to_polygons(std::vector &&paths) +inline Polygons to_polygons(VecOfPoints &&paths) { Polygons out; out.reserve(paths.size()); @@ -270,6 +297,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 diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp index a9bf48f50..2e867b0df 100644 --- a/src/libslic3r/Polyline.cpp +++ b/src/libslic3r/Polyline.cpp @@ -497,6 +497,33 @@ 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()) diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index 8879988bd..580e463d1 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -164,6 +164,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) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 9804dd45c..6664b28a3 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -839,7 +839,7 @@ void update_volume_bboxes( layer_range.volumes.emplace_back(*it); } else layer_range.volumes.push_back({ model_volume->id(), - transformed_its_bbox2d(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix(false)), offset) }); + transformed_its_bbox2d(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix()), offset) }); } } else { std::vector> volumes_old; @@ -871,7 +871,7 @@ void update_volume_bboxes( layer_range.volumes.emplace_back(*it); } } else { - transformed_its_bboxes_in_z_ranges(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix(false)), ranges, bboxes, offset); + transformed_its_bboxes_in_z_ranges(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix()), ranges, bboxes, offset); for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) if (auto &bbox = bboxes[&layer_range - layer_ranges.data()]; bbox.second) layer_range.volumes.push_back({ model_volume->id(), bbox.first }); diff --git a/src/libslic3r/SLA/AGGRaster.hpp b/src/libslic3r/SLA/AGGRaster.hpp index 2243a3c1b..8d86962fc 100644 --- a/src/libslic3r/SLA/AGGRaster.hpp +++ b/src/libslic3r/SLA/AGGRaster.hpp @@ -1,9 +1,12 @@ +///|/ Copyright (c) Prusa Research 2020 - 2022 Tomáš Mészáros @tamasmeszaros, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef AGGRASTER_HPP #define AGGRASTER_HPP #include #include "libslic3r/ExPolygon.hpp" -#include "libslic3r/MTUtils.hpp" // For rasterizing #include @@ -42,7 +45,7 @@ public: using TValue = typename TColor::value_type; using TPixel = typename PixelRenderer::pixel_type; using TRawBuffer = agg::rendering_buffer; - + protected: Resolution m_resolution; @@ -154,8 +157,8 @@ public: } Trafo trafo() const override { return m_trafo; } - Resolution resolution() const override { return m_resolution; } - PixelDim pixel_dimensions() const override + Resolution resolution() const { return m_resolution; } + PixelDim pixel_dimensions() const { return {SCALING_FACTOR / m_pxdim_scaled.w_mm, SCALING_FACTOR / m_pxdim_scaled.h_mm}; @@ -187,11 +190,15 @@ class RasterGrayscaleAA : public _RasterGrayscaleAA { using typename Base::TValue; public: template - RasterGrayscaleAA(const RasterBase::Resolution &res, - const RasterBase::PixelDim & pd, - const RasterBase::Trafo & trafo, - GammaFn && fn) - : Base(res, pd, trafo, Colors::White, Colors::Black, + RasterGrayscaleAA(const Resolution &res, + const PixelDim &pd, + const RasterBase::Trafo &trafo, + GammaFn &&fn) + : Base(res, + pd, + trafo, + Colors::White, + Colors::Black, std::forward(fn)) {} @@ -209,10 +216,10 @@ public: class RasterGrayscaleAAGammaPower: public RasterGrayscaleAA { public: - RasterGrayscaleAAGammaPower(const RasterBase::Resolution &res, - const RasterBase::PixelDim & pd, - const RasterBase::Trafo & trafo, - double gamma = 1.) + RasterGrayscaleAAGammaPower(const Resolution &res, + const PixelDim &pd, + const RasterBase::Trafo &trafo, + double gamma = 1.) : RasterGrayscaleAA(res, pd, trafo, agg::gamma_power(gamma)) {} }; diff --git a/src/libslic3r/SLA/RasterBase.cpp b/src/libslic3r/SLA/RasterBase.cpp index 581e84880..9f599e048 100644 --- a/src/libslic3r/SLA/RasterBase.cpp +++ b/src/libslic3r/SLA/RasterBase.cpp @@ -1,3 +1,8 @@ +///|/ Copyright (c) Prusa Research 2020 - 2022 Tomáš Mészáros @tamasmeszaros +///|/ Copyright (c) 2022 ole00 @ole00 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef SLARASTER_CPP #define SLARASTER_CPP @@ -11,11 +16,6 @@ namespace Slic3r { namespace sla { -const RasterBase::TMirroring RasterBase::NoMirror = {false, false}; -const RasterBase::TMirroring RasterBase::MirrorX = {true, false}; -const RasterBase::TMirroring RasterBase::MirrorY = {false, true}; -const RasterBase::TMirroring RasterBase::MirrorXY = {true, true}; - EncodedRaster PNGRasterEncoder::operator()(const void *ptr, size_t w, size_t h, size_t num_components) { @@ -68,15 +68,17 @@ EncodedRaster PPMRasterEncoder::operator()(const void *ptr, size_t w, size_t h, } std::unique_ptr create_raster_grayscale_aa( - const RasterBase::Resolution &res, - const RasterBase::PixelDim & pxdim, - double gamma, - const RasterBase::Trafo & tr) + const Resolution &res, + const PixelDim &pxdim, + double gamma, + const RasterBase::Trafo &tr) { std::unique_ptr rst; if (gamma > 0) rst = std::make_unique(res, pxdim, tr, gamma); + else if (std::abs(gamma - 1.) < 1e-6) + rst = std::make_unique(res, pxdim, tr, agg::gamma_none()); else rst = std::make_unique(res, pxdim, tr, agg::gamma_threshold(.5)); diff --git a/src/libslic3r/SLA/RasterBase.hpp b/src/libslic3r/SLA/RasterBase.hpp index 1eba360e3..5f2826aec 100644 --- a/src/libslic3r/SLA/RasterBase.hpp +++ b/src/libslic3r/SLA/RasterBase.hpp @@ -1,3 +1,8 @@ +///|/ Copyright (c) Prusa Research 2020 - 2022 Tomáš Mészáros @tamasmeszaros, Vojtěch Bubník @bubnikv +///|/ Copyright (c) 2022 ole00 @ole00 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef SLA_RASTERBASE_HPP #define SLA_RASTERBASE_HPP @@ -9,7 +14,6 @@ #include #include -#include namespace Slic3r { @@ -31,6 +35,27 @@ public: const char * extension() const { return m_ext.c_str(); } }; +/// Type that represents a resolution in pixels. +struct Resolution { + size_t width_px = 0; + size_t height_px = 0; + + Resolution() = default; + Resolution(size_t w, size_t h) : width_px(w), height_px(h) {} + size_t pixels() const { return width_px * height_px; } +}; + +/// Types that represents the dimension of a pixel in millimeters. +struct PixelDim { + double w_mm = 1.; + double h_mm = 1.; + + PixelDim() = default; + PixelDim(double px_width_mm, double px_height_mm) + : w_mm(px_width_mm), h_mm(px_height_mm) + {} +}; + using RasterEncoder = std::function; @@ -40,10 +65,10 @@ public: enum Orientation { roLandscape, roPortrait }; using TMirroring = std::array; - static const TMirroring NoMirror; - static const TMirroring MirrorX; - static const TMirroring MirrorY; - static const TMirroring MirrorXY; + static const constexpr TMirroring NoMirror = {false, false}; + static const constexpr TMirroring MirrorX = {true, false}; + static const constexpr TMirroring MirrorY = {false, true}; + static const constexpr TMirroring MirrorXY = {true, true}; struct Trafo { bool mirror_x = false, mirror_y = false, flipXY = false; @@ -63,35 +88,14 @@ public: Point get_center() const { return {center_x, center_y}; } }; - /// Type that represents a resolution in pixels. - struct Resolution { - size_t width_px = 0; - size_t height_px = 0; - - Resolution() = default; - Resolution(size_t w, size_t h) : width_px(w), height_px(h) {} - size_t pixels() const { return width_px * height_px; } - }; - - /// Types that represents the dimension of a pixel in millimeters. - struct PixelDim { - double w_mm = 1.; - double h_mm = 1.; - - PixelDim() = default; - PixelDim(double px_width_mm, double px_height_mm) - : w_mm(px_width_mm), h_mm(px_height_mm) - {} - }; - virtual ~RasterBase() = default; /// Draw a polygon with holes. virtual void draw(const ExPolygon& poly) = 0; /// Get the resolution of the raster. - virtual Resolution resolution() const = 0; - virtual PixelDim pixel_dimensions() const = 0; +// virtual Resolution resolution() const = 0; +// virtual PixelDim pixel_dimensions() const = 0; virtual Trafo trafo() const = 0; virtual EncodedRaster encode(RasterEncoder encoder) const = 0; @@ -109,10 +113,10 @@ std::ostream& operator<<(std::ostream &stream, const EncodedRaster &bytes); // If gamma is zero, thresholding will be performed which disables AA. std::unique_ptr create_raster_grayscale_aa( - const RasterBase::Resolution &res, - const RasterBase::PixelDim & pxdim, - double gamma = 1.0, - const RasterBase::Trafo & tr = {}); + const Resolution &res, + const PixelDim &pxdim, + double gamma = 1.0, + const RasterBase::Trafo &tr = {}); }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index 3fa6757d5..887942c9c 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -6,6 +6,7 @@ #include "PrintBase.hpp" #include "SLA/RasterBase.hpp" #include "SLA/SupportTree.hpp" +#include "Execution/ExecutionTBB.hpp" #include "Point.hpp" #include "MTUtils.hpp" #include "Zipper.hpp" diff --git a/src/libslic3r/TextConfiguration.hpp b/src/libslic3r/TextConfiguration.hpp new file mode 100644 index 000000000..8da67cdd0 --- /dev/null +++ b/src/libslic3r/TextConfiguration.hpp @@ -0,0 +1,191 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Filip Sykala @Jony01, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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..18dd844bc --- /dev/null +++ b/src/libslic3r/Timer.cpp @@ -0,0 +1,25 @@ +///|/ Copyright (c) Prusa Research 2023 Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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..2fd63f234 --- /dev/null +++ b/src/libslic3r/Timer.hpp @@ -0,0 +1,96 @@ +///|/ Copyright (c) Prusa Research 2023 Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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..c75a53197 --- /dev/null +++ b/src/libslic3r/Triangulation.cpp @@ -0,0 +1,332 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Filip Sykala @Jony01, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "Triangulation.hpp" +#include "IntersectionPoints.hpp" +#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 (Vec3i &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..1fb7b6782 --- /dev/null +++ b/src/libslic3r/Triangulation.hpp @@ -0,0 +1,76 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Vojtěch Bubník @bubnikv, Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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 8687a503e..2a261476c 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -372,6 +372,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 ececd0b15..88f78f22d 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -1225,6 +1225,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/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 8b40dc1e4..7d8e060ea 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -124,6 +124,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmosCommon.hpp GUI/Gizmos/GLGizmoBase.cpp GUI/Gizmos/GLGizmoBase.hpp + GUI/Gizmos/GLGizmoEmboss.cpp + GUI/Gizmos/GLGizmoEmboss.hpp GUI/Gizmos/GLGizmoMove.cpp GUI/Gizmos/GLGizmoMove.hpp GUI/Gizmos/GLGizmoRotate.cpp @@ -144,6 +146,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoPainterBase.hpp GUI/Gizmos/GLGizmoSimplify.cpp GUI/Gizmos/GLGizmoSimplify.hpp + GUI/Gizmos/GLGizmoSVG.cpp + GUI/Gizmos/GLGizmoSVG.hpp GUI/Gizmos/GLGizmoMmuSegmentation.cpp GUI/Gizmos/GLGizmoMmuSegmentation.hpp #GUI/Gizmos/GLGizmoFaceDetector.cpp @@ -152,8 +156,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoMeasure.hpp GUI/Gizmos/GLGizmoSeam.cpp GUI/Gizmos/GLGizmoSeam.hpp - GUI/Gizmos/GLGizmoText.cpp - GUI/Gizmos/GLGizmoText.hpp + #GUI/Gizmos/GLGizmoText.cpp + #GUI/Gizmos/GLGizmoText.hpp GUI/Gizmos/GLGizmoMeshBoolean.cpp GUI/Gizmos/GLGizmoMeshBoolean.hpp GUI/GLSelectionRectangle.cpp @@ -194,6 +198,8 @@ set(SLIC3R_GUI_SOURCES GUI/GUI_Geometry.hpp GUI/I18N.cpp GUI/I18N.hpp + GUI/IconManager.cpp + GUI/IconManager.hpp GUI/MainFrame.cpp GUI/MainFrame.hpp GUI/BBLTopbar.cpp @@ -303,6 +309,10 @@ set(SLIC3R_GUI_SOURCES GUI/RemovableDriveManager.hpp GUI/SendSystemInfoDialog.cpp GUI/SendSystemInfoDialog.hpp + GUI/SurfaceDrag.cpp + GUI/SurfaceDrag.hpp + GUI/TextLines.cpp + GUI/TextLines.hpp GUI/PlateSettingsDialog.cpp GUI/PlateSettingsDialog.hpp GUI/ImGuiWrapper.hpp @@ -329,13 +339,21 @@ set(SLIC3R_GUI_SOURCES GUI/UpdateDialogs.cpp GUI/UpdateDialogs.hpp GUI/Jobs/Job.hpp - GUI/Jobs/Job.cpp - GUI/Jobs/PlaterJob.hpp - GUI/Jobs/PlaterJob.cpp + GUI/Jobs/Worker.hpp + GUI/Jobs/BoostThreadWorker.hpp + GUI/Jobs/BoostThreadWorker.cpp + GUI/Jobs/BusyCursorJob.hpp + GUI/Jobs/PlaterWorker.hpp GUI/Jobs/UpgradeNetworkJob.hpp GUI/Jobs/UpgradeNetworkJob.cpp GUI/Jobs/ArrangeJob.hpp GUI/Jobs/ArrangeJob.cpp + GUI/Jobs/CreateFontNameImageJob.cpp + GUI/Jobs/CreateFontNameImageJob.hpp + GUI/Jobs/CreateFontStyleImagesJob.cpp + GUI/Jobs/CreateFontStyleImagesJob.hpp + GUI/Jobs/EmbossJob.cpp + GUI/Jobs/EmbossJob.hpp GUI/Jobs/OrientJob.hpp GUI/Jobs/OrientJob.cpp GUI/Jobs/RotoptimizeJob.hpp @@ -353,6 +371,8 @@ set(SLIC3R_GUI_SOURCES GUI/Jobs/BindJob.cpp GUI/Jobs/NotificationProgressIndicator.hpp GUI/Jobs/NotificationProgressIndicator.cpp + GUI/Jobs/ThreadSafeQueue.hpp + GUI/Jobs/SLAImportDialog.hpp GUI/PhysicalPrinterDialog.hpp GUI/PhysicalPrinterDialog.cpp GUI/ProgressStatusBar.hpp @@ -449,6 +469,10 @@ set(SLIC3R_GUI_SOURCES Utils/Http.hpp Utils/FixModelByWin10.cpp Utils/FixModelByWin10.hpp + Utils/EmbossStyleManager.cpp + Utils/EmbossStyleManager.hpp + Utils/FontConfigHelp.cpp + Utils/FontConfigHelp.hpp Utils/Bonjour.cpp Utils/Bonjour.hpp Utils/FileHelp.cpp @@ -457,6 +481,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 @@ -480,8 +506,10 @@ set(SLIC3R_GUI_SOURCES Utils/PrintHost.hpp Utils/Serial.cpp Utils/Serial.hpp - Utils/MKS.hpp Utils/MKS.cpp + Utils/MKS.hpp + Utils/WxFontUtils.cpp + Utils/WxFontUtils.hpp Utils/Duet.cpp Utils/Duet.hpp Utils/FlashAir.cpp @@ -591,7 +619,7 @@ endif () if (UNIX AND NOT APPLE) find_package(GTK${SLIC3R_GTK} REQUIRED) target_include_directories(libslic3r_gui PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS}) - target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES}) + target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig) # We add GStreamer for bambu:/// support. pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0) diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index f407a999a..cb7ff02fd 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -940,9 +940,9 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disab const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); //BBS: add outline related logic - if (with_outline && volume.first->selected) - volume.first->render_with_outline(view_matrix * model_matrix); - else + //if (with_outline && volume.first->selected) + // volume.first->render_with_outline(view_matrix * model_matrix); + //else volume.first->render(); #if ENABLE_ENVIRONMENT_MAP diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index c3f0d95b2..a67127739 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -243,14 +243,15 @@ public: const Geometry::Transformation& get_instance_transformation() const { return m_instance_transformation; } void set_instance_transformation(const Geometry::Transformation& transformation) { m_instance_transformation = transformation; set_bounding_boxes_as_dirty(); } + void set_instance_transformation(const Transform3d& transform) { m_instance_transformation.set_matrix(transform); set_bounding_boxes_as_dirty(); } - const Vec3d& get_instance_offset() const { return m_instance_transformation.get_offset(); } + Vec3d get_instance_offset() const { return m_instance_transformation.get_offset(); } double get_instance_offset(Axis axis) const { return m_instance_transformation.get_offset(axis); } void set_instance_offset(const Vec3d& offset) { m_instance_transformation.set_offset(offset); set_bounding_boxes_as_dirty(); } void set_instance_offset(Axis axis, double offset) { m_instance_transformation.set_offset(axis, offset); set_bounding_boxes_as_dirty(); } - const Vec3d& get_instance_rotation() const { return m_instance_transformation.get_rotation(); } + Vec3d get_instance_rotation() const { return m_instance_transformation.get_rotation(); } double get_instance_rotation(Axis axis) const { return m_instance_transformation.get_rotation(axis); } void set_instance_rotation(const Vec3d& rotation) { m_instance_transformation.set_rotation(rotation); set_bounding_boxes_as_dirty(); } @@ -262,7 +263,7 @@ public: void set_instance_scaling_factor(const Vec3d& scaling_factor) { m_instance_transformation.set_scaling_factor(scaling_factor); set_bounding_boxes_as_dirty(); } void set_instance_scaling_factor(Axis axis, double scaling_factor) { m_instance_transformation.set_scaling_factor(axis, scaling_factor); set_bounding_boxes_as_dirty(); } - const Vec3d& get_instance_mirror() const { return m_instance_transformation.get_mirror(); } + Vec3d get_instance_mirror() const { return m_instance_transformation.get_mirror(); } double get_instance_mirror(Axis axis) const { return m_instance_transformation.get_mirror(axis); } void set_instance_mirror(const Vec3d& mirror) { m_instance_transformation.set_mirror(mirror); set_bounding_boxes_as_dirty(); } @@ -270,26 +271,27 @@ public: const Geometry::Transformation& get_volume_transformation() const { return m_volume_transformation; } void set_volume_transformation(const Geometry::Transformation& transformation) { m_volume_transformation = transformation; set_bounding_boxes_as_dirty(); } + void set_volume_transformation(const Transform3d& transform) { m_volume_transformation.set_matrix(transform); set_bounding_boxes_as_dirty(); } - const Vec3d& get_volume_offset() const { return m_volume_transformation.get_offset(); } + Vec3d get_volume_offset() const { return m_volume_transformation.get_offset(); } double get_volume_offset(Axis axis) const { return m_volume_transformation.get_offset(axis); } void set_volume_offset(const Vec3d& offset) { m_volume_transformation.set_offset(offset); set_bounding_boxes_as_dirty(); } void set_volume_offset(Axis axis, double offset) { m_volume_transformation.set_offset(axis, offset); set_bounding_boxes_as_dirty(); } - const Vec3d& get_volume_rotation() const { return m_volume_transformation.get_rotation(); } + Vec3d get_volume_rotation() const { return m_volume_transformation.get_rotation(); } double get_volume_rotation(Axis axis) const { return m_volume_transformation.get_rotation(axis); } void set_volume_rotation(const Vec3d& rotation) { m_volume_transformation.set_rotation(rotation); set_bounding_boxes_as_dirty(); } void set_volume_rotation(Axis axis, double rotation) { m_volume_transformation.set_rotation(axis, rotation); set_bounding_boxes_as_dirty(); } - const Vec3d& get_volume_scaling_factor() const { return m_volume_transformation.get_scaling_factor(); } + Vec3d get_volume_scaling_factor() const { return m_volume_transformation.get_scaling_factor(); } double get_volume_scaling_factor(Axis axis) const { return m_volume_transformation.get_scaling_factor(axis); } void set_volume_scaling_factor(const Vec3d& scaling_factor) { m_volume_transformation.set_scaling_factor(scaling_factor); set_bounding_boxes_as_dirty(); } void set_volume_scaling_factor(Axis axis, double scaling_factor) { m_volume_transformation.set_scaling_factor(axis, scaling_factor); set_bounding_boxes_as_dirty(); } - const Vec3d& get_volume_mirror() const { return m_volume_transformation.get_mirror(); } + Vec3d get_volume_mirror() const { return m_volume_transformation.get_mirror(); } double get_volume_mirror(Axis axis) const { return m_volume_transformation.get_mirror(axis); } void set_volume_mirror(const Vec3d& mirror) { m_volume_transformation.set_mirror(mirror); set_bounding_boxes_as_dirty(); } diff --git a/src/slic3r/GUI/BindDialog.cpp b/src/slic3r/GUI/BindDialog.cpp index ca0d4fd70..dd4e826ec 100644 --- a/src/slic3r/GUI/BindDialog.cpp +++ b/src/slic3r/GUI/BindDialog.cpp @@ -13,6 +13,8 @@ #include "MainFrame.hpp" #include "GUI_App.hpp" #include "Plater.hpp" +#include "Jobs/BoostThreadWorker.hpp" +#include "Jobs/PlaterWorker.hpp" #include "Widgets/WebView.hpp" namespace Slic3r { @@ -374,6 +376,8 @@ wxString get_fail_reason(int code) m_status_bar = std::make_shared(m_simplebook); + m_worker = std::make_unique>(this, m_status_bar, "bind_worker"); + auto button_panel = new wxPanel(m_simplebook, wxID_ANY, wxDefaultPosition, BIND_DIALOG_BUTTON_PANEL_SIZE); button_panel->SetBackgroundColour(*wxWHITE); wxBoxSizer *m_sizer_button = new wxBoxSizer(wxHORIZONTAL); @@ -513,10 +517,7 @@ wxString get_fail_reason(int code) void BindMachineDialog::on_destroy() { - if (m_bind_job) { - m_bind_job->cancel(); - m_bind_job->join(); - } + m_worker.get()->cancel_all(); } void BindMachineDialog::on_close(wxCloseEvent &event) @@ -572,7 +573,7 @@ wxString get_fail_reason(int code) agent->track_update_property("dev_ota_version", m_machine_info->get_ota_version()); m_simplebook->SetSelection(0); - m_bind_job = std::make_shared(m_status_bar, wxGetApp().plater(), m_machine_info->dev_id, m_machine_info->dev_ip, m_machine_info->bind_sec_link); + auto m_bind_job = std::make_unique(m_machine_info->dev_id, m_machine_info->dev_ip, m_machine_info->bind_sec_link); if (m_machine_info && (m_machine_info->get_printer_series() == PrinterSeries::SERIES_X1)) { m_bind_job->set_improved(false); @@ -582,7 +583,7 @@ wxString get_fail_reason(int code) } m_bind_job->set_event_handle(this); - m_bind_job->start(); + replace_job(*m_worker, std::move(m_bind_job)); } void BindMachineDialog::on_dpi_changed(const wxRect &suggested_rect) diff --git a/src/slic3r/GUI/BindDialog.hpp b/src/slic3r/GUI/BindDialog.hpp index 8a8b878e3..636d08811 100644 --- a/src/slic3r/GUI/BindDialog.hpp +++ b/src/slic3r/GUI/BindDialog.hpp @@ -28,6 +28,7 @@ #include "Jobs/BindJob.hpp" #include "BBLStatusBar.hpp" #include "BBLStatusBarBind.hpp" +#include "Jobs/Worker.hpp" #define BIND_DIALOG_GREY200 wxColour(248, 248, 248) #define BIND_DIALOG_GREY800 wxColour(50, 58, 61) @@ -77,8 +78,8 @@ private: std::shared_ptr m_tocken; MachineObject * m_machine_info{nullptr}; - std::shared_ptr m_bind_job; std::shared_ptr m_status_bar; + std::unique_ptr m_worker; public: BindMachineDialog(Plater *plater = nullptr); diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index bf60b50aa..23e611f43 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -1437,12 +1437,10 @@ void CalibrationPresetPage::on_cali_finished_job() void CalibrationPresetPage::on_cali_cancel_job() { BOOST_LOG_TRIVIAL(info) << "CalibrationWizard::print_job: enter canceled"; - if (CalibUtils::print_job) { - if (CalibUtils::print_job->is_running()) { - BOOST_LOG_TRIVIAL(info) << "calibration_print_job: canceled"; - CalibUtils::print_job->cancel(); - } - CalibUtils::print_job->join(); + if (CalibUtils::print_worker) { + BOOST_LOG_TRIVIAL(info) << "calibration_print_job: canceled"; + CalibUtils::print_worker->cancel_all(); + CalibUtils::print_worker->wait_for_idle(); } m_sending_panel->reset(); diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.cpp b/src/slic3r/GUI/CalibrationWizardSavePage.cpp index 42828fb90..2c8f6c646 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.cpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.cpp @@ -1396,12 +1396,10 @@ void CalibrationFlowCoarseSavePage::on_cali_finished_job() void CalibrationFlowCoarseSavePage::on_cali_cancel_job() { BOOST_LOG_TRIVIAL(info) << "CalibrationWizard::print_job: enter canceled"; - if (CalibUtils::print_job) { - if (CalibUtils::print_job->is_running()) { - BOOST_LOG_TRIVIAL(info) << "calibration_print_job: canceled"; - CalibUtils::print_job->cancel(); - } - CalibUtils::print_job->join(); + if (CalibUtils::print_worker) { + BOOST_LOG_TRIVIAL(info) << "calibration_print_job: canceled"; + CalibUtils::print_worker->cancel_all(); + CalibUtils::print_worker->wait_for_idle(); } m_sending_panel->reset(); diff --git a/src/slic3r/GUI/DownloadProgressDialog.cpp b/src/slic3r/GUI/DownloadProgressDialog.cpp index 4a80a38cb..542db0856 100644 --- a/src/slic3r/GUI/DownloadProgressDialog.cpp +++ b/src/slic3r/GUI/DownloadProgressDialog.cpp @@ -20,6 +20,8 @@ #include "wxExtensions.hpp" #include "slic3r/GUI/MainFrame.hpp" #include "GUI_App.hpp" +#include "Jobs/BoostThreadWorker.hpp" +#include "Jobs/PlaterWorker.hpp" #define DESIGN_INPUT_SIZE wxSize(FromDIP(100), -1) @@ -59,7 +61,8 @@ DownloadProgressDialog::DownloadProgressDialog(wxString title) m_panel_download->SetSize(wxSize(FromDIP(400), FromDIP(70))); m_panel_download->SetMinSize(wxSize(FromDIP(400), FromDIP(70))); m_panel_download->SetMaxSize(wxSize(FromDIP(400), FromDIP(70))); - + + m_worker = std::make_unique>(this, m_status_bar, "download_worker"); //mode Download Failed auto m_panel_download_failed = new wxPanel(m_simplebook_status, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); @@ -144,7 +147,7 @@ bool DownloadProgressDialog::Show(bool show) { if (show) { m_simplebook_status->SetSelection(0); - m_upgrade_job = make_job(m_status_bar); + auto m_upgrade_job = make_job(); m_upgrade_job->set_event_handle(this); m_status_bar->set_progress(0); Bind(EVT_UPGRADE_NETWORK_SUCCESS, [this](wxCommandEvent& evt) { @@ -182,23 +185,17 @@ bool DownloadProgressDialog::Show(bool show) }); m_status_bar->set_cancel_callback_fina([this]() { - if (m_upgrade_job) { - m_upgrade_job->cancel(); - //EndModal(wxID_CLOSE); - } - + m_worker->cancel_all(); }); - m_upgrade_job->start(); + + replace_job(*m_worker, std::move(m_upgrade_job)); } return DPIDialog::Show(show); } void DownloadProgressDialog::on_close(wxCloseEvent& event) { - if (m_upgrade_job) { - m_upgrade_job->cancel(); - m_upgrade_job->join(); - } + m_worker.get()->cancel_all(); event.Skip(); } @@ -208,7 +205,7 @@ void DownloadProgressDialog::on_dpi_changed(const wxRect &suggested_rect) {} void DownloadProgressDialog::update_release_note(std::string release_note, std::string version) {} -std::shared_ptr DownloadProgressDialog::make_job(std::shared_ptr pri) { return std::make_shared(pri); } +std::unique_ptr DownloadProgressDialog::make_job() { return std::make_unique(); } void DownloadProgressDialog::on_finish() { wxGetApp().restart_networking(); } diff --git a/src/slic3r/GUI/DownloadProgressDialog.hpp b/src/slic3r/GUI/DownloadProgressDialog.hpp index 938d2706a..fa23be524 100644 --- a/src/slic3r/GUI/DownloadProgressDialog.hpp +++ b/src/slic3r/GUI/DownloadProgressDialog.hpp @@ -16,6 +16,7 @@ #include "Widgets/Button.hpp" #include "BBLStatusBar.hpp" #include "BBLStatusBarSend.hpp" +#include "Jobs/Worker.hpp" #include "Jobs/UpgradeNetworkJob.hpp" class wxBoxSizer; @@ -47,11 +48,11 @@ public: wxSimplebook* m_simplebook_status{nullptr}; std::shared_ptr m_status_bar; - std::shared_ptr m_upgrade_job { nullptr }; + std::unique_ptr m_worker; wxPanel * m_panel_download; protected: - virtual std::shared_ptr make_job(std::shared_ptr pri); + virtual std::unique_ptr make_job(); virtual void on_finish(); }; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index aedc729bc..e703a4051 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -361,7 +361,7 @@ void GCodeViewer::SequentialView::Marker::render(int canvas_width, int canvas_he std::string layer_time = ImGui::ColorMarkerStart + _u8L("Layer Time: ") + ImGui::ColorMarkerEnd; std::string fanspeed = ImGui::ColorMarkerStart + _u8L("Fan: ") + ImGui::ColorMarkerEnd; std::string temperature = ImGui::ColorMarkerStart + _u8L("Temperature: ") + ImGui::ColorMarkerEnd; - const float item_size = imgui.calc_text_size("X: 000.000 ").x; + const float item_size = imgui.calc_text_size(std::string_view{"X: 000.000 "}).x; const float item_spacing = imgui.get_item_spacing().x; const float window_padding = ImGui::GetStyle().WindowPadding.x; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 187221838..a82ea0264 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1588,7 +1588,6 @@ void GLCanvas3D::enable_legend_texture(bool enable) void GLCanvas3D::enable_picking(bool enable) { m_picking_enabled = enable; - m_selection.set_mode(Selection::Instance); } void GLCanvas3D::enable_moving(bool enable) @@ -2171,7 +2170,17 @@ std::vector GLCanvas3D::load_object(const Model& model, int obj_idx) void GLCanvas3D::mirror_selection(Axis axis) { - m_selection.mirror(axis); + TransformationType transformation_type; + if (wxGetApp().obj_manipul()->is_local_coordinates()) + transformation_type.set_local(); + else if (wxGetApp().obj_manipul()->is_instance_coordinates()) + transformation_type.set_instance(); + + transformation_type.set_relative(); + + m_selection.setup_cache(); + m_selection.mirror(axis, transformation_type); + do_mirror(L("Mirror Object")); // BBS //wxGetApp().obj_manipul()->set_dirty(); @@ -3364,7 +3373,9 @@ void GLCanvas3D::on_key(wxKeyEvent& evt) else displacement = multiplier * direction; - m_selection.translate(displacement); + TransformationType trafo_type; + trafo_type.set_relative(); + m_selection.translate(displacement, trafo_type); m_dirty = true; } );} @@ -4136,7 +4147,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } } - m_selection.translate(cur_pos - m_mouse.drag.start_position_3D); + TransformationType trafo_type; + trafo_type.set_relative(); + m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type); if (current_printer_technology() == ptFFF && (fff_print()->config().print_sequence == PrintSequence::ByObject)) update_sequential_clearance(); // BBS @@ -4364,6 +4377,40 @@ 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::Move || + type == GLGizmosManager::EType::Rotate || + type == GLGizmosManager::EType::Scale || + type == GLGizmosManager::EType::Emboss || + 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(); @@ -4460,6 +4507,9 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) int instance_idx = v->instance_idx(); int volume_idx = v->volume_idx(); + if (volume_idx < 0) + continue; + std::pair done_id(object_idx, instance_idx); if (0 <= object_idx && object_idx < (int)m_model->objects.size()) { @@ -4469,10 +4519,10 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { if (selection_mode == Selection::Instance) - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); else if (selection_mode == Selection::Volume) { - if (model_object->volumes[volume_idx]->get_offset() != v->get_volume_offset()) { - model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); + if (model_object->volumes[volume_idx]->get_transformation() != v->get_volume_transformation()) { + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); // BBS: backup Slic3r::save_object_mesh(*model_object); } @@ -4564,26 +4614,26 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) Selection::EMode selection_mode = m_selection.get_mode(); for (const GLVolume* v : m_volumes.volumes) { - int object_idx = v->object_idx(); + const int object_idx = v->object_idx(); if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) continue; - int instance_idx = v->instance_idx(); - int volume_idx = v->volume_idx(); + const int instance_idx = v->instance_idx(); + const int volume_idx = v->volume_idx(); + + if (volume_idx < 0) + continue; done.insert(std::pair(object_idx, instance_idx)); // Rotate instances/volumes. ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { - if (selection_mode == Selection::Instance) { - model_object->instances[instance_idx]->set_rotation(v->get_instance_rotation()); - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); - } + if (selection_mode == Selection::Instance) + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); else if (selection_mode == Selection::Volume) { - if (model_object->volumes[volume_idx]->get_rotation() != v->get_volume_rotation()) { - model_object->volumes[volume_idx]->set_rotation(v->get_volume_rotation()); - model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); + if (model_object->volumes[volume_idx]->get_transformation() != v->get_volume_transformation()) { + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); // BBS: backup Slic3r::save_object_mesh(*model_object); } @@ -4645,27 +4695,27 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) Selection::EMode selection_mode = m_selection.get_mode(); for (const GLVolume* v : m_volumes.volumes) { - int object_idx = v->object_idx(); + const int object_idx = v->object_idx(); if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) continue; - int instance_idx = v->instance_idx(); - int volume_idx = v->volume_idx(); + const int instance_idx = v->instance_idx(); + const int volume_idx = v->volume_idx(); + + if (volume_idx < 0) + continue; done.insert(std::pair(object_idx, instance_idx)); // Rotate instances/volumes ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { - if (selection_mode == Selection::Instance) { - model_object->instances[instance_idx]->set_scaling_factor(v->get_instance_scaling_factor()); - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); - } + if (selection_mode == Selection::Instance) + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); else if (selection_mode == Selection::Volume) { - if (model_object->volumes[volume_idx]->get_scaling_factor() != v->get_volume_scaling_factor()) { - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); - model_object->volumes[volume_idx]->set_scaling_factor(v->get_volume_scaling_factor()); - model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); + if (model_object->volumes[volume_idx]->get_transformation() != v->get_volume_transformation()) { + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); // BBS: backup Slic3r::save_object_mesh(*model_object); } @@ -4756,10 +4806,10 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type) ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { if (selection_mode == Selection::Instance) - model_object->instances[instance_idx]->set_mirror(v->get_instance_mirror()); + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); else if (selection_mode == Selection::Volume) { - if (model_object->volumes[volume_idx]->get_mirror() != v->get_volume_mirror()) { - model_object->volumes[volume_idx]->set_mirror(v->get_volume_mirror()); + if (model_object->volumes[volume_idx]->get_transformation() != v->get_volume_transformation()) { + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); // BBS: backup Slic3r::save_object_mesh(*model_object); } @@ -5210,6 +5260,14 @@ bool GLCanvas3D::is_object_sinking(int object_idx) const return false; } +void GLCanvas3D::apply_retina_scale(Vec2d &screen_coordinate) const +{ +#if ENABLE_RETINA_GL + double scale = static_cast(m_retina_helper->get_scale_factor()); + screen_coordinate *= scale; +#endif // ENABLE_RETINA_GL +} + bool GLCanvas3D::_is_shown_on_screen() const { return (m_canvas != nullptr) ? m_canvas->IsShownOnScreen() : false; @@ -7148,12 +7206,14 @@ void GLCanvas3D::_check_and_update_toolbar_icon_scale() auto* m_notification = wxGetApp().plater()->get_notification_manager(); m_notification->set_scale(sc); + m_gizmos.set_overlay_scale(sc); #else //BBS: GUI refactor: GLToolbar m_main_toolbar.set_icons_size(GLGizmosManager::Default_Icons_Size * scale); m_assemble_view_toolbar.set_icons_size(size); m_separator_toolbar.set_icons_size(size); collapse_toolbar.set_icons_size(size / 2.0); + m_gizmos.set_overlay_icon_size(size); #endif // ENABLE_RETINA_GL // Update collapse toolbar @@ -7210,31 +7270,6 @@ void GLCanvas3D::_render_overlays() _render_assemble_control(); _render_assemble_info(); - // main toolbar and undoredo toolbar need to be both updated before rendering because both their sizes are needed - // to correctly place them -#if ENABLE_RETINA_GL - const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale(/*true*/); - //BBS: GUI refactor: GLToolbar - m_main_toolbar.set_scale(scale); - m_assemble_view_toolbar.set_scale(scale); - m_separator_toolbar.set_scale(scale); - wxGetApp().plater()->get_collapse_toolbar().set_scale(scale / 2.0); - m_gizmos.set_overlay_scale(scale); -#else - // BBS adjust display scale - const float size = int(GLToolbar::Default_Icons_Size * wxGetApp().toolbar_icon_scale(/*true*/)); - const float gizmo_size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale()); - //const float size = int(GLToolbar::Default_Icons_Size); - //const float gizmo_size = int(GLGizmosManager::Default_Icons_Size); - - //BBS: GUI refactor: GLToolbar - m_main_toolbar.set_icons_size(gizmo_size); - m_assemble_view_toolbar.set_icons_size(gizmo_size); - m_separator_toolbar.set_icons_size(gizmo_size); - wxGetApp().plater()->get_collapse_toolbar().set_icons_size(size / 2.0); - m_gizmos.set_overlay_icon_size(gizmo_size); -#endif // ENABLE_RETINA_GL - _render_separator_toolbar_right(); _render_separator_toolbar_left(); _render_main_toolbar(); @@ -8077,7 +8112,7 @@ void GLCanvas3D::_render_assemble_control() const const float text_size_x = std::max(imgui->calc_text_size(_L("Reset direction")).x + 2 * ImGui::GetStyle().FramePadding.x, std::max(imgui->calc_text_size(_L("Explosion Ratio")).x, imgui->calc_text_size(_L("Section View")).x)); const float slider_width = 75.0f; - const float value_size = imgui->calc_text_size("3.00").x + text_padding * 2; + const float value_size = imgui->calc_text_size(std::string_view{"3.00"}).x + text_padding * 2; const float item_spacing = imgui->get_item_spacing().x; ImVec2 window_padding = ImGui::GetStyle().WindowPadding; @@ -9511,5 +9546,108 @@ void GLCanvas3D::GizmoHighlighter::blink() invalidate(); } +const ModelVolume *get_model_volume(const GLVolume &v, const Model &model) +{ + const ModelVolume * ret = nullptr; + + if (v.object_idx() < (int)model.objects.size()) { + const ModelObject *obj = model.objects[v.object_idx()]; + if (v.volume_idx() < (int)obj->volumes.size()) + ret = obj->volumes[v.volume_idx()]; + } + + return ret; +} + +ModelVolume *get_model_volume(const ObjectID &volume_id, const ModelObjectPtrs &objects) +{ + for (const ModelObject *obj : objects) + for (ModelVolume *vol : obj->volumes) + if (vol->id() == volume_id) + return vol; + return nullptr; +} + +ModelVolume *get_model_volume(const GLVolume &v, const ModelObject& object) { + if (v.volume_idx() < 0) + return nullptr; + + size_t volume_idx = static_cast(v.volume_idx()); + if (volume_idx >= object.volumes.size()) + return nullptr; + + return object.volumes[volume_idx]; +} + +ModelVolume *get_model_volume(const GLVolume &v, const ModelObjectPtrs &objects) +{ + if (v.object_idx() < 0) + return nullptr; + size_t objext_idx = static_cast(v.object_idx()); + if (objext_idx >= objects.size()) + return nullptr; + if (objects[objext_idx] == nullptr) + return nullptr; + 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 84cb030b8..5981eb87a 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -966,6 +966,12 @@ public: Size get_canvas_size() const; Vec2d get_local_mouse_position() const; + // store opening position of menu + std::optional m_popup_menu_positon; // position of mouse right click + void set_popup_menu_position(const Vec2d &position) { m_popup_menu_positon = position; } + const std::optional& get_popup_menu_position() const { return m_popup_menu_positon; } + void clear_popup_menu_position() { m_popup_menu_positon.reset(); } + void set_tooltip(const std::string& tooltip); // the following methods add a snapshot to the undo/redo stack, unless the given string is empty @@ -1107,6 +1113,8 @@ public: bool is_object_sinking(int object_idx) const; + void apply_retina_scale(Vec2d &screen_coordinate) const; + void _perform_layer_editing_action(wxMouseEvent* evt = nullptr); // Convert the screen space coordinate to an object space coordinate. @@ -1233,6 +1241,21 @@ private: float get_overlay_window_width() { return 0; /*LayersEditing::get_overlay_window_width();*/ } }; +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 f083a7e48..4de587417 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Oleksandra Iushchenko @YuSanka, Pavel Mikuš @Godrak, Tomáš Mészáros @tamasmeszaros, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" @@ -15,6 +19,8 @@ #include "format.hpp" //BBS: add partplate related logic #include "PartPlate.hpp" +#include "Gizmos/GLGizmoEmboss.hpp" +#include "Gizmos/GLGizmoSVG.hpp" #include #include "slic3r/Utils/FixModelByWin10.hpp" @@ -273,24 +279,28 @@ wxBitmapBundle* SettingsFactory::get_category_bitmap(const std::string& category //------------------------------------- // 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 constexpr std::array, 5> ADD_VOLUME_MENU_ITEMS = {{ + // menu_item Name menu_item bitmap name {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 -}; + {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() { @@ -428,12 +438,26 @@ std::vector MenuFactory::get_volume_bitmaps() { std::vector volume_bmps; volume_bmps.reserve(ADD_VOLUME_MENU_ITEMS.size()); - for (auto item : ADD_VOLUME_MENU_ITEMS){ - if(!item.second.empty()){ - //volume_bmps.push_back(create_menu_bitmap(item.second)); - volume_bmps.push_back(get_bmp_bundle(item.second)); - } - } + for (const auto& item : ADD_VOLUME_MENU_ITEMS) + volume_bmps.push_back(get_bmp_bundle(item.second)); + 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(get_bmp_bundle(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(get_bmp_bundle(item.second)); return volume_bmps; } @@ -463,19 +487,6 @@ void MenuFactory::append_menu_item_delete(wxMenu* menu) #endif } -void MenuFactory::append_menu_item_edit_text(wxMenu *menu) -{ -#ifdef __WINDOWS__ - append_menu_item( - menu, wxID_ANY, _L("Edit Text"), "", [](wxCommandEvent &) { plater()->edit_text(); }, "", nullptr, - []() { return plater()->can_edit_text(); }, m_parent); -#else - append_menu_item( - menu, wxID_ANY, _L("Edit Text"), "", [](wxCommandEvent &) { plater()->edit_text(); }, "", nullptr, - []() { return plater()->can_edit_text(); }, m_parent); -#endif -} - wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType type) { auto sub_menu = new wxMenu; @@ -509,6 +520,10 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty }, "", menu); } + + append_menu_item_add_text(sub_menu, type); + append_menu_item_add_svg(sub_menu, type); + sub_menu->AppendSeparator(); for (auto &item : {L("Cube"), L("Cylinder"), L("Sphere"), L("Cone")}) { append_menu_item( @@ -522,13 +537,69 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty return sub_menu; } +static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) { + auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { + const GLCanvas3D *canvas = plater()->canvas3D(); + const GLGizmosManager &mng = canvas->get_gizmos_manager(); + GLGizmoBase *gizmo_base = mng.get_gizmo(gizmo_type); + + ModelVolumeType volume_type = type; + // no selected object means create new object + if (volume_type == ModelVolumeType::INVALID) + volume_type = ModelVolumeType::MODEL_PART; + + auto screen_position = canvas->get_popup_menu_position(); + if (gizmo_type == GLGizmosManager::Emboss) { + auto emboss = dynamic_cast(gizmo_base); + assert(emboss != nullptr); + if (emboss == nullptr) return; + if (screen_position.has_value()) { + emboss->create_volume(volume_type, *screen_position); + } else { + emboss->create_volume(volume_type); + } + } else if (gizmo_type == GLGizmosManager::Svg) { + auto svg = dynamic_cast(gizmo_base); + assert(svg != nullptr); + if (svg == nullptr) return; + if (screen_position.has_value()) { + svg->create_volume(volume_type, *screen_position); + } else { + svg->create_volume(volume_type); + } + } + }; + + if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER || + type == ModelVolumeType::INVALID // cannot use gizmo without selected object + ) { + wxString item_name = wxString(is_submenu_item ? "" : _(ADD_VOLUME_MENU_ITEMS[int(type)].first) + ": ") + name; + menu->AppendSeparator(); + const std::string icon_name = is_submenu_item ? "" : ADD_VOLUME_MENU_ITEMS[int(type)].second; + append_menu_item(menu, wxID_ANY, item_name, "", add_, icon_name, menu); + } +} + +void MenuFactory::append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item/* = true*/){ + append_menu_itemm_add_(_L("Text"), GLGizmosManager::Emboss, menu, type, is_submenu_item); +} + +void MenuFactory::append_menu_item_add_svg(wxMenu *menu, ModelVolumeType type, bool is_submenu_item /* = true*/){ + append_menu_itemm_add_(_L("SVG"), GLGizmosManager::Svg, menu, type, is_submenu_item); +} + void MenuFactory::append_menu_items_add_volume(wxMenu* menu) { // Update "add" items(delete old & create new) settings popupmenu for (auto& item : ADD_VOLUME_MENU_ITEMS) { - const auto settings_id = menu->FindItem(_(item.first)); - if (settings_id != wxNOT_FOUND) - menu->Destroy(settings_id); + const wxString item_name = _(item.first); + int item_id = menu->FindItem(item_name); + if (item_id != wxNOT_FOUND) + menu->Destroy(item_id); + + item_id = menu->FindItem(item_name + ": " + _L("Text")); + if (item_id != wxNOT_FOUND) + menu->Destroy(item_id); } for (size_t type = 0; type < ADD_VOLUME_MENU_ITEMS.size(); type++) @@ -992,6 +1063,81 @@ void MenuFactory::append_menu_items_mirror(wxMenu* menu) []() { return plater()->can_mirror(); }, m_parent); } +void MenuFactory::append_menu_item_edit_text(wxMenu *menu) +{ + wxString name = _L("Edit text"); + + auto can_edit_text = []() { + 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_text(); + }; + + if (menu != &m_text_part_menu) { + const int menu_item_id = menu->FindItem(name); + if (menu_item_id != wxNOT_FOUND) + menu->Destroy(menu_item_id); + if (!can_edit_text()) + return; + } + + wxString description = _L("Ability to change text, font, size, ..."); + std::string icon = "cog"; + auto open_emboss = [](const wxCommandEvent &) { + GLGizmosManager &mng = plater()->get_view3D_canvas3D()->get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Emboss) + mng.open_gizmo(GLGizmosManager::Emboss); // close() and reopen - move to be visible + mng.open_gizmo(GLGizmosManager::Emboss); + }; + append_menu_item(menu, wxID_ANY, name, description, open_emboss, icon, nullptr, can_edit_text, m_parent); +} + +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 = "cog"; + 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); +} + void MenuFactory::append_menu_item_invalidate_cut_info(wxMenu *menu) { const wxString menu_name = _L("Invalidate cut info"); @@ -1175,6 +1321,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; @@ -1301,7 +1475,8 @@ void MenuFactory::init(wxWindow* parent) //create_object_menu(); create_sla_object_menu(); //create_part_menu(); - + create_text_part_menu(); + create_svg_part_menu(); create_extra_object_menu(); create_bbl_part_menu(); create_bbl_assemble_object_menu(); @@ -1330,6 +1505,8 @@ 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_text(&m_object_menu); + append_menu_item_edit_svg(&m_object_menu); append_menu_item_change_filament(&m_object_menu); return &m_object_menu; } @@ -1339,6 +1516,8 @@ wxMenu* MenuFactory::sla_object_menu() append_menu_items_convert_unit(&m_sla_object_menu); append_menu_item_settings(&m_sla_object_menu); //update_menu_items_instance_manipulation(mtObjectSLA); + append_menu_item_edit_text(&m_sla_object_menu); + append_menu_item_edit_svg(&m_object_menu); return &m_sla_object_menu; } @@ -1351,6 +1530,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 8f4c9d6d0..70cb225af 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Oleksandra Iushchenko @YuSanka, Tomáš Mészáros @tamasmeszaros, Lukáš Matěna @lukasmatena, Pavel Mikuš @Godrak, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_GUI_Factories_hpp_ #define slic3r_GUI_Factories_hpp_ @@ -47,8 +51,9 @@ struct SettingsFactory class MenuFactory { public: - static const std::vector> ADD_VOLUME_MENU_ITEMS; - static std::vector get_volume_bitmaps(); + static std::vector get_volume_bitmaps(); + static std::vector get_text_volume_bitmaps(); + static std::vector get_svg_volume_bitmaps(); MenuFactory(); ~MenuFactory() = default; @@ -65,6 +70,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(); @@ -85,6 +92,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; @@ -104,6 +113,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 @@ -113,6 +124,8 @@ private: void create_bbl_assemble_part_menu(); wxMenu* append_submenu_add_generic(wxMenu* menu, ModelVolumeType type); + void append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item = true); + void append_menu_item_add_svg(wxMenu *menu, ModelVolumeType type, bool is_submenu_item = true); void append_menu_items_add_volume(wxMenu* menu); wxMenuItem* append_menu_item_layers_editing(wxMenu* menu); wxMenuItem* append_menu_item_settings(wxMenu* menu); @@ -128,7 +141,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); @@ -137,6 +149,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 7e088b435..697fba968 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1331,11 +1331,20 @@ void ObjectList::show_context_menu(const bool evt_context_menu) const ItemType type = m_objects_model->GetItemType(item); if (!(type & (itPlate | itObject | itVolume | itInstance))) return; + 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 = 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(); + menu = volume->is_text() ? plater->text_part_menu() : + 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; @@ -1999,7 +2008,7 @@ void ObjectList::load_modifier(const wxArrayString& input_files, ModelObject& mo // First (any) GLVolume of the selected instance. They all share the same instance matrix. const GLVolume* v = selection.get_first_volume(); const Geometry::Transformation inst_transform = v->get_instance_transformation(); - const Transform3d inv_inst_transform = inst_transform.get_matrix(true).inverse(); + const Transform3d inv_inst_transform = inst_transform.get_matrix_no_offset().inverse(); const Vec3d instance_offset = v->get_instance_offset(); for (size_t i = 0; i < input_files.size(); ++i) { @@ -2143,7 +2152,7 @@ void ObjectList::load_generic_subobject(const std::string& type_name, const Mode Vec3d(0., 0., 0.5 * mesh_bb.size().z() + instance_bb.min.z() - v->get_instance_offset().z()) : // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. Vec3d(instance_bb.max.x(), instance_bb.min.y(), instance_bb.min.z()) + 0.5 * mesh_bb.size() - v->get_instance_offset(); - new_volume->set_offset(v->get_instance_transformation().get_matrix(true).inverse() * offset); + new_volume->set_offset(v->get_instance_transformation().get_matrix_no_offset().inverse() * offset); // BBS: backup Slic3r::save_object_mesh(model_object); @@ -2273,56 +2282,6 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name #endif /* _DEBUG */ } -int ObjectList::load_mesh_part(const TriangleMesh &mesh, const wxString &name, const TextInfo &text_info, bool is_temp) -{ - wxDataViewItem item = GetSelection(); - // we can add volumes for Object or Instance - if (!item || !(m_objects_model->GetItemType(item) & (itObject | itInstance))) - return -1; - const int obj_idx = m_objects_model->GetObjectIdByItem(item); - - if (obj_idx < 0) - return -1; - - // Get object item, if Instance is selected - if (m_objects_model->GetItemType(item) & itInstance) - item = m_objects_model->GetItemById(obj_idx); - - ModelObject* mo = (*m_objects)[obj_idx]; - - Geometry::Transformation instance_transformation = mo->instances[0]->get_transformation(); - - // apply the instance transform to all volumes and reset instance transform except the offset - apply_object_instance_transfrom_to_all_volumes(mo, !is_temp); - - ModelVolume *mv = mo->add_volume(mesh); - mv->name = name.ToStdString(); - if (!text_info.m_text.empty()) - mv->set_text_info(text_info); - - if (!is_temp) { - std::vector volumes; - volumes.push_back(mv); - wxDataViewItemArray items = reorder_volumes_and_get_selection(obj_idx, [volumes](const ModelVolume *volume) { - return std::find(volumes.begin(), volumes.end(), volume) != volumes.end(); - }); - - wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_object((size_t) obj_idx); - - if (items.size() > 1) { - m_selection_mode = smVolume; - m_last_selected_item = wxDataViewItem(nullptr); - } - select_items(items); - - selection_changed(); - } - - //BBS: notify partplate the modify - notify_instance_updated(obj_idx); - return mo->volumes.size() - 1; -} - //BBS bool ObjectList::del_object(const int obj_idx, bool refresh_immediately) { @@ -2614,7 +2573,9 @@ 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, - get_warning_icon_name(volume->mesh().stats()), + volume->is_text(), + volume->is_svg(), + get_warning_icon_name(volume->mesh().stats()), volume->config.has("extruder") ? volume->config.extruder() : 0, false); // add settings to the part, if it has those @@ -3787,6 +3748,8 @@ wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, st object_item, from_u8(volume->name), volume->type(), + volume->is_text(), + volume->is_svg(), get_warning_icon_name(volume->mesh().stats()), volume->config.has("extruder") ? volume->config.extruder() : 0, false); @@ -5838,6 +5801,8 @@ wxDataViewItemArray ObjectList::reorder_volumes_and_get_selection(int obj_idx, s for (const ModelVolume* volume : object->volumes) { wxDataViewItem vol_item = m_objects_model->AddVolumeChild(object_item, from_u8(volume->name), volume->type(), + volume->is_text(), + volume->is_svg(), get_warning_icon_name(volume->mesh().stats()), volume->config.has("extruder") ? volume->config.extruder() : 0, false); diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index b270b1163..1c96e3e5a 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -27,7 +27,6 @@ class ModelConfig; class ModelObject; class ModelVolume; class TriangleMesh; -struct TextInfo; enum class ModelVolumeType : int; // FIXME: broken build on mac os because of this is missing: @@ -287,7 +286,6 @@ public: void load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center = true); // BBS void switch_to_object_process(); - int load_mesh_part(const TriangleMesh &mesh, const wxString &name, const TextInfo &text_info, bool is_temp); bool del_object(const int obj_idx, bool refresh_immediately = true); void del_subobject_item(wxDataViewItem& item); void del_settings_from_config(const wxDataViewItem& parent_item); diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index af4dbc40f..74d1f959d 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -1,3 +1,9 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Filip Sykala @Jony01, David Kocík @kocikdav, Tomáš Mészáros @tamasmeszaros, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2022 André Althaus +///|/ Copyright (c) 2019 John Drake @foxox +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ //#include "stdlib.h" #include "libslic3r/libslic3r.h" #include "libslic3r/Layer.hpp" @@ -65,6 +71,7 @@ bool View3D::init(wxWindow* parent, Bed3D& bed, Model* model, DynamicPrintConfig m_canvas->allow_multisample(OpenGLManager::can_multisample()); // XXX: If have OpenGL m_canvas->enable_picking(true); + m_canvas->get_selection().set_mode(Selection::Instance); m_canvas->enable_moving(true); // XXX: more config from 3D.pm m_canvas->set_model(model); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index ed98b8193..1ba93df82 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -252,12 +252,17 @@ void GLGizmoBase::GizmoImguiEnd() void GLGizmoBase::GizmoImguiSetNextWIndowPos(float &x, float y, int flag, float pivot_x, float pivot_y) { - if (abs(last_input_window_width) > 0.01f) { - if (x + last_input_window_width > m_parent.get_canvas_size().get_width()) { - if (last_input_window_width > m_parent.get_canvas_size().get_width()) { + GizmoImguiSetNextWIndowPos(x, y, last_input_window_width, 0, flag, pivot_x, pivot_y); +} + +void GLGizmoBase::GizmoImguiSetNextWIndowPos(float &x, float y, float w, float h, int flag, float pivot_x, float pivot_y) +{ + if (abs(w) > 0.01f) { + if (x + w > m_parent.get_canvas_size().get_width()) { + if (w > m_parent.get_canvas_size().get_width()) { x = 0; } else { - x = m_parent.get_canvas_size().get_width() - last_input_window_width; + x = m_parent.get_canvas_size().get_width() - w; } } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index d60e50485..9e7aa0520 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -250,6 +250,7 @@ protected: bool GizmoImguiBegin(const std::string& name, int flags); void GizmoImguiEnd(); void GizmoImguiSetNextWIndowPos(float &x, float y, int flag, float pivot_x = 0.0f, float pivot_y = 0.0f); + void GizmoImguiSetNextWIndowPos(float &x, float y, float w, float h, int flag, float pivot_x = 0.0f, float pivot_y = 0.0f); void register_grabbers_for_picking(); void unregister_grabbers_for_picking(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index c0267c988..700264128 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -1769,7 +1769,7 @@ BoundingBoxf3 GLGizmoCut3D::transformed_bounding_box(const Vec3d& plane_center, // respect just to the solid parts for FFF and ignore pad and supports for SLA if (!volume->is_modifier && !volume->is_sla_pad() && !volume->is_sla_support()) { - const auto instance_matrix = volume->get_instance_transformation().get_matrix(true); + const auto instance_matrix = volume->get_instance_transformation().get_matrix_no_offset(); auto volume_trafo = instance_matrix * volume->get_volume_transformation().get_matrix(); ret.merge(volume->transformed_convex_hull_bounding_box(cut_matrix * volume_trafo)); } @@ -2284,13 +2284,13 @@ void GLGizmoCut3D::render_connectors_input_window(CutConnectors &connectors, flo render_flip_plane_button(m_connectors_editing && connectors.empty()); m_imgui->text(m_labels_map["Type"]); - ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.00f, 0.00f, 0.00f, 1.00f)); + ImGuiWrapper::push_radio_style(); bool type_changed = render_connect_type_radio_button(CutConnectorType::Plug); type_changed |= render_connect_type_radio_button(CutConnectorType::Dowel); type_changed |= render_connect_type_radio_button(CutConnectorType::Snap); if (type_changed) apply_selected_connectors([this, &connectors] (size_t idx) { connectors[idx].attribs.type = CutConnectorType(m_connector_type); }); - ImGui::PopStyleColor(1); + ImGuiWrapper::pop_radio_style(); m_imgui->disabled_begin(m_connector_type != CutConnectorType::Plug); if (type_changed && m_connector_type == CutConnectorType::Dowel) { @@ -2948,7 +2948,7 @@ void GLGizmoCut3D::show_tooltip_information(float x, float y) ImTextureID normal_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP); ImTextureID hover_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER); - caption_max += m_imgui->calc_text_size(": ").x + 35.f; + caption_max += m_imgui->calc_text_size(std::string_view{": "}).x + 35.f; float font_size = ImGui::GetFontSize(); ImVec2 button_size = ImVec2(font_size * 1.8, font_size * 1.3); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp new file mode 100644 index 000000000..9f5e95798 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -0,0 +1,3664 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Tomáš Mészáros @tamasmeszaros, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "GLGizmoEmboss.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.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/Plater.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/CameraUtils.hpp" +#include "slic3r/GUI/Jobs/EmbossJob.hpp" +#include "slic3r/GUI/Jobs/CreateFontNameImageJob.hpp" +#include "slic3r/GUI/Jobs/NotificationProgressIndicator.hpp" +#include "slic3r/Utils/WxFontUtils.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +#include "libslic3r/Geometry.hpp" // to range pi pi +#include "libslic3r/Timer.hpp" + +#include "libslic3r/Model.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/ClipperUtils.hpp" // union_ex +#include "libslic3r/AppConfig.hpp" // store/load font list +#include "libslic3r/Format/OBJ.hpp" // load obj file for default object +#include "libslic3r/BuildVolume.hpp" + +#include "imgui/imgui_stdlib.h" // using std::string for inputs +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include + +#include +#include +#include +#include +#include // detection of change DPI +#include + +#include +#include // serialize deserialize facenames +#include +#include + +// cache font list by cereal +#include +#include +#include +#include + +#include +#include // measure enumeration of fonts + +// uncomment for easier debug +//#define ALLOW_DEBUG_MODE +#ifdef ALLOW_DEBUG_MODE +#define ALLOW_ADD_FONT_BY_FILE +#define ALLOW_ADD_FONT_BY_OS_SELECTOR +#define SHOW_WX_FONT_DESCRIPTOR // OS specific descriptor | file path --> in edit style +#define SHOW_FONT_FILE_PROPERTY // ascent, descent, line gap, cache --> in advanced +#define SHOW_CONTAIN_3MF_FIX // when contain fix matrix --> show gray '3mf' next to close button +#define SHOW_OFFSET_DURING_DRAGGING // when drag with text over surface visualize used center +#define SHOW_IMGUI_ATLAS +#define SHOW_ICONS_TEXTURE +#define SHOW_FINE_POSITION // draw convex hull around volume +#define DRAW_PLACE_TO_ADD_TEXT // Interactive draw of window position +#define ALLOW_OPEN_NEAR_VOLUME +#endif // ALLOW_DEBUG_MODE + +//#define USE_PIXEL_SIZE_IN_WX_FONT + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + +namespace { +template struct Limit { + // Limitation for view slider range in GUI + MinMax gui; + // Real limits for setting exacts values + MinMax values; +}; +// Variable keep limits for variables +static const struct Limits +{ + MinMax emboss{0.01, 1e4}; // in mm + MinMax size_in_mm{0.1f, 1000.f}; // in mm + Limit boldness{{-200.f, 200.f}, {-2e4f, 2e4f}}; // in font points + Limit skew{{-1.f, 1.f}, {-100.f, 100.f}}; // ration without unit + MinMax char_gap{-20000, 20000}; // in font points + MinMax line_gap{-20000, 20000}; // in font points + // distance text object from surface + MinMax angle{-180.f, 180.f}; // in degrees +} limits; + +static const float SELECTABLE_INNER_OFFSET = 8.0f; + +/// +/// Prepare data for emboss +/// +/// Text to emboss +/// Keep actual selected style +/// Needed when transform per glyph +/// Needed for transform per glyph +/// Define type of volume - side of surface(in / out) +/// Cancel for previous job +/// Base data for emboss text +std::unique_ptr create_emboss_data_base( + const std::string& text, + StyleManager& style_manager, + TextLinesModel& text_lines, + const Selection& selection, + ModelVolumeType type, + std::shared_ptr>& cancel); +CreateVolumeParams create_input(GLCanvas3D &canvas, const StyleManager::Style &style, RaycastManager &raycaster, ModelVolumeType volume_type); + +/// +/// Move window for edit emboss text near to embossed object +/// NOTE: embossed object must be selected +/// +ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size); + +struct TextDataBase : public DataBase +{ + TextDataBase(DataBase &&parent, const FontFileWithCache &font_file, + TextConfiguration &&text_configuration, const EmbossProjection& projection); + // Create shape from text + font configuration + EmbossShape &create_shape() override; + void write(ModelVolume &volume) const override; + +private: + // Keep pointer on Data of font (glyph shapes) + FontFileWithCache m_font_file; + // font item is not used for create object + TextConfiguration m_text_configuration; +}; + +// Loaded icons enum +// Have to match order of files in function GLGizmoEmboss::init_icons() +enum class IconType : unsigned { + rename = 0, + erase, + add, + save, + undo, + italic, + unitalic, + bold, + unbold, + system_selector, + open_file, + exclamation, + lock, + lock_bold, + unlock, + unlock_bold, + align_horizontal_left, + align_horizontal_center, + align_horizontal_right, + align_vertical_top, + align_vertical_center, + align_vertical_bottom, + // automatic calc of icon's count + _count +}; +// 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); +// short call of Slic3r::GUI::button +bool draw_button(const IconManager::VIcons& icons, IconType type, bool disable = false); + +struct FaceName +{ + wxString wx_name; + std::string name_truncated = ""; + size_t texture_index = 0; + // State for generation of texture + // when start generate create share pointers + std::shared_ptr> cancel = nullptr; + // R/W only on main thread - finalize of job + std::shared_ptr is_created = nullptr; +}; + +// Implementation of forwarded struct +// Keep sorted list of loadable face names +struct Facenames +{ + // flag to keep need of enumeration fonts from OS + // false .. wants new enumeration check by Hash + // true .. already enumerated(During opened combo box) + bool is_init = false; + + bool has_truncated_names = false; + + // data of can_load() faces + std::vector faces = {}; + std::vector faces_names = {}; + // Sorter set of Non valid face names in OS + std::vector bad = {}; + + // Configuration of font encoding + static const wxFontEncoding encoding = wxFontEncoding::wxFONTENCODING_SYSTEM; + + // Identify if preview texture exists + GLuint texture_id = 0; + + // protection for open too much font files together + // Gtk:ERROR:../../../../gtk/gtkiconhelper.c:494:ensure_surface_for_gicon: assertion failed (error == NULL): Failed to load + // /usr/share/icons/Yaru/48x48/status/image-missing.png: Error opening file /usr/share/icons/Yaru/48x48/status/image-missing.png: Too + // many open files (g-io-error-quark, 31) This variable must exist until no CreateFontImageJob is running + unsigned int count_opened_font_files = 0; + + // Configuration for texture height + const int count_cached_textures = 32; + + // index for new generated texture index(must be lower than count_cached_textures) + size_t texture_index = 0; + + // hash created from enumerated font from OS + // check when new font was installed + size_t hash = 0; + + // filtration pattern + //std::string search = ""; + //std::vector hide; // result of filtration +}; + +bool store(const Facenames &facenames); +bool load(Facenames &facenames); +void init_face_names(Facenames &facenames); +void init_truncated_names(Facenames &face_names, float max_width); + +// 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; + bool dark_mode = false; + + // Zero means it is calculated in init function + float height_of_volume_type_selector = 0.f; + float input_width = 0.f; + float delete_pos_x = 0.f; + float max_style_name_width = 0.f; + unsigned int icon_width = 0; + float max_tooltip_width = 0.f; + + // maximal width and height of style image + Vec2i max_style_image_size = Vec2i(0, 0); + + float indent = 0.f; + float input_offset = 0.f; + float advanced_input_offset = 0.f; + float lock_offset = 0.f; + + ImVec2 text_size; + + // maximal size of face name image + Vec2i face_name_size = Vec2i(0, 0); + float face_name_texture_offset_x = 0.f; + + // maximal texture generate jobs running at once + unsigned int max_count_opened_font_files = 10; + + // Only translations needed for calc GUI size + struct Translations + { + std::string font; + std::string height; + std::string depth; + + // advanced + std::string use_surface; + std::string per_glyph; + std::string alignment; + std::string char_gap; + std::string line_gap; + std::string boldness; + std::string skew_ration; + std::string from_surface; + std::string rotation; + std::string keep_up; + std::string collection; + }; + Translations translations; +}; +GuiCfg create_gui_configuration(); + +void draw_font_preview(FaceName &face, const std::string &text, Facenames &faces, const GuiCfg &cfg, bool is_visible); +// for existing volume which is selected(could init different(to volume text) lines count when edit text) +void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* const*/ StyleManager &style_manager, unsigned count_lines=0); +} // namespace priv + +// use private definition +struct GLGizmoEmboss::Facenames: public ::Facenames{}; +struct GLGizmoEmboss::GuiCfg: public ::GuiCfg{}; + +GLGizmoEmboss::GLGizmoEmboss(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) + , m_gui_cfg(nullptr) + , m_style_manager(m_imgui->get_glyph_ranges(), create_default_styles) + , m_face_names(std::make_unique()) + , 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); + // to use https://fontawesome.com/ (copy & paste) unicode symbols from web + // paste HEX unicode into notepad move cursor after unicode press [alt] + [x] +} + +bool GLGizmoEmboss::create_volume(ModelVolumeType volume_type, const Vec2d& mouse_pos) +{ + if (!init_create(volume_type)) + return false; + + // NOTE: change style manager - be carefull with order changes + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); + CreateVolumeParams input = create_input(m_parent, m_style_manager.get_style(), m_raycast_manager, volume_type); + return start_create_volume(input, std::move(base), mouse_pos); +} + +// Designed for create volume without information of mouse in scene +bool GLGizmoEmboss::create_volume(ModelVolumeType volume_type) +{ + if (!init_create(volume_type)) + return false; + + // NOTE: change style manager - be carefull with order changes + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, m_parent.get_selection(), volume_type, m_job_cancel); + CreateVolumeParams input = create_input(m_parent, m_style_manager.get_style(), m_raycast_manager, volume_type); + return start_create_volume_without_position(input, std::move(base)); +} + + +void GLGizmoEmboss::on_shortcut_key() { + set_volume_by_selection(); + if (m_volume == nullptr) { + // No volume to select from selection so create volume. + // NOTE: After finish job for creation emboss Text volume, + // GLGizmoEmboss will be opened + create_volume(ModelVolumeType::MODEL_PART); + } + else { + // shortcut is pressed when text is selected soo start edit it. + auto& mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() != GLGizmosManager::Emboss) + mng.open_gizmo(GLGizmosManager::Emboss); + } +} + +bool GLGizmoEmboss::re_emboss(const ModelVolume &text_volume, std::shared_ptr> job_cancel) +{ + assert(text_volume.text_configuration.has_value()); + assert(text_volume.emboss_shape.has_value()); + if (!text_volume.text_configuration.has_value() || + !text_volume.emboss_shape.has_value()) + return false; // not valid text volume to re emboss + const TextConfiguration &tc = *text_volume.text_configuration; + const EmbossShape &es = *text_volume.emboss_shape; + const ImWchar* ranges = ImGui::GetIO().Fonts->GetGlyphRangesDefault(); + + StyleManager style_manager(ranges, create_default_styles); + StyleManager::Style style{tc.style, es.projection}; + if (!style_manager.load_style(style)) + return false; // can't load font + + TextLinesModel text_lines; + const Selection &selection = wxGetApp().plater()->canvas3D()->get_selection(); + DataBasePtr base = create_emboss_data_base(tc.text, style_manager, text_lines, selection, text_volume.type(), job_cancel); + DataUpdate data{std::move(base), text_volume.id()}; + + RaycastManager raycast_manager; // Nothing is cached now, so It need to create raycasters + return start_update_volume(std::move(data), text_volume, selection, raycast_manager); +} + +namespace{ +ModelVolumePtrs prepare_volumes_to_slice(const ModelVolume &mv) +{ + const ModelVolumePtrs &volumes = mv.get_object()->volumes; + ModelVolumePtrs result; + result.reserve(volumes.size()); + for (ModelVolume *volume : volumes) { + // only part could be surface for volumes + if (!volume->is_model_part()) + continue; + + // is selected volume + if (mv.id() == volume->id()) + continue; + + result.push_back(volume); + } + return result; +} +} // namespace + +bool GLGizmoEmboss::do_mirror(size_t axis) +{ + // is valid input + assert(axis < 3); + if (axis >= 3) + return false; + + // is gizmo opened and initialized? + assert(m_parent.get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss); + if (m_parent.get_gizmos_manager().get_current_type() != GLGizmosManager::Emboss) + return false; + + const std::optional &tc = m_volume->text_configuration; + assert(tc.has_value()); + bool is_per_glyph = tc.has_value()? tc->style.prop.per_glyph : false; + + const std::optional &es = m_volume->emboss_shape; + assert(es.has_value()); + bool use_surface = es.has_value()? es->projection.use_surface : false; + if (!use_surface && !is_per_glyph) { + // do normal mirror with fix matrix + Selection &selection = m_parent.get_selection(); + selection.setup_cache(); + + auto selection_mirror_fnc = [&selection, &axis]() { selection.mirror((Axis) axis, get_drag_transformation_type(selection)); }; + selection_transform(selection, selection_mirror_fnc); + + m_parent.do_mirror(L("Set Mirror")); + wxGetApp().obj_manipul()->UpdateAndShow(true); + return true; + } + + Vec3d scale = Vec3d::Ones(); + scale[axis] = -1.; + + Transform3d tr = m_volume->get_matrix(); + if (es.has_value()) { + const std::optional &fix_tr = es->fix_3mf_tr; + if (fix_tr.has_value()) + tr = tr * (fix_tr->inverse()); + } + + // mirror + tr = tr * Eigen::Scaling(scale); + + if (is_per_glyph) { + // init textlines before mirroring on mirrored text volume transformation + ModelVolumePtrs volumes = prepare_volumes_to_slice(*m_volume); + m_text_lines.init(tr, volumes, m_style_manager, m_text_lines.get_lines().size()); + } + + m_volume->set_transformation(tr); + // setting to volume is not visible for user(not GLVolume) + // NOTE: Staff around volume transformation change is done in job finish + return process(); +} + +namespace{ +// verify correct volume type for creation of text +bool check(ModelVolumeType volume_type) { + return volume_type == ModelVolumeType::MODEL_PART || + volume_type == ModelVolumeType::NEGATIVE_VOLUME || + volume_type == ModelVolumeType::PARAMETER_MODIFIER; +} +} + +bool GLGizmoEmboss::init_create(ModelVolumeType volume_type) +{ + // check valid volume type + if (!check(volume_type)){ + BOOST_LOG_TRIVIAL(error) << "Can't create embossed volume with this type: " << (int) volume_type; + return false; + } + + if (!is_activable()) { + BOOST_LOG_TRIVIAL(error) << "Can't create text. Gizmo is not activabled."; + return false; + } + + // Check can't be inside is_activable() cause crash + // steps to reproduce: start App -> key 't' -> key 'delete' + if (wxGetApp().obj_list()->has_selected_cut_object()) { + BOOST_LOG_TRIVIAL(error) << "Can't create text on cut object"; + return false; + } + + m_style_manager.discard_style_changes(); + + // set default text + m_text = _u8L("Embossed text"); + return true; +} + +bool GLGizmoEmboss::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()) { + // check that style is activ + assert(m_style_manager.is_active_font()); + if (!m_style_manager.is_active_font()) + return used; + + std::optional &angle_opt = m_style_manager.get_style().angle; + dragging_rotate_gizmo(m_rotate_gizmo.get_angle(), angle_opt, m_rotate_start_angle, m_parent.get_selection()); + } + return used; +} + +bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) +{ + // exist selected volume? + if (m_volume == nullptr) + return false; + + 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) + volume_transformation_changed(); + + // 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) { + const Selection &selection = m_parent.get_selection(); + const GLVolume *gl_volume = get_selected_gl_volume(selection); + assert(gl_volume != nullptr); + assert(m_style_manager.is_active_font()); + if (gl_volume == nullptr || !m_style_manager.is_active_font()) + return res; + m_style_manager.get_style().angle = calc_angle(selection); + } + + volume_transformation_changing(); + } + return res; +} + +void GLGizmoEmboss::on_mouse_change_selection(const wxMouseEvent &mouse_event) +{ + static bool was_dragging = true; + if ((mouse_event.LeftUp() || mouse_event.RightUp()) && !was_dragging) { + // is hovered volume closest hovered? + int hovered_idx = m_parent.get_first_hover_volume_idx(); + if (hovered_idx < 0) + // unselect object + return close(); + + const GLVolumePtrs &gl_volumes = m_parent.get_volumes().volumes; + auto hovered_idx_ = static_cast(hovered_idx); + if (hovered_idx_ >= gl_volumes.size()) + return close(); + + const GLVolume *gl_volume = gl_volumes[hovered_idx_]; + if (gl_volume == nullptr) + return close(); + + const ModelVolume *volume = get_model_volume(*gl_volume, m_parent.get_model()->objects); + if (volume == nullptr || !volume->text_configuration.has_value()) + // select volume without text configuration + return close(); + + // Reselection of text to another text + } + was_dragging = mouse_event.Dragging(); + + // Hook When click on object for reselection must be on event left down not up + if (mouse_event.LeftDown()) { + // is hovered volume closest hovered? + int hovered_idx = m_parent.get_first_hover_volume_idx(); + if (hovered_idx < 0) + // Potentionaly move with camera (drag) + return; + + const GLVolumePtrs &gl_volumes = m_parent.get_volumes().volumes; + auto hovered_idx_ = static_cast(hovered_idx); + if (hovered_idx_ >= gl_volumes.size()) + return; + const GLVolume *gl_volume = gl_volumes[hovered_idx_]; + if (gl_volume == nullptr) + return; + const ModelVolume *volume = get_model_volume(*gl_volume, m_parent.get_model()->objects); + if (volume == nullptr) + return; + + if (volume->text_configuration.has_value()) + return; // Reselection of text to another text + + // select volume without text configuration + return close(); + } + + // Hook When drag with scene by right mouse button + // object it is selected after drag scene !! + if (mouse_event.RightDown()) { + // is hovered volume closest hovered? + int hovered_idx = m_parent.get_first_hover_volume_idx(); + if (hovered_idx < 0) + // Potentionaly move with camera (drag) + return; + + const GLVolumePtrs &gl_volumes = m_parent.get_volumes().volumes; + auto hovered_idx_ = static_cast(hovered_idx); + if (hovered_idx_ >= gl_volumes.size()) + return; + const GLVolume *gl_volume = gl_volumes[hovered_idx_]; + if (gl_volume == nullptr) + return; + const ModelVolume *volume = get_model_volume(*gl_volume, m_parent.get_model()->objects); + if (volume == nullptr) + return; + + // is actual selected? + if (m_volume->id() == volume->id()) + return; + + // select volume without text configuration + return close(); + } +} + +bool GLGizmoEmboss::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->text_configuration.has_value()) return false; + + if (on_mouse_for_rotation(mouse_event)) return true; + if (on_mouse_for_translate(mouse_event)) return true; + on_mouse_change_selection(mouse_event); + return false; +} + +void GLGizmoEmboss::volume_transformation_changing() +{ + if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { + assert(false); + return; + } + const FontProp &prop = m_volume->text_configuration->style.prop; + if (prop.per_glyph) + init_text_lines(m_text_lines, m_parent.get_selection(), m_style_manager, m_text_lines.get_lines().size()); +} + +void GLGizmoEmboss::volume_transformation_changed() +{ + if (m_volume == nullptr || + !m_volume->text_configuration.has_value() || + !m_volume->emboss_shape.has_value() || + !m_style_manager.is_active_font()) { + assert(false); + return; + } + + if (!m_keep_up) { + // Re-Calculate current angle of up vector + m_style_manager.get_style().angle = calc_angle(m_parent.get_selection()); + } else { + // angle should be the same + assert(is_approx(m_style_manager.get_style().angle, calc_angle(m_parent.get_selection()))); + } + + const TextConfiguration &tc = *m_volume->text_configuration; + const EmbossShape &es = *m_volume->emboss_shape; + + bool per_glyph = tc.style.prop.per_glyph; + if (per_glyph) + init_text_lines(m_text_lines, m_parent.get_selection(), m_style_manager, m_text_lines.get_lines().size()); + + bool use_surface = es.projection.use_surface; + + // Update surface by new position + if (use_surface || per_glyph) + process(); + 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 GLGizmoEmboss::wants_enter_leave_snapshots() const { return true; } +std::string GLGizmoEmboss::get_gizmo_entering_text() const { return _u8L("Enter emboss gizmo"); } +std::string GLGizmoEmboss::get_gizmo_leaving_text() const { return _u8L("Leave emboss gizmo"); } +std::string GLGizmoEmboss::get_action_snapshot_name() const { return _u8L("Embossing actions"); } + +bool GLGizmoEmboss::on_init() +{ + m_rotate_gizmo.init(); + ColorRGBA gray_color(.6f, .6f, .6f, .3f); + m_rotate_gizmo.set_highlight_color(gray_color); + + // NOTE: It has special handling in GLGizmosManager::handle_shortcut + m_shortcut_key = WXK_CONTROL_T; + + // initialize text styles + m_style_manager.init(wxGetApp().app_config); + + // Set rotation gizmo upwardrotate + m_rotate_gizmo.set_angle(PI / 2); + return true; +} + +std::string GLGizmoEmboss::on_get_name() const { return _u8L("Emboss"); } + +void GLGizmoEmboss::on_render() { + // no volume selected + const Selection &selection = m_parent.get_selection(); + if (m_volume == nullptr || + get_model_volume(m_volume_id, selection.get_model()->objects) == nullptr) + return; + if (selection.is_empty()) return; + + // prevent get local coordinate system on multi volumes + if (!selection.is_single_volume_or_modifier() && + !selection.is_single_volume_instance()) return; + + const GLVolume *gl_volume_ptr = m_parent.get_selection().get_first_volume(); + if (gl_volume_ptr == nullptr) return; + + if (m_text_lines.is_init()) { + const Transform3d& tr = gl_volume_ptr->world_matrix(); + const auto &fix = m_volume->emboss_shape->fix_3mf_tr; + if (fix.has_value()) + m_text_lines.render(tr * fix->inverse()); + else + m_text_lines.render(tr); + } + + 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; + if (is_rotate_by_grabbers || + (!is_surface_dragging && !is_parent_dragging)) { + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + m_rotate_gizmo.render(); + } +} + +void GLGizmoEmboss::on_register_raycasters_for_picking(){ + m_rotate_gizmo.register_raycasters_for_picking(); +} +void GLGizmoEmboss::on_unregister_raycasters_for_picking(){ + m_rotate_gizmo.unregister_raycasters_for_picking(); +} + +#ifdef SHOW_FINE_POSITION +// draw suggested position of window +static void draw_fine_position(const Selection &selection, + const Size &canvas, + const ImVec2 &windows_size) +{ + const Selection::IndicesList& indices = selection.get_volume_idxs(); + // no selected volume + if (indices.empty()) return; + const GLVolume *volume = selection.get_volume(*indices.begin()); + // bad volume selected (e.g. deleted one) + if (volume == nullptr) return; + + const Camera &camera = wxGetApp().plater()->get_camera(); + Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *volume); + ImVec2 canvas_size(canvas.get_width(), canvas.get_height()); + ImVec2 offset = ImGuiWrapper::suggest_location(windows_size, hull, + canvas_size); + Slic3r::Polygon rect( + {Point(offset.x, offset.y), Point(offset.x + windows_size.x, offset.y), + Point(offset.x + windows_size.x, offset.y + windows_size.y), + Point(offset.x, offset.y + windows_size.y)}); + ImGuiWrapper::draw(hull); + ImGuiWrapper::draw(rect); +} +#endif // SHOW_FINE_POSITION + +#ifdef DRAW_PLACE_TO_ADD_TEXT +static void draw_place_to_add_text() +{ + ImVec2 mp = ImGui::GetMousePos(); + Vec2d mouse_pos(mp.x, mp.y); + const Camera &camera = wxGetApp().plater()->get_camera(); + Vec3d p1 = CameraUtils::get_z0_position(camera, mouse_pos); + std::vector rect3d{p1 + Vec3d(5, 5, 0), p1 + Vec3d(-5, 5, 0), + p1 + Vec3d(-5, -5, 0), p1 + Vec3d(5, -5, 0)}; + Points rect2d = CameraUtils::project(camera, rect3d); + ImGuiWrapper::draw(Slic3r::Polygon(rect2d)); +} +#endif // DRAW_PLACE_TO_ADD_TEXT + +#ifdef SHOW_OFFSET_DURING_DRAGGING +static void draw_mouse_offset(const std::optional &offset) +{ + if (!offset.has_value()) return; + // debug draw + auto draw_list = ImGui::GetOverlayDrawList(); + ImVec2 p1 = ImGui::GetMousePos(); + ImVec2 p2(p1.x + offset->x(), p1.y + offset->y()); + ImU32 color = ImGui::GetColorU32(ImGuiWrapper::COL_ORANGE_LIGHT); + float thickness = 3.f; + draw_list->AddLine(p1, p2, color, thickness); +} +#endif // SHOW_OFFSET_DURING_DRAGGING + +void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit) +{ + assert(m_volume != nullptr); + // Do not render window for not selected text volume + if (m_volume == nullptr || + get_model_volume(m_volume_id, m_parent.get_selection().get_model()->objects) == nullptr || + !m_volume->text_configuration.has_value()) { + // This closing could lead to bad behavior of undo/redo stack when unselection create snapshot before close + close(); + return; + } + + // Not known situation when could happend this is only for sure + if (!m_is_unknown_font && !m_style_manager.is_active_font()) + create_notification_not_valid_font("No active font in style. Select correct one."); + else if (!m_is_unknown_font && !m_style_manager.get_wx_font().IsOk()) + create_notification_not_valid_font("WxFont is not loaded properly."); + + double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); + + // Orca + ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0, 5.0) * screen_scale); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 4.0f * screen_scale); + + // 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)); + + // change resolution regenerate icons + init_icons(); + m_style_manager.clear_imgui_font(); + } + + // Draw origin position of text during dragging + if (m_surface_drag.has_value()) { + ImVec2 mouse_pos = ImGui::GetMousePos(); + ImVec2 center( + mouse_pos.x + m_surface_drag->mouse_offset.x(), + mouse_pos.y + m_surface_drag->mouse_offset.y()); + ImU32 color = ImGui::GetColorU32( + m_surface_drag->exist_hit ? + ImVec4(1.f, 1.f, 1.f, .75f) : // transparent white + ImVec4(1.f, .3f, .3f, .75f) + ); // Warning color + const float radius = 16.f; + ImGuiWrapper::draw_cross_hair(center, radius, color); + } + +#ifdef SHOW_FINE_POSITION + draw_fine_position(m_parent.get_selection(), m_parent.get_canvas_size(), min_window_size); +#endif // SHOW_FINE_POSITION +#ifdef DRAW_PLACE_TO_ADD_TEXT + draw_place_to_add_text(); +#endif // DRAW_PLACE_TO_ADD_TEXT +#ifdef SHOW_OFFSET_DURING_DRAGGING + draw_mouse_offset(m_dragging_mouse_offset); +#endif // SHOW_OFFSET_DURING_DRAGGING + + static float last_y = 0.0f; + static float last_h = 0.0f; + + // adjust window position to avoid overlap the view toolbar + 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; + } + + GizmoImguiBegin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); + + draw_window(); + + GizmoImguiEnd(); + + // Orca + ImGui::PopStyleVar(2); + ImGuiWrapper::pop_toolbar_style(); +} + +void GLGizmoEmboss::on_set_state() +{ + // enable / disable bed from picking + // Rotation gizmo must work through bed + m_parent.set_raycaster_gizmos_on_top(m_state == GLGizmoBase::On); + + m_rotate_gizmo.set_state(m_state); + + // Closing gizmo. e.g. selecting another one + if (m_state == GLGizmoBase::Off) { + // refuse outgoing during text preview + reset_volume(); + // Store order and last activ index into app.ini + // TODO: what to do when can't store into file? + m_style_manager.store_styles_to_app_config(false); + remove_notification_not_valid_font(); + } else if (m_state == GLGizmoBase::On) { + // to reload fonts from system, when install new one + wxFontEnumerator::InvalidateCache(); + + // Immediately after set state On is called function data_changed(), + // where one could distiguish undo/redo serialization from opening by letter 'T' + // set_volume_by_selection(); + } +} + +void GLGizmoEmboss::data_changed(bool is_serializing) { + set_volume_by_selection(); + if (!is_serializing && m_volume == nullptr) + close(); +} + +void GLGizmoEmboss::on_start_dragging() { m_rotate_gizmo.start_dragging(); } +void GLGizmoEmboss::on_stop_dragging() +{ + m_rotate_gizmo.stop_dragging(); + + // 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 + m_parent.do_rotate(L("Text-Rotate")); + m_rotate_start_angle.reset(); + volume_transformation_changed(); +} +void GLGizmoEmboss::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(data); } + +EmbossStyles GLGizmoEmboss::create_default_styles() +{ + wxFontEnumerator::InvalidateCache(); + wxArrayString facenames = wxFontEnumerator::GetFacenames(Facenames::encoding); + + wxFont wx_font_normal = *wxNORMAL_FONT; +#ifdef __APPLE__ + // Set normal font to helvetica when possible + for (const wxString &facename : facenames) { + if (facename.IsSameAs("Helvetica")) { + wx_font_normal = wxFont(wxFontInfo().FaceName(facename).Encoding(Facenames::encoding)); + break; + } + } +#endif // __APPLE__ + + // https://docs.wxwidgets.org/3.0/classwx_font.html + // Predefined objects/pointers: wxNullFont, wxNORMAL_FONT, wxSMALL_FONT, wxITALIC_FONT, wxSWISS_FONT + EmbossStyles styles = { + WxFontUtils::create_emboss_style(wx_font_normal, _u8L("NORMAL")), // wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT) + WxFontUtils::create_emboss_style(*wxSMALL_FONT, _u8L("SMALL")), // A font using the wxFONTFAMILY_SWISS family and 2 points smaller than wxNORMAL_FONT. + WxFontUtils::create_emboss_style(*wxITALIC_FONT, _u8L("ITALIC")), // A font using the wxFONTFAMILY_ROMAN family and wxFONTSTYLE_ITALIC style and of the same size of wxNORMAL_FONT. + WxFontUtils::create_emboss_style(*wxSWISS_FONT, _u8L("SWISS")), // A font identic to wxNORMAL_FONT except for the family used which is wxFONTFAMILY_SWISS. + WxFontUtils::create_emboss_style(wxFont(10, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD), _u8L("MODERN")), + }; + + // Not all predefined font for wx must be valid TTF, but at least one style must be loadable + styles.erase(std::remove_if(styles.begin(), styles.end(), [](const EmbossStyle& style) { + wxFont wx_font = WxFontUtils::create_wxFont(style); + + // check that face name is setabled + if (style.prop.face_name.has_value()) { + wxString face_name = wxString::FromUTF8(style.prop.face_name->c_str()); + wxFont wx_font_temp; + if (!wx_font_temp.SetFaceName(face_name)) + return true; + } + + // Check that exsit valid TrueType Font for wx font + return WxFontUtils::create_font_file(wx_font) == nullptr; + }),styles.end() + ); + + // exist some valid style? + if (!styles.empty()) + return styles; + + // No valid style in defult list + // at least one style must contain loadable font + wxFont wx_font; + for (const wxString &face : facenames) { + wx_font = wxFont(face); + if (WxFontUtils::create_font_file(wx_font) != nullptr) + break; + wx_font = wxFont(); // NotOk + } + + if (wx_font.IsOk()) { + // use first alphabetic sorted installed font + styles.push_back(WxFontUtils::create_emboss_style(wx_font, _u8L("First font"))); + } else { + // On current OS is not installed any correct TTF font + // use font packed with Slic3r + std::string font_path = Slic3r::resources_dir() + "/fonts/NotoSans-Regular.ttf"; + styles.push_back(EmbossStyle{_u8L("Default font"), font_path, EmbossStyle::Type::file_path}); + } + return styles; +} + +namespace { + +/// +/// Check installed fonts whether optional face name exist in installed fonts +/// +/// Name from style - style.prop.face_name +/// All installed good and bad fonts - not const must be possible to initialize it +/// When it could be installed it contain value(potentionaly empty string) +std::optional get_installed_face_name(const std::optional &face_name_opt, ::Facenames& face_names) +{ + // Could exist OS without getter on face_name, + // but it is able to restore font from descriptor + // Soo default value must be TRUE + if (!face_name_opt.has_value()) + return wxString(); + + wxString face_name = wxString::FromUTF8(face_name_opt->c_str()); + + // search in enumerated fonts + // refresh list of installed font in the OS. + init_face_names(face_names); + face_names.is_init = false; + + auto cmp = [](const FaceName &fn, const wxString &wx_name) { return fn.wx_name < wx_name; }; + const std::vector &faces = face_names.faces; + // is font installed? + if (auto it = std::lower_bound(faces.begin(), faces.end(), face_name, cmp); + it != faces.end() && it->wx_name == face_name) + return face_name; + + const std::vector &bad = face_names.bad; + auto it_bad = std::lower_bound(bad.begin(), bad.end(), face_name); + if (it_bad == bad.end() || *it_bad != face_name) { + // check if wx allowed to set it up - another encoding of name + wxFontEnumerator::InvalidateCache(); + wxFont wx_font_; // temporary structure + if (wx_font_.SetFaceName(face_name) && WxFontUtils::create_font_file(wx_font_) != nullptr // can load TTF file? + ) { + return wxString(); + // QUESTION: add this name to allowed faces? + // Could create twin of font face name + // When not add it will be hard to select it again when change font + } + } + return {}; // not installed +} + +void init_text_lines(TextLinesModel &text_lines, const Selection& selection, /* const*/ StyleManager &style_manager, unsigned count_lines) +{ + const GLVolume *gl_volume_ptr = selection.get_first_volume(); + if (gl_volume_ptr == nullptr) + return; + const GLVolume &gl_volume = *gl_volume_ptr; + const ModelObjectPtrs &objects = selection.get_model()->objects; + const ModelVolume *mv_ptr = get_model_volume(gl_volume, objects); + if (mv_ptr == nullptr) + return; + const ModelVolume &mv = *mv_ptr; + if (mv.is_the_only_one_part()) + return; + + const std::optional &es_opt = mv.emboss_shape; + if (!es_opt.has_value()) + return; + const EmbossShape &es = *es_opt; + + const std::optional &tc_opt = mv.text_configuration; + if (!tc_opt.has_value()) + return; + const TextConfiguration &tc = *tc_opt; + + // calculate count lines when not set + if (count_lines == 0) { + count_lines = get_count_lines(tc.text); + if (count_lines == 0) + return; + } + + // prepare volumes to slice + ModelVolumePtrs volumes = prepare_volumes_to_slice(mv); + + // For interactivity during drag over surface it must be from gl_volume not volume. + Transform3d mv_trafo = gl_volume.get_volume_transformation().get_matrix(); + if (es.fix_3mf_tr.has_value()) + mv_trafo = mv_trafo * (es.fix_3mf_tr->inverse()); + text_lines.init(mv_trafo, volumes, style_manager, count_lines); +} +} + +void GLGizmoEmboss::reinit_text_lines(unsigned count_lines) { + init_text_lines(m_text_lines, m_parent.get_selection(), m_style_manager, count_lines); +} + +void GLGizmoEmboss::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 = m_parent.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 && + m_volume != nullptr && + volume->text_configuration->style == m_volume->text_configuration->style) + return; + + // for changed volume notification is NOT valid + remove_notification_not_valid_font(); + + // 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 selected volume text volume? + const std::optional &tc_opt = volume->text_configuration; + if (!tc_opt.has_value()) + return reset_volume(); + + // Emboss shape must be setted + assert(volume->emboss_shape.has_value()); + if (!volume->emboss_shape.has_value()) + return; + + const TextConfiguration &tc = *tc_opt; + const EmbossStyle &style = tc.style; + + std::optional installed_name = get_installed_face_name(style.prop.face_name, *m_face_names); + + wxFont wx_font; + // load wxFont from same OS when font name is installed + if (style.type == WxFontUtils::get_current_type() && installed_name.has_value()) + wx_font = WxFontUtils::load_wxFont(style.path); + + // Flag that is selected same font + bool is_exact_font = true; + // Different OS or try found on same OS + if (!wx_font.IsOk()) { + is_exact_font = false; + // Try create similar wx font by FontFamily + wx_font = WxFontUtils::create_wxFont(style); + if (installed_name.has_value() && !installed_name->empty()) + is_exact_font = wx_font.SetFaceName(*installed_name); + + // Have to use some wxFont + if (!wx_font.IsOk()) + wx_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + } + assert(wx_font.IsOk()); + + // Load style to style manager + const auto &styles = m_style_manager.get_styles(); + auto has_same_name = [&name = style.name](const StyleManager::Style &style_item) { return style_item.name == name; }; + + StyleManager::Style style_{style}; // copy + style_.projection = volume->emboss_shape->projection; + style_.angle = calc_angle(selection); + style_.distance = calc_distance(*gl_volume, m_raycast_manager, m_parent); + + if (auto it = std::find_if(styles.begin(), styles.end(), has_same_name); + it == styles.end()) { + // style was not found + m_style_manager.load_style(style_, wx_font); + } else { + // style name is in styles list + size_t style_index = it - styles.begin(); + if (!m_style_manager.load_style(style_index)) { + // can`t load stored style + m_style_manager.erase(style_index); + m_style_manager.load_style(style_, wx_font); + } else { + // stored style is loaded, now set modification of style + m_style_manager.get_style() = style_; + m_style_manager.set_wx_font(wx_font); + } + } + + if (!is_exact_font) + create_notification_not_valid_font(tc); + + // cancel previous job + if (m_job_cancel != nullptr) { + m_job_cancel->store(true); + m_job_cancel = nullptr; + } + + m_text = tc.text; + m_volume = volume; + m_volume_id = volume->id(); + + if (tc.style.prop.per_glyph) + reinit_text_lines(); + + // Calculate current angle of up vector + assert(m_style_manager.is_active_font()); + if (m_style_manager.is_active_font()) + m_style_manager.get_style().angle = calc_angle(selection); + + // calculate scale for height and depth inside of scaled object instance + calculate_scale(); +} + +void GLGizmoEmboss::reset_volume() +{ + if (m_volume == nullptr) + return; // already reseted + + m_volume = nullptr; + m_volume_id.id = 0; + + // No more need of current notification + remove_notification_not_valid_font(); +} + +void GLGizmoEmboss::calculate_scale() { + Transform3d to_world = m_parent.get_selection().get_first_volume()->world_matrix(); + auto to_world_linear = to_world.linear(); + auto calc = [&to_world_linear](const Vec3d &axe, std::optional& scale)->bool { + Vec3d axe_world = to_world_linear * axe; + double norm_sq = axe_world.squaredNorm(); + if (is_approx(norm_sq, 1.)) { + if (scale.has_value()) + scale.reset(); + else + return false; + } else { + scale = sqrt(norm_sq); + } + return true; + }; + + bool exist_change = calc(Vec3d::UnitY(), m_scale_height); + exist_change |= calc(Vec3d::UnitZ(), m_scale_depth); + + // Change of scale has to change font imgui font size + if (exist_change) + m_style_manager.clear_imgui_font(); +} + +namespace { +bool is_text_empty(std::string_view text) { return text.empty() || text.find_first_not_of(" \n\t\r") == std::string::npos; } +} // namespace + +bool GLGizmoEmboss::process() +{ + // no volume is selected -> selection from right panel + assert(m_volume != nullptr); + if (m_volume == nullptr) return false; + + // without text there is nothing to emboss + if (is_text_empty(m_text)) return false; + + // exist loaded font file? + if (!m_style_manager.is_active_font()) return false; + + const Selection& selection = m_parent.get_selection(); + DataBasePtr base = create_emboss_data_base(m_text, m_style_manager, m_text_lines, selection, m_volume->type(), m_job_cancel); + DataUpdate data{std::move(base), m_volume->id()}; + + // check valid count of text lines + assert(data.base->text_lines.empty() || data.base->text_lines.size() == get_count_lines(m_text)); + + if (!start_update_volume(std::move(data), *m_volume, selection, m_raycast_manager)) + return false; + + // notification is removed befor object is changed by job + remove_notification_not_valid_font(); + return true; +} + +void GLGizmoEmboss::close() +{ + // remove volume when text is empty + if (m_volume != nullptr && + m_volume->text_configuration.has_value() && + is_text_empty(m_text)) { + Plater &p = *wxGetApp().plater(); + // is the text object? + if (m_volume->is_the_only_one_part()) { + // delete whole object + p.remove(m_parent.get_selection().get_object_idx()); + } else { + // delete text volume + p.remove_selected(); + } + } + + // close gizmo == open it again + auto& mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Emboss) + mng.open_gizmo(GLGizmosManager::Emboss); +} + +void GLGizmoEmboss::draw_window() +{ +#ifdef ALLOW_DEBUG_MODE + if (ImGui::Button("re-process")) process(); +#endif // ALLOW_DEBUG_MODE + + // Setter of indent must be befor disable !!! + ImGui::PushStyleVar(ImGuiStyleVar_IndentSpacing, m_gui_cfg->indent); + ScopeGuard indent_sc([](){ ImGui::PopStyleVar(/*ImGuiStyleVar_IndentSpacing*/); }); + + // Disable all except selection of font, when open text from 3mf with unknown font + m_imgui->disabled_begin(m_is_unknown_font); + ScopeGuard unknown_font_sc([imgui = m_imgui]() { imgui->disabled_end(/*m_is_unknown_font*/); }); + + draw_text_input(); + + ImGui::Indent(); + // When unknown font is inside .3mf only font selection is allowed + m_imgui->disabled_end(/*m_is_unknown_font*/); + draw_font_list_line(); + m_imgui->disabled_begin(m_is_unknown_font); + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + draw_height(use_inch); + draw_depth(use_inch); + ImGui::Unindent(); + + // close advanced style property when unknown font is selected + if (m_is_unknown_font && m_is_advanced_edit_style) + ImGui::SetNextTreeNodeOpen(false); + + if (ImGui::TreeNode(_u8L("Advanced").c_str())) { + if (!m_is_advanced_edit_style) { + m_is_advanced_edit_style = true; + m_imgui->set_requires_extra_frame(); + } else { + draw_advanced(); + } + ImGui::TreePop(); + } else if (m_is_advanced_edit_style) { + m_is_advanced_edit_style = false; + m_imgui->set_requires_extra_frame(); + } + + ImGui::Separator(); + + draw_style_list(); + + // Do not select volume type, when it is text object + if (!m_volume->is_the_only_one_part()) { + ImGui::Separator(); + draw_model_type(); + } + +#ifdef SHOW_WX_FONT_DESCRIPTOR + if (is_selected_style) + m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, m_style_manager.get_style().path); +#endif // SHOW_WX_FONT_DESCRIPTOR + +#ifdef SHOW_CONTAIN_3MF_FIX + if (m_volume!=nullptr && + m_volume->text_configuration.has_value() && + m_volume->text_configuration->fix_3mf_tr.has_value()) { + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, ".3mf"); + if (ImGui::IsItemHovered()) { + Transform3d &fix = *m_volume->text_configuration->fix_3mf_tr; + std::stringstream ss; + ss << fix.matrix(); + std::string filename = (m_volume->source.input_file.empty())? "unknown.3mf" : + m_volume->source.input_file + ".3mf"; + ImGui::SetTooltip("Text configuation contain \n" + "Fix Transformation Matrix \n" + "%s\n" + "loaded from \"%s\" file.", + ss.str().c_str(), filename.c_str() + ); + } + } +#endif // SHOW_CONTAIN_3MF_FIX +#ifdef SHOW_ICONS_TEXTURE + auto &t = m_icons_texture; + ImGui::Image((void *) t.get_id(), ImVec2(t.get_width(), t.get_height())); +#endif //SHOW_ICONS_TEXTURE +#ifdef SHOW_IMGUI_ATLAS + const auto &atlas = m_style_manager.get_atlas(); + ImGui::Image(atlas.TexID, ImVec2(atlas.TexWidth, atlas.TexHeight)); +#endif // SHOW_IMGUI_ATLAS + +#ifdef ALLOW_OPEN_NEAR_VOLUME + ImGui::SameLine(); + if (ImGui::Checkbox("##ALLOW_OPEN_NEAR_VOLUME", &m_allow_open_near_volume)) { + if (m_allow_open_near_volume) + m_set_window_offset = calc_fine_position(m_parent.get_selection(), get_minimal_window_size(), m_parent.get_canvas_size()); + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", ((m_allow_open_near_volume) ? + "Fix settings position": + "Allow floating window near text").c_str()); + } +#endif // ALLOW_FLOAT_WINDOW + } + +#include "imgui/imgui_internal.h" // scroll bar existence + +void GLGizmoEmboss::draw_text_input() +{ + auto create_range_text_prep = [&mng = m_style_manager, &text = m_text, &exist_unknown = m_text_contain_unknown_glyph]() { + auto& ff = mng.get_font_file_with_cache(); + assert(ff.has_value()); + const auto &cn = mng.get_font_prop().collection_number; + unsigned int font_index = (cn.has_value()) ? *cn : 0; + return create_range_text(text, *ff.font_file, font_index, &exist_unknown); + }; + + double scale = m_scale_height.has_value() ? *m_scale_height : 1.; + ImFont *imgui_font = m_style_manager.get_imgui_font(); + if (imgui_font == nullptr) { + // try create new imgui font + double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); + double imgui_scale = scale * screen_scale; + m_style_manager.create_imgui_font(create_range_text_prep(), imgui_scale); + imgui_font = m_style_manager.get_imgui_font(); + } + bool exist_font = + imgui_font != nullptr && + imgui_font->IsLoaded() && + imgui_font->Scale > 0.f && + imgui_font->ContainerAtlas != nullptr; + // NOTE: Symbol fonts doesn't have atlas + // when their glyph range is out of language character range + if (exist_font) ImGui::PushFont(imgui_font); + + // show warning about incorrectness view of font + std::string warning_tool_tip; + if (!exist_font) { + warning_tool_tip = _u8L("The text cannot be written using the selected font. Please try choosing a different font."); + } else { + auto append_warning = [&warning_tool_tip](std::string t) { + if (!warning_tool_tip.empty()) + warning_tool_tip += "\n"; + warning_tool_tip += t; + }; + if (is_text_empty(m_text)) + append_warning(_u8L("Embossed text cannot contain only white spaces.")); + if (m_text_contain_unknown_glyph) + append_warning(_u8L("Text contains character glyph (represented by '?') unknown by font.")); + + const FontProp &prop = m_style_manager.get_font_prop(); + if (prop.skew.has_value()) append_warning(_u8L("Text input doesn't show font skew.")); + if (prop.boldness.has_value()) append_warning(_u8L("Text input doesn't show font boldness.")); + if (prop.line_gap.has_value()) append_warning(_u8L("Text input doesn't show gap between lines.")); + auto &ff = m_style_manager.get_font_file_with_cache(); + float imgui_size = StyleManager::get_imgui_font_size(prop, *ff.font_file, scale); + if (imgui_size > StyleManager::max_imgui_font_size) + append_warning(_u8L("Too tall, diminished font height inside text input.")); + if (imgui_size < StyleManager::min_imgui_font_size) + append_warning(_u8L("Too small, enlarged font height inside text input.")); + bool is_multiline = m_text_lines.get_lines().size() > 1; + if (is_multiline && (prop.align.first == FontProp::HorizontalAlign::center || prop.align.first == FontProp::HorizontalAlign::right)) + append_warning(_u8L("Text doesn't show current horizontal alignment.")); + } + + // flag for extend font ranges if neccessary + // ranges can't be extend during font is activ(pushed) + std::string range_text; + ImVec2 input_size(m_gui_cfg->text_size.x, m_gui_cfg->text_size.y); + const ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_AutoSelectAll; + if (ImGui::InputTextMultiline("##Text", &m_text, input_size, flags)) { + if (m_style_manager.get_font_prop().per_glyph) { + unsigned count_lines = get_count_lines(m_text); + if (count_lines != m_text_lines.get_lines().size()) + // Necesarry to initialize count by given number (differ from stored in volume at the moment) + reinit_text_lines(count_lines); + } + process(); + range_text = create_range_text_prep(); + } + + if (exist_font) ImGui::PopFont(); + + // warning tooltip has to be with default font + if (!warning_tool_tip.empty()) { + // Multiline input has hidden window for scrolling + const ImGuiWindow *input = ImGui::GetCurrentWindow()->DC.ChildWindows.front(); + const ImGuiStyle &style = ImGui::GetStyle(); + float scrollbar_width = (input->ScrollbarY) ? style.ScrollbarSize : 0.f; + float scrollbar_height = (input->ScrollbarX) ? style.ScrollbarSize : 0.f; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(warning_tool_tip, m_gui_cfg->max_tooltip_width); + + ImVec2 cursor = ImGui::GetCursorPos(); + float width = ImGui::GetContentRegionAvailWidth(); + const ImVec2& padding = style.FramePadding; + ImVec2 icon_pos(width - m_gui_cfg->icon_width - scrollbar_width + padding.x, + cursor.y - m_gui_cfg->icon_width - scrollbar_height - 2*padding.y); + + ImGui::SetCursorPos(icon_pos); + draw(get_icon(m_icons, IconType::exclamation, IconState::hovered)); + ImGui::SetCursorPos(cursor); + } + + // NOTE: must be after ImGui::font_pop() + // -> imgui_font has to be unused + // IMPROVE: only extend not clear + // Extend font ranges + if (!range_text.empty() && + !ImGuiWrapper::contain_all_glyphs(imgui_font, range_text) ) + m_style_manager.clear_imgui_font(); +} + +// create texture for visualization font face +void GLGizmoEmboss::init_font_name_texture() { + Timer t("init_font_name_texture"); + // check if already exists + GLuint &id = m_face_names->texture_id; + if (id != 0) return; + // create texture for font + GLenum target = GL_TEXTURE_2D; + glsafe(::glGenTextures(1, &id)); + glsafe(::glBindTexture(target, id)); + glsafe(::glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); + glsafe(::glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); + const Vec2i &size = m_gui_cfg->face_name_size; + GLint w = size.x(), h = m_face_names->count_cached_textures * size.y(); + std::vector data(4*w * h, {0}); + const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; + const GLint level = 0, internal_format = GL_RGBA, border = 0; + glsafe(::glTexImage2D(target, level, internal_format, w, h, border, format, type, data.data())); + + // bind default texture + GLuint no_texture_id = 0; + glsafe(::glBindTexture(target, no_texture_id)); + + // clear info about creation of texture - no one is initialized yet + for (FaceName &face : m_face_names->faces) { + face.cancel = nullptr; + face.is_created = nullptr; + } + + // Prepare filtration cache + //m_face_names->hide = std::vector(m_face_names->faces.size(), {false}); +} + +bool GLGizmoEmboss::select_facename(const wxString &facename) +{ + if (!wxFontEnumerator::IsValidFacename(facename)) return false; + // Select font + wxFont wx_font(wxFontInfo().FaceName(facename).Encoding(Facenames::encoding)); + if (!wx_font.IsOk()) return false; +#ifdef USE_PIXEL_SIZE_IN_WX_FONT + // wx font could change source file by size of font + int point_size = static_cast(m_style_manager.get_font_prop().size_in_mm); + wx_font.SetPointSize(point_size); +#endif // USE_PIXEL_SIZE_IN_WX_FONT + if (!m_style_manager.set_wx_font(wx_font)) return false; + process(); + return true; +} + +void GLGizmoEmboss::push_button_style(bool pressed) +{ + if (m_is_dark_mode) { + if (pressed) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(43 / 255.f, 64 / 255.f, 54 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(43 / 255.f, 64 / 255.f, 54 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(43 / 255.f, 64 / 255.f, 54 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.f, 174 / 255.f, 66 / 255.f, 1.f)); + } + else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(45.f / 255.f, 45.f / 255.f, 49.f / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(84 / 255.f, 84 / 255.f, 90 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(84 / 255.f, 84 / 255.f, 90 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(45.f / 255.f, 45.f / 255.f, 49.f / 255.f, 1.f)); + } + } + else { + if (pressed) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(219 / 255.f, 253 / 255.f, 231 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(219 / 255.f, 253 / 255.f, 231 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(219 / 255.f, 253 / 255.f, 231 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.f, 174 / 255.f, 66 / 255.f, 1.f)); + } + else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.f, 1.f, 1.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(238 / 255.f, 238 / 255.f, 238 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(238 / 255.f, 238 / 255.f, 238 / 255.f, 1.f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.f, 1.f, 1.f, 1.f)); + } + + } +} + +void GLGizmoEmboss::pop_button_style() +{ + ImGui::PopStyleColor(4); +} + +void GLGizmoEmboss::draw_font_list_line() +{ + ImGui::AlignTextToFramePadding(); + + bool exist_stored_style = m_style_manager.exist_stored_style(); + bool exist_change_in_font = m_style_manager.is_font_changed(); + const std::string& font_text = m_gui_cfg->translations.font; + if (exist_change_in_font || !exist_stored_style) + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORCA, font_text); + else + ImGuiWrapper::text(font_text); + + ImGui::SameLine(m_gui_cfg->input_offset); + + draw_font_list(); + + bool exist_change = false; + if (!m_is_unknown_font) { + ImGui::SameLine(); + if (draw_italic_button()) + exist_change = true; + ImGui::SameLine(); + if (draw_bold_button()) + exist_change = true; + } else { + // when exist unknown font add confirmation button + ImGui::SameLine(); + // Apply for actual selected font + if (ImGui::Button(_u8L("Apply").c_str())) + exist_change = true; + } + + EmbossStyle &style = m_style_manager.get_style(); + if (exist_change_in_font) { + ImGui::SameLine(ImGui::GetStyle().WindowPadding.x); + if (draw_button(m_icons, IconType::undo)) { + const EmbossStyle *stored_style = m_style_manager.get_stored_style(); + + style.path = stored_style->path; + style.prop.boldness = stored_style->prop.boldness; + style.prop.skew = stored_style->prop.skew; + + wxFont new_wx_font = WxFontUtils::load_wxFont(style.path); + if (new_wx_font.IsOk() && m_style_manager.set_wx_font(new_wx_font)) + exist_change = true; + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Revert font changes."), m_gui_cfg->max_tooltip_width); + } + + if (exist_change) { + m_style_manager.clear_glyphs_cache(); + if (m_style_manager.get_font_prop().per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); + process(); + } +} + +void GLGizmoEmboss::draw_font_list() +{ + ImGuiWrapper::push_combo_style(m_gui_cfg->screen_scale); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 4.0f * m_gui_cfg->screen_scale); + + wxString tooltip_name = ""; + + // Set partial + wxString actual_face_name; + if (m_style_manager.is_active_font()) { + const wxFont &wx_font = m_style_manager.get_wx_font(); + if (wx_font.IsOk()) + actual_face_name = wx_font.GetFaceName(); + } + // name of actual selected font + const char * selected = (!actual_face_name.empty()) ? + (const char *)actual_face_name.c_str() : " --- "; + + // Do not remove font face during enumeration + // When deletation of font appear this variable is set + std::optional del_index; + + ImGui::SetNextItemWidth(2 * m_gui_cfg->input_width); + std::vector filtered_items_idx; + bool is_filtered = false; + if (m_imgui->bbl_combo_with_filter("##Combo_Font", selected, m_face_names->faces_names, + &filtered_items_idx, &is_filtered, m_imgui->scaled(32.f / 15.f))) { + bool set_selection_focus = false; + if (!m_face_names->is_init) { + init_face_names(*m_face_names); + set_selection_focus = true; + } + + if (!m_face_names->has_truncated_names) + init_truncated_names(*m_face_names, m_gui_cfg->input_width); + + if (m_face_names->texture_id == 0) + init_font_name_texture(); + + int show_items_count = is_filtered ? filtered_items_idx.size() : m_face_names->faces.size(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0); + + for (int i = 0; i < show_items_count; i++) { + int idx = is_filtered ? filtered_items_idx[i] : i; + FaceName &face = m_face_names->faces[idx]; + const wxString &wx_face_name = face.wx_name; + + ImGui::PushID(idx); + ScopeGuard sg([]() { ImGui::PopID(); }); + bool is_selected = (actual_face_name == wx_face_name); + ImVec2 selectable_size(0, m_imgui->scaled(32.f / 15.f)); + ImGuiSelectableFlags flags = 0; + if (ImGui::BBLSelectable(face.name_truncated.c_str(), is_selected, flags, selectable_size)) { + if (!select_facename(wx_face_name)) { + del_index = idx; + MessageDialog(wxGetApp().plater(), GUI::format_wxstr(_L("Font \"%1%\" can't be selected."), wx_face_name)); + } else { + ImGui::CloseCurrentPopup(); + } + } + // tooltip as full name of font face + if (ImGui::IsItemHovered()) + tooltip_name = wx_face_name; + + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + + // on first draw set focus on selected font + if (set_selection_focus && is_selected) + ImGui::SetScrollHereY(); + ::draw_font_preview(face, m_text, *m_face_names, *m_gui_cfg, ImGui::IsItemVisible()); + } + + ImGui::PopStyleVar(3); + ImGui::EndListBox(); + ImGui::EndPopup(); + } else if (m_face_names->is_init) { + // Just one after close combo box + // free texture and set id to zero + m_face_names->is_init = false; + // cancel all process for generation of texture + for (FaceName &face : m_face_names->faces) + if (face.cancel != nullptr) + face.cancel->store(true); + glsafe(::glDeleteTextures(1, &m_face_names->texture_id)); + m_face_names->texture_id = 0; + } + + // delete unloadable face name when try to use + if (del_index.has_value()) { + auto face = m_face_names->faces.begin() + (*del_index); + std::vector& bad = m_face_names->bad; + // sorted insert into bad fonts + auto it = std::upper_bound(bad.begin(), bad.end(), face->wx_name); + bad.insert(it, face->wx_name); + m_face_names->faces.erase(face); + m_face_names->faces_names.erase(m_face_names->faces_names.begin() + (*del_index)); + // update cached file + store(*m_face_names); + } + +#ifdef ALLOW_ADD_FONT_BY_FILE + ImGui::SameLine(); + // select font file by file browser + if (draw_button(IconType::open_file)) { + if (choose_true_type_file()) { + process(); + } + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Add file with font(.ttf, .ttc)"); +#endif // ALLOW_ADD_FONT_BY_FILE + +#ifdef ALLOW_ADD_FONT_BY_OS_SELECTOR + ImGui::SameLine(); + if (draw_button(IconType::system_selector)) { + if (choose_font_by_wxdialog()) { + process(); + } + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Open dialog for choose from fonts."); +#endif // ALLOW_ADD_FONT_BY_OS_SELECTOR + + ImGui::PopStyleVar(2); + ImGuiWrapper::pop_combo_style(); + + if (!tooltip_name.IsEmpty()) + m_imgui->tooltip(tooltip_name, m_gui_cfg->max_tooltip_width); +} + +void GLGizmoEmboss::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(); + Plater::TakeSnapshot snapshot(plater, _u8L("Change Text 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(); + + // 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::Emboss) + mng.open_gizmo(GLGizmosManager::Emboss); + // TODO: select volume back - Ask @Sasa + } +} + +void GLGizmoEmboss::draw_style_rename_popup() { + std::string& new_name = m_style_manager.get_style().name; + const std::string &old_name = m_style_manager.get_stored_style()->name; + std::string text_in_popup = GUI::format(_L("Rename style(%1%) for embossing text"), old_name) + ": "; + ImGui::Text("%s", text_in_popup.c_str()); + + bool is_unique = (new_name == old_name) || // could be same as before rename + m_style_manager.is_unique_style_name(new_name); + + bool allow_change = false; + if (new_name.empty()) { + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name can't be empty.")); + }else if (!is_unique) { + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name has to be unique.")); + } else { + ImGui::NewLine(); + allow_change = true; + } + + bool store = false; + ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue; + if (ImGui::InputText("##rename style", &new_name, flags) && allow_change) store = true; + if (m_imgui->button(_L("OK"), ImVec2(0.f, 0.f), allow_change)) store = true; + ImGui::SameLine(); + if (ImGui::Button(_u8L("Cancel").c_str())) { + new_name = old_name; + ImGui::CloseCurrentPopup(); + } + + if (store) { + // rename style in all objects and volumes + for (const ModelObject *mo :wxGetApp().plater()->model().objects) { + for (ModelVolume *mv : mo->volumes) { + if (!mv->text_configuration.has_value()) continue; + std::string& name = mv->text_configuration->style.name; + if (name != old_name) continue; + name = new_name; + } + } + + m_style_manager.rename(new_name); + m_style_manager.store_styles_to_app_config(); + ImGui::CloseCurrentPopup(); + } +} + +void GLGizmoEmboss::draw_style_rename_button() +{ + bool can_rename = m_style_manager.exist_stored_style(); + std::string title = _u8L("Rename style"); + const char * popup_id = title.c_str(); + if (draw_button(m_icons, IconType::rename, !can_rename)) { + assert(m_style_manager.get_stored_style()); + ImGui::OpenPopup(popup_id); + } + else if (ImGui::IsItemHovered()) { + if (can_rename) m_imgui->tooltip(_u8L("Rename current style."), m_gui_cfg->max_tooltip_width); + else m_imgui->tooltip(_u8L("Can't rename temporary style."), m_gui_cfg->max_tooltip_width); + } + if (ImGui::BeginPopupModal(popup_id, 0, ImGuiWindowFlags_AlwaysAutoResize)) { + m_imgui->disable_background_fadeout_animation(); + draw_style_rename_popup(); + ImGui::EndPopup(); + } +} + +void GLGizmoEmboss::draw_style_save_button(bool is_modified) +{ + if (draw_button(m_icons, IconType::save, !is_modified)) { + // save styles to app config + m_style_manager.store_styles_to_app_config(); + }else if (ImGui::IsItemHovered()) { + std::string tooltip; + if (!m_style_manager.exist_stored_style()) { + tooltip = _u8L("First Add style to list."); + } else if (is_modified) { + tooltip = GUI::format(_L("Save %1% style"), m_style_manager.get_style().name); + } else { + tooltip = _u8L("No changes to save."); + } + m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); + } +} + +void GLGizmoEmboss::draw_style_save_as_popup() { + ImGui::Text("%s", (_u8L("New name of style") +": ").c_str()); + + // use name inside of volume configuration as temporary new name + std::string &new_name = m_volume->text_configuration->style.name; + bool is_unique = m_style_manager.is_unique_style_name(new_name); + bool allow_change = false; + if (new_name.empty()) { + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name can't be empty.")); + }else if (!is_unique) { + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name has to be unique.")); + } else { + ImGui::NewLine(); + allow_change = true; + } + + bool save_style = false; + ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue; + if (ImGui::InputText("##save as style", &new_name, flags)) + save_style = true; + + if (m_imgui->button(_L("OK"), ImVec2(0.f, 0.f), allow_change)) + save_style = true; + + ImGui::SameLine(); + if (ImGui::Button(_u8L("Cancel").c_str())){ + // write original name to volume TextConfiguration + new_name = m_style_manager.get_style().name; + ImGui::CloseCurrentPopup(); + } + + if (save_style && allow_change) { + m_style_manager.add_style(new_name); + m_style_manager.store_styles_to_app_config(); + ImGui::CloseCurrentPopup(); + } +} + +void GLGizmoEmboss::draw_style_add_button() +{ + bool only_add_style = !m_style_manager.exist_stored_style(); + bool can_add = true; + if (only_add_style && + m_volume->text_configuration->style.type != WxFontUtils::get_current_type()) + can_add = false; + + std::string title = _u8L("Save as new style"); + const char *popup_id = title.c_str(); + // save as new style + ImGui::SameLine(); + if (draw_button(m_icons, IconType::add, !can_add)) { + if (!m_style_manager.exist_stored_style()) { + m_style_manager.store_styles_to_app_config(wxGetApp().app_config); + } else { + ImGui::OpenPopup(popup_id); + } + } else if (ImGui::IsItemHovered()) { + if (!can_add) { + m_imgui->tooltip(_u8L("Only valid font can be added to style."), m_gui_cfg->max_tooltip_width); + } else if (only_add_style) { + m_imgui->tooltip(_u8L("Add style to my list."), m_gui_cfg->max_tooltip_width); + } else { + m_imgui->tooltip(_u8L("Save as new style."), m_gui_cfg->max_tooltip_width); + } + } + + if (ImGui::BeginPopupModal(popup_id, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + m_imgui->disable_background_fadeout_animation(); + draw_style_save_as_popup(); + ImGui::EndPopup(); + } +} + +void GLGizmoEmboss::draw_delete_style_button() { + bool is_stored = m_style_manager.exist_stored_style(); + bool is_last = m_style_manager.get_styles().size() == 1; + bool can_delete = is_stored && !is_last; + + if (draw_button(m_icons, IconType::erase, !can_delete)) { + std::string style_name = m_style_manager.get_style().name; // copy + wxString dialog_title = _L("Remove style"); + size_t next_style_index = std::numeric_limits::max(); + Plater *plater = wxGetApp().plater(); + bool exist_change = false; + while (true) { + // NOTE: can't use previous loaded activ index -> erase could change index + size_t active_index = m_style_manager.get_style_index(); + next_style_index = (active_index > 0) ? active_index - 1 : + active_index + 1; + + if (next_style_index >= m_style_manager.get_styles().size()) { + MessageDialog msg(plater, _L("Can't remove the last existing style."), dialog_title, wxICON_ERROR | wxOK); + msg.ShowModal(); + break; + } + + // IMPROVE: add function can_load? + // clean unactivable styles + if (!m_style_manager.load_style(next_style_index)) { + m_style_manager.erase(next_style_index); + exist_change = true; + continue; + } + + wxString message = GUI::format_wxstr(_L("Are you sure you want to permanently remove the \"%1%\" style?"), style_name); + MessageDialog msg(plater, message, dialog_title, wxICON_WARNING | wxYES | wxNO); + if (msg.ShowModal() == wxID_YES) { + // delete style + m_style_manager.erase(active_index); + exist_change = true; + process(); + } else { + // load back style + m_style_manager.load_style(active_index); + } + break; + } + if (exist_change) + m_style_manager.store_styles_to_app_config(wxGetApp().app_config); + } + + if (ImGui::IsItemHovered()) { + const std::string &style_name = m_style_manager.get_style().name; + std::string tooltip; + if (can_delete) tooltip = GUI::format(_L("Delete \"%1%\" style."), style_name); + else if (is_last) tooltip = GUI::format(_L("Can't delete \"%1%\". It is last style."), style_name); + else/*if(!is_stored)*/ tooltip = GUI::format(_L("Can't delete temporary style \"%1%\"."), style_name); + m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); + } +} + +namespace { +// FIX IT: It should not change volume position before successfull change volume by process +void fix_transformation(const StyleManager::Style &from, const StyleManager::Style &to, GLCanvas3D &canvas) { + // fix Z rotation when exists difference in styles + const std::optional &f_angle_opt = from.angle; + const std::optional &t_angle_opt = to.angle; + if (!is_approx(f_angle_opt, t_angle_opt)) { + // fix rotation + float f_angle = f_angle_opt.value_or(.0f); + float t_angle = t_angle_opt.value_or(.0f); + do_local_z_rotate(canvas, t_angle - f_angle); + } + + // fix distance (Z move) when exists difference in styles + const std::optional &f_move_opt = from.distance; + const std::optional &t_move_opt = to.distance; + if (!is_approx(f_move_opt, t_move_opt)) { + float f_move = f_move_opt.value_or(.0f); + float t_move = t_move_opt.value_or(.0f); + do_local_z_move(canvas, t_move - f_move); + } +} +} // namesapce + +void GLGizmoEmboss::draw_style_list() { + if (!m_style_manager.is_active_font()) return; + + const StyleManager::Style *stored_style = nullptr; + bool is_stored = m_style_manager.exist_stored_style(); + if (is_stored) + stored_style = m_style_manager.get_stored_style(); + const StyleManager::Style ¤t_style = m_style_manager.get_style(); + bool is_changed = (stored_style)? !(*stored_style == current_style) : true; + bool is_modified = is_stored && is_changed; + + const float &max_style_name_width = m_gui_cfg->max_style_name_width; + std::string &trunc_name = m_style_manager.get_truncated_name(); + if (trunc_name.empty()) { + // generate trunc name + std::string current_name = current_style.name; + ImGuiWrapper::escape_double_hash(current_name); + trunc_name = ImGuiWrapper::trunc(current_name, max_style_name_width); + } + + std::string title = _u8L("Style"); + if (m_style_manager.exist_stored_style()) + ImGui::Text("%s", title.c_str()); + else + ImGui::TextColored(ImGuiWrapper::COL_ORCA, "%s", title.c_str()); + + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + auto add_text_modify = [&is_modified](const std::string& name) { + if (!is_modified) return name; + return name + Preset::suffix_modified(); + }; + std::optional selected_style_index; + std::string tooltip = ""; + ImGuiWrapper::push_combo_style(m_parent.get_scale()); + if (ImGui::BBLBeginCombo("##style_selector", add_text_modify(trunc_name).c_str())) { + m_style_manager.init_style_images(m_gui_cfg->max_style_image_size, m_text); + m_style_manager.init_trunc_names(max_style_name_width); + std::optional> swap_indexes; + const StyleManager::Styles &styles = m_style_manager.get_styles(); + for (const StyleManager::Style &style : styles) { + size_t index = &style - &styles.front(); + const std::string &actual_style_name = style.name; + ImGui::PushID(actual_style_name.c_str()); + bool is_selected = (index == m_style_manager.get_style_index()); + + float select_height = static_cast(m_gui_cfg->max_style_image_size.y()); + ImVec2 select_size(0.f, select_height); // 0,0 --> calculate in draw + const std::optional &img = style.image; + // allow click delete button + ImGuiSelectableFlags_ flags = ImGuiSelectableFlags_AllowItemOverlap; + if (ImGui::BBLSelectable(style.truncated_name.c_str(), is_selected, flags, select_size)) { + selected_style_index = index; + } else if (ImGui::IsItemHovered()) + tooltip = actual_style_name; + + // reorder items + if (ImGui::IsItemActive() && !ImGui::IsItemHovered()) { + if (ImGui::GetMouseDragDelta(0).y < 0.f) { + if (index > 0) + swap_indexes = {index, index - 1}; + } else if ((index + 1) < styles.size()) + swap_indexes = {index, index + 1}; + if (swap_indexes.has_value()) + ImGui::ResetMouseDragDelta(); + } + + // draw style name + if (img.has_value()) { + ImGui::SameLine(max_style_name_width); + ImVec4 tint_color = ImGui::GetStyleColorVec4(ImGuiCol_Text); + ImGui::Image(img->texture_id, img->tex_size, img->uv0, img->uv1, tint_color); + } + + ImGui::PopID(); + } + if (swap_indexes.has_value()) + m_style_manager.swap(swap_indexes->first, + swap_indexes->second); + ImGui::EndCombo(); + } else { + // do not keep in memory style images when no combo box open + m_style_manager.free_style_images(); + if (ImGui::IsItemHovered()) { + std::string style_name = add_text_modify(current_style.name); + tooltip = is_modified? + GUI::format(_L("Modified style \"%1%\""), current_style.name): + GUI::format(_L("Current style is \"%1%\""), current_style.name); + } + } + ImGuiWrapper::pop_combo_style(); + if (!tooltip.empty()) + m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); + + // Check whether user wants lose actual style modification + if (selected_style_index.has_value() && is_modified) { + const std::string & style_name = m_style_manager.get_styles()[*selected_style_index].name; + wxString message = GUI::format_wxstr(_L("Changing style to \"%1%\" will discard current style modification.\n\nWould you like to continue anyway?"), style_name); + MessageDialog not_loaded_style_message(nullptr, message, _L("Warning"), wxICON_WARNING | wxYES | wxNO); + if (not_loaded_style_message.ShowModal() != wxID_YES) + selected_style_index.reset(); + } + + // selected style from combo box + if (selected_style_index.has_value()) { + const StyleManager::Style &style = m_style_manager.get_styles()[*selected_style_index]; + // create copy to be able do fix transformation only when successfully load style + StyleManager::Style cur_s = current_style; // copy + StyleManager::Style new_s = style; // copy + if (m_style_manager.load_style(*selected_style_index)) { + ::fix_transformation(cur_s, new_s, m_parent); + process(); + } else { + wxString title = _L("Not valid style."); + wxString message = GUI::format_wxstr(_L("Style \"%1%\" can't be used and will be removed from a list."), style.name); + MessageDialog not_loaded_style_message(nullptr, message, title, wxOK); + not_loaded_style_message.ShowModal(); + m_style_manager.erase(*selected_style_index); + } + } + + ImGui::SameLine(); + draw_style_rename_button(); + + ImGui::SameLine(); + draw_style_save_button(is_modified); + + ImGui::SameLine(); + draw_style_add_button(); + + // delete button + ImGui::SameLine(); + draw_delete_style_button(); +} + +bool GLGizmoEmboss::draw_italic_button() +{ + const wxFont &wx_font = m_style_manager.get_wx_font(); + const auto& ff = m_style_manager.get_font_file_with_cache(); + if (!wx_font.IsOk() || !ff.has_value()) { + draw(get_icon(m_icons, IconType::italic, IconState::disabled)); + return false; + } + + std::optional &skew = m_style_manager.get_font_prop().skew; + bool is_font_italic = skew.has_value() || WxFontUtils::is_italic(wx_font); + if (is_font_italic) { + // unset italic + if (clickable(get_icon(m_icons, IconType::italic, IconState::hovered), + get_icon(m_icons, IconType::unitalic, IconState::hovered))) { + skew.reset(); + if (wx_font.GetStyle() != wxFontStyle::wxFONTSTYLE_NORMAL) { + wxFont new_wx_font = wx_font; // copy + new_wx_font.SetStyle(wxFontStyle::wxFONTSTYLE_NORMAL); + if(!m_style_manager.set_wx_font(new_wx_font)) + return false; + } + return true; + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Unset italic"), m_gui_cfg->max_tooltip_width); + } else { + // set italic + if (draw_button(m_icons, IconType::italic)) { + wxFont new_wx_font = wx_font; // copy + auto new_ff = WxFontUtils::set_italic(new_wx_font, *ff.font_file); + if (new_ff != nullptr) { + if(!m_style_manager.set_wx_font(new_wx_font, std::move(new_ff))) + return false; + } else { + // italic font doesn't exist + // add skew when wxFont can't set it + skew = 0.2f; + } + return true; + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Set italic"), m_gui_cfg->max_tooltip_width); + } + return false; +} + +bool GLGizmoEmboss::draw_bold_button() { + const wxFont &wx_font = m_style_manager.get_wx_font(); + const auto& ff = m_style_manager.get_font_file_with_cache(); + if (!wx_font.IsOk() || !ff.has_value()) { + draw(get_icon(m_icons, IconType::bold, IconState::disabled)); + return false; + } + + std::optional &boldness = m_style_manager.get_font_prop().boldness; + bool is_font_bold = boldness.has_value() || WxFontUtils::is_bold(wx_font); + if (is_font_bold) { + // unset bold + if (clickable(get_icon(m_icons, IconType::bold, IconState::hovered), + get_icon(m_icons, IconType::unbold, IconState::hovered))) { + boldness.reset(); + if (wx_font.GetWeight() != wxFontWeight::wxFONTWEIGHT_NORMAL) { + wxFont new_wx_font = wx_font; // copy + new_wx_font.SetWeight(wxFontWeight::wxFONTWEIGHT_NORMAL); + if(!m_style_manager.set_wx_font(new_wx_font)) + return false; + } + return true; + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Unset bold"), m_gui_cfg->max_tooltip_width); + } else { + // set bold + if (draw_button(m_icons, IconType::bold)) { + wxFont new_wx_font = wx_font; // copy + auto new_ff = WxFontUtils::set_bold(new_wx_font, *ff.font_file); + if (new_ff != nullptr) { + if(!m_style_manager.set_wx_font(new_wx_font, std::move(new_ff))) + return false; + } else { + // bold font can't be loaded + // set up boldness + boldness = 20.f; + //font_file->cache.empty(); + } + return true; + } + if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Set bold"), m_gui_cfg->max_tooltip_width); + } + return false; +} + +template bool exist_change(const T &value, const T *default_value){ + if (default_value == nullptr) return false; + return (value != *default_value); +} + +template<> bool exist_change(const std::optional &value, const std::optional *default_value){ + if (default_value == nullptr) return false; + return !is_approx(value, *default_value); +} + +template<> bool exist_change(const float &value, const float *default_value){ + if (default_value == nullptr) return false; + return !is_approx(value, *default_value); +} + +template +bool GLGizmoEmboss::revertible(const std::string &name, + T &value, + const T *default_value, + const std::string &undo_tooltip, + float undo_offset, + Draw draw) const +{ + ImGui::AlignTextToFramePadding(); + bool changed = exist_change(value, default_value); + if (changed || default_value == nullptr) + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORCA, name); + else + ImGuiWrapper::text(name); + + // render revert changes button + if (changed) { + ImGuiWindow *window = ImGui::GetCurrentWindow(); + float prev_x = window->DC.CursorPosPrevLine.x; + ImGui::SameLine(undo_offset); // change cursor postion + if (draw_button(m_icons, IconType::undo)) { + value = *default_value; + return true; + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(undo_tooltip, m_gui_cfg->max_tooltip_width); + window->DC.CursorPosPrevLine.x = prev_x; // set back previous position + } + return draw(); +} +// May be move to ImGuiWrapper +template bool imgui_input(const char *label, T *v, T step, T step_fast, const char *format, ImGuiInputTextFlags flags); +template<> bool imgui_input(const char *label, float *v, float step, float step_fast, const char *format, ImGuiInputTextFlags flags) +{ return ImGui::InputFloat(label, v, step, step_fast, format, flags); } +template<> bool imgui_input(const char *label, double *v, double step, double step_fast, const char *format, ImGuiInputTextFlags flags) +{ return ImGui::InputDouble(label, v, step, step_fast, format, flags); } + +template +bool GLGizmoEmboss::rev_input(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, ImGuiInputTextFlags flags) const +{ + // draw offseted input + auto draw_offseted_input = [&offset = m_gui_cfg->input_offset, &width = m_gui_cfg->input_width, + &name, &value, &step, &step_fast, format, flags](){ + ImGui::SameLine(offset); + ImGui::SetNextItemWidth(width); + return imgui_input(("##" + name).c_str(), + &value, step, step_fast, format, flags); + }; + float undo_offset = ImGui::GetStyle().WindowPadding.x; + return revertible(name, value, default_value, undo_tooltip, undo_offset, draw_offseted_input); +} + +template +bool GLGizmoEmboss::rev_input_mm(const std::string &name, + T &value, + const T *default_value_ptr, + const std::string &undo_tooltip, + T step, + T step_fast, + const char *format, + bool use_inch, + const std::optional &scale) const +{ + // _variable which temporary keep value + T value_ = value; + T default_value_; + if (use_inch) { + // calc value in inch + value_ *= GizmoObjectManipulation::mm_to_in; + if (default_value_ptr) { + default_value_ = GizmoObjectManipulation::mm_to_in * (*default_value_ptr); + default_value_ptr = &default_value_; + } + } + if (scale.has_value()) + value_ *= *scale; + bool use_correction = use_inch || scale.has_value(); + if (rev_input(name, use_correction ? value_ : value, default_value_ptr, undo_tooltip, step, step_fast, format)) { + if (use_correction) { + value = value_; + if (use_inch) + value *= GizmoObjectManipulation::in_to_mm; + if (scale.has_value()) + value /= *scale; + } + return true; + } + return false; +} + +bool GLGizmoEmboss::rev_checkbox(const std::string &name, + bool &value, + const bool *default_value, + const std::string &undo_tooltip) const +{ + // draw offseted input + auto draw_offseted_input = [this, &offset = m_gui_cfg->advanced_input_offset, &name, &value](){ + ImGui::SameLine(offset); + return m_imgui->bbl_checkbox(("##" + name).c_str(), value); + }; + float undo_offset = ImGui::GetStyle().WindowPadding.x; + return revertible(name, value, default_value, undo_tooltip, + undo_offset, draw_offseted_input); +} + +bool GLGizmoEmboss::set_height() { + float &value = m_style_manager.get_font_prop().size_in_mm; + + // size can't be zero or negative + apply(value, limits.size_in_mm); + + if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { + assert(false); + return false; + } + + // only different value need process + if (is_approx(value, m_volume->text_configuration->style.prop.size_in_mm)) + return false; + + if (m_style_manager.get_font_prop().per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); + +#ifdef USE_PIXEL_SIZE_IN_WX_FONT + // store font size into path serialization + const wxFont &wx_font = m_style_manager.get_wx_font(); + if (wx_font.IsOk()) { + wxFont wx_font_new = wx_font; // copy + wx_font_new.SetPointSize(static_cast(value)); + m_style_manager.set_wx_font(wx_font_new); + } +#endif + return true; +} + +void GLGizmoEmboss::draw_height(bool use_inch) +{ + float &value = m_style_manager.get_font_prop().size_in_mm; + const EmbossStyle* stored_style = m_style_manager.get_stored_style(); + const float *stored = (stored_style != nullptr)? &stored_style->prop.size_in_mm : nullptr; + const char *size_format = use_inch ? "%.2f in" : "%.1f mm"; + const std::string revert_text_size = _u8L("Revert text size."); + const std::string& name = m_gui_cfg->translations.height; + if (rev_input_mm(name, value, stored, revert_text_size, 0.1f, 1.f, size_format, use_inch, m_scale_height)) + if (set_height()) + process(); +} + +void GLGizmoEmboss::draw_depth(bool use_inch) +{ + double &value = m_style_manager.get_style().projection.depth; + const StyleManager::Style * stored_style = m_style_manager.get_stored_style(); + const double *stored = ((stored_style!=nullptr)? &stored_style->projection.depth : nullptr); + const std::string revert_emboss_depth = _u8L("Revert embossed depth."); + const char *size_format = ((use_inch) ? "%.3f in" : "%.2f mm"); + const std::string name = m_gui_cfg->translations.depth; + if (rev_input_mm(name, value, stored, revert_emboss_depth, 0.1, 1., size_format, use_inch, m_scale_depth)){ + // size can't be zero or negative + apply(value, limits.emboss); + + // only different value need process + if(!is_approx(value, m_volume->emboss_shape->projection.depth)) + process(); + } +} + +bool GLGizmoEmboss::rev_slider(const std::string &name, + std::optional& value, + const std::optional *default_value, + const std::string &undo_tooltip, + int v_min, + int v_max, + const std::string& format, + const wxString &tooltip) const +{ + auto draw_slider_optional_int = [&]() -> bool { + float slider_offset = m_gui_cfg->advanced_input_offset; + float slider_width = m_gui_cfg->input_width; + ImGui::SameLine(slider_offset); + ImGui::SetNextItemWidth(slider_width); + return m_imgui->slider_optional_int( ("##" + name).c_str(), value, + v_min, v_max, format.c_str(), 1.f, false, tooltip); + }; + float undo_offset = ImGui::GetStyle().WindowPadding.x; + return revertible(name, value, default_value, + undo_tooltip, undo_offset, draw_slider_optional_int); +} + +bool GLGizmoEmboss::rev_slider(const std::string &name, + std::optional& value, + const std::optional *default_value, + const std::string &undo_tooltip, + float v_min, + float v_max, + const std::string& format, + const wxString &tooltip) const +{ + auto draw_slider_optional_float = [&]() -> bool { + float slider_offset = m_gui_cfg->advanced_input_offset; + float slider_width = m_gui_cfg->input_width; + ImGui::SameLine(slider_offset); + ImGui::SetNextItemWidth(slider_width); + return m_imgui->slider_optional_float(("##" + name).c_str(), value, + v_min, v_max, format.c_str(), 1.f, false, tooltip); + }; + float undo_offset = ImGui::GetStyle().WindowPadding.x; + return revertible(name, value, default_value, + undo_tooltip, undo_offset, draw_slider_optional_float); +} + +bool GLGizmoEmboss::rev_slider(const std::string &name, + float &value, + const float *default_value, + const std::string &undo_tooltip, + float v_min, + float v_max, + const std::string &format, + const wxString &tooltip) const +{ + auto draw_slider_float = [&]() -> bool { + float slider_offset = m_gui_cfg->advanced_input_offset; + float slider_width = m_gui_cfg->input_width; + ImGui::SameLine(slider_offset); + ImGui::SetNextItemWidth(slider_width); + return m_imgui->slider_float("##" + name, &value, v_min, v_max, + format.c_str(), 1.f, false, tooltip); + }; + float undo_offset = ImGui::GetStyle().WindowPadding.x; + return revertible(name, value, default_value, + undo_tooltip, undo_offset, draw_slider_float); +} + +void GLGizmoEmboss::draw_advanced() +{ + const auto &ff = m_style_manager.get_font_file_with_cache(); + if (!ff.has_value()) { + ImGui::Text("%s", _u8L("Advanced options cannot be changed for the selected font.\n" + "Select another font.").c_str()); + return; + } + + FontProp &font_prop = m_style_manager.get_font_prop(); + const FontFile::Info &font_info = get_font_info(*ff.font_file, font_prop); +#ifdef SHOW_FONT_FILE_PROPERTY + ImGui::SameLine(); + int cache_size = ff.has_value()? (int)ff.cache->size() : 0; + std::string ff_property = + "ascent=" + std::to_string(font_info.ascent) + + ", descent=" + std::to_string(font_info.descent) + + ", lineGap=" + std::to_string(font_info.linegap) + + ", unitPerEm=" + std::to_string(font_info.unit_per_em) + + ", cache(" + std::to_string(cache_size) + " glyphs)"; + if (font_file->infos.size() > 1) { + unsigned int collection = current_prop.collection_number.value_or(0); + ff_property += ", collect=" + std::to_string(collection+1) + "/" + std::to_string(font_file->infos.size()); + } + m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, ff_property); +#endif // SHOW_FONT_FILE_PROPERTY + + bool exist_change = false; + auto &tr = m_gui_cfg->translations; + + const StyleManager::Style *stored_style = nullptr; + if (m_style_manager.exist_stored_style()) + stored_style = m_style_manager.get_stored_style(); + + bool is_the_only_one_part = m_volume->is_the_only_one_part(); + bool can_use_surface = (m_volume->emboss_shape->projection.use_surface)? true : // already used surface must have option to uncheck + !is_the_only_one_part; + m_imgui->disabled_begin(!can_use_surface); + const bool *def_use_surface = stored_style ? + &stored_style->projection.use_surface : nullptr; + StyleManager::Style ¤t_style = m_style_manager.get_style(); + bool &use_surface = current_style.projection.use_surface; + if (rev_checkbox(tr.use_surface, use_surface, def_use_surface, + _u8L("Revert using of model surface."))) { + if (use_surface) + // when using surface distance is not used + current_style.distance.reset(); + process(); + } + m_imgui->disabled_end(); // !can_use_surface + + bool &per_glyph = font_prop.per_glyph; + bool can_use_per_glyph = (per_glyph) ? true : // already used surface must have option to uncheck + !is_the_only_one_part; + m_imgui->disabled_begin(!can_use_per_glyph); + const bool *def_per_glyph = stored_style ? &stored_style->prop.per_glyph : nullptr; + if (rev_checkbox(tr.per_glyph, per_glyph, def_per_glyph, + _u8L("Revert Transformation per glyph."))) { + if (per_glyph && !m_text_lines.is_init()) + reinit_text_lines(); + process(); + } else if (ImGui::IsItemHovered()) { + if (per_glyph) { + m_imgui->tooltip(_u8L("Set global orientation for whole text."), m_gui_cfg->max_tooltip_width); + } else { + m_imgui->tooltip(_u8L("Set position and orientation per glyph."), m_gui_cfg->max_tooltip_width); + if (!m_text_lines.is_init()) + reinit_text_lines(); + } + } else if (!per_glyph && m_text_lines.is_init()) + m_text_lines.reset(); + m_imgui->disabled_end(); // !can_use_per_glyph + + auto draw_align = [&align = font_prop.align, input_offset = m_gui_cfg->advanced_input_offset, &icons = m_icons, &m_imgui = m_imgui, &m_gui_cfg = m_gui_cfg]() { + bool is_change = false; + ImGui::SameLine(input_offset); + if (align.first==FontProp::HorizontalAlign::left) draw(get_icon(icons, IconType::align_horizontal_left, IconState::hovered)); + else if (draw_button(icons, IconType::align_horizontal_left)) { align.first=FontProp::HorizontalAlign::left; is_change = true; } + else if (ImGui::IsItemHovered()) m_imgui->tooltip(_CTX_utf8(L_CONTEXT("Left", "Alignment"), "Alignment"), m_gui_cfg->max_tooltip_width); + ImGui::SameLine(); + if (align.first==FontProp::HorizontalAlign::center) draw(get_icon(icons, IconType::align_horizontal_center, IconState::hovered)); + else if (draw_button(icons, IconType::align_horizontal_center)) { align.first=FontProp::HorizontalAlign::center; is_change = true; } + else if (ImGui::IsItemHovered()) m_imgui->tooltip(_CTX_utf8(L_CONTEXT("Center", "Alignment"), "Alignment"), m_gui_cfg->max_tooltip_width); + ImGui::SameLine(); + if (align.first==FontProp::HorizontalAlign::right) draw(get_icon(icons, IconType::align_horizontal_right, IconState::hovered)); + else if (draw_button(icons, IconType::align_horizontal_right)) { align.first=FontProp::HorizontalAlign::right; is_change = true; } + else if (ImGui::IsItemHovered()) m_imgui->tooltip(_CTX_utf8(L_CONTEXT("Right", "Alignment"), "Alignment"), m_gui_cfg->max_tooltip_width); + + ImGui::SameLine(); + if (align.second==FontProp::VerticalAlign::top) draw(get_icon(icons, IconType::align_vertical_top, IconState::hovered)); + else if (draw_button(icons, IconType::align_vertical_top)) { align.second=FontProp::VerticalAlign::top; is_change = true; } + else if (ImGui::IsItemHovered()) m_imgui->tooltip(_CTX_utf8(L_CONTEXT("Top", "Alignment"), "Alignment"), m_gui_cfg->max_tooltip_width); + ImGui::SameLine(); + if (align.second==FontProp::VerticalAlign::center) draw(get_icon(icons, IconType::align_vertical_center, IconState::hovered)); + else if (draw_button(icons, IconType::align_vertical_center)) { align.second=FontProp::VerticalAlign::center; is_change = true; } + else if (ImGui::IsItemHovered()) m_imgui->tooltip(_CTX_utf8(L_CONTEXT("Middle", "Alignment"), "Alignment"), m_gui_cfg->max_tooltip_width); + ImGui::SameLine(); + if (align.second==FontProp::VerticalAlign::bottom) draw(get_icon(icons, IconType::align_vertical_bottom, IconState::hovered)); + else if (draw_button(icons, IconType::align_vertical_bottom)) { align.second=FontProp::VerticalAlign::bottom; is_change = true; } + else if (ImGui::IsItemHovered()) m_imgui->tooltip(_CTX_utf8(L_CONTEXT("Bottom", "Alignment"), "Alignment"), m_gui_cfg->max_tooltip_width); + return is_change; + }; + const FontProp::Align * def_align = stored_style ? &stored_style->prop.align : nullptr; + float undo_offset = ImGui::GetStyle().WindowPadding.x; + if (revertible(tr.alignment, font_prop.align, def_align, _u8L("Revert alignment."), undo_offset, draw_align)) { + if (font_prop.per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); + // TODO: move with text in finalize to not change position + process(); + } + + // TRN EmbossGizmo: font units + std::string units = _u8L("points"); + std::string units_fmt = "%.0f " + units; + + // input gap between characters + auto def_char_gap = stored_style ? + &stored_style->prop.char_gap : nullptr; + + int half_ascent = font_info.ascent / 2; + int min_char_gap = -half_ascent; + int max_char_gap = half_ascent; + FontProp ¤t_prop = current_style.prop; + if (rev_slider(tr.char_gap, current_prop.char_gap, def_char_gap, _u8L("Revert gap between characters"), + min_char_gap, max_char_gap, units_fmt, _L("Distance between characters"))){ + // Condition prevent recalculation when insertint out of limits value by imgui input + const std::optional &volume_char_gap = m_volume->text_configuration->style.prop.char_gap; + if (!apply(current_prop.char_gap, limits.char_gap) || + !volume_char_gap.has_value() || volume_char_gap != current_prop.char_gap) { + // char gap is stored inside of imgui font atlas + m_style_manager.clear_imgui_font(); + exist_change = true; + } + } + + // input gap between lines + bool is_multiline = m_text_lines.get_lines().size() > 1; + m_imgui->disabled_begin(!is_multiline); + auto def_line_gap = stored_style ? + &stored_style->prop.line_gap : nullptr; + int min_line_gap = -half_ascent; + int max_line_gap = half_ascent; + if (rev_slider(tr.line_gap, current_prop.line_gap, def_line_gap, _u8L("Revert gap between lines"), + min_line_gap, max_line_gap, units_fmt, _L("Distance between lines"))){ + // Condition prevent recalculation when insertint out of limits value by imgui input + const std::optional &volume_line_gap = m_volume->text_configuration->style.prop.line_gap; + if (!apply(current_prop.line_gap, limits.line_gap) || + !volume_line_gap.has_value() || volume_line_gap != current_prop.line_gap) { + // line gap is planed to be stored inside of imgui font atlas + m_style_manager.clear_imgui_font(); + if (font_prop.per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); + exist_change = true; + } + } + m_imgui->disabled_end(); // !is_multiline + + // input boldness + auto def_boldness = stored_style ? + &stored_style->prop.boldness : nullptr; + if (rev_slider(tr.boldness, current_prop.boldness, def_boldness, _u8L("Undo boldness"), + limits.boldness.gui.min, limits.boldness.gui.max, units_fmt, _L("Tiny / Wide glyphs"))){ + const std::optional &volume_boldness = m_volume->text_configuration->style.prop.boldness; + if (!apply(current_prop.boldness, limits.boldness.values) || + !volume_boldness.has_value() || volume_boldness != current_prop.boldness) + exist_change = true; + } + + // input italic + auto def_skew = stored_style ? + &stored_style->prop.skew : nullptr; + if (rev_slider(tr.skew_ration, current_prop.skew, def_skew, _u8L("Undo letter's skew"), + limits.skew.gui.min, limits.skew.gui.max, "%.2f", _L("Italic strength ratio"))){ + const std::optional &volume_skew = m_volume->text_configuration->style.prop.skew; + if (!apply(current_prop.skew, limits.skew.values) || + !volume_skew.has_value() ||volume_skew != current_prop.skew) + exist_change = true; + } + + // input surface distance + bool allowe_surface_distance = !use_surface && !m_volume->is_the_only_one_part(); + std::optional &distance = current_style.distance; + float prev_distance = distance.value_or(.0f); + float min_distance = static_cast(-2 * current_style.projection.depth); + float max_distance = static_cast(2 * current_style.projection.depth); + auto def_distance = stored_style ? + &stored_style->distance : nullptr; + m_imgui->disabled_begin(!allowe_surface_distance); + bool use_inch = wxGetApp().app_config->get_bool("use_inches"); + const std::string undo_move_tooltip = _u8L("Undo translation"); + const wxString move_tooltip = _L("Distance of the center of the text to the model surface."); + bool is_moved = false; + if (use_inch) { + std::optional distance_inch; + if (distance.has_value()) distance_inch = (*distance * GizmoObjectManipulation::mm_to_in); + std::optional def_distance_inch; + if (def_distance != nullptr) { + if (def_distance->has_value()) def_distance_inch = GizmoObjectManipulation::mm_to_in * (*(*def_distance)); + def_distance = &def_distance_inch; + } + min_distance *= GizmoObjectManipulation::mm_to_in; + max_distance *= GizmoObjectManipulation::mm_to_in; + if (rev_slider(tr.from_surface, distance_inch, def_distance, undo_move_tooltip, min_distance, max_distance, "%.3f in", move_tooltip)) { + if (distance_inch.has_value()) { + distance = *distance_inch * GizmoObjectManipulation::in_to_mm; + } else { + distance.reset(); + } + is_moved = true; + } + } else { + if (rev_slider(tr.from_surface, distance, def_distance, undo_move_tooltip, + min_distance, max_distance, "%.2f mm", move_tooltip)) is_moved = true; + } + + if (is_moved){ + if (font_prop.per_glyph){ + process(); + } else { + do_local_z_move(m_parent, distance.value_or(.0f) - prev_distance); + } + } + m_imgui->disabled_end(); // allowe_surface_distance + + // 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 = current_style.angle.value_or(0.f); + float angle_deg = static_cast(-angle * 180 / M_PI); + float def_angle_deg_val = + (!stored_style || !stored_style->angle.has_value()) ? + 0.f : (*stored_style->angle * -180 / M_PI); + float* def_angle_deg = stored_style ? + &def_angle_deg_val : nullptr; + if (rev_slider(tr.rotation, angle_deg, def_angle_deg, _u8L("Undo rotation"), + limits.angle.min, limits.angle.max, u8"%.2f °", + _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, diff_angle); + + // calc angle after rotation + const Selection &selection = m_parent.get_selection(); + const GLVolume *gl_volume = get_selected_gl_volume(selection); + assert(gl_volume != nullptr); + assert(m_style_manager.is_active_font()); + if (m_style_manager.is_active_font() && gl_volume != nullptr) + m_style_manager.get_style().angle = calc_angle(selection); + + if (font_prop.per_glyph) + reinit_text_lines(m_text_lines.get_lines().size()); + + // recalculate for surface cut + if (use_surface || font_prop.per_glyph) + process(); + } + + // 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_bold : IconType::unlock_bold, IconState::activable); + const IconManager::Icon &icon_disable = get_icon(m_icons, m_keep_up ? IconType::lock : IconType::unlock, IconState::disabled); + if (button(icon, icon_hover, icon_disable)) + m_keep_up = !m_keep_up; + + if (ImGui::IsItemHovered()) + m_imgui->tooltip(m_keep_up? + _u8L("Unlock the text's rotation when moving text along the object's surface."): + _u8L("Lock the text's rotation when moving text along the object's surface.") + , m_gui_cfg->max_tooltip_width); + } + + // when more collection add selector + if (ff.font_file->infos.size() > 1) { + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", tr.collection.c_str()); + ImGui::SameLine(m_gui_cfg->advanced_input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + unsigned int selected = current_prop.collection_number.value_or(0); + std::string tooltip = ""; + ImGuiWrapper::push_combo_style(m_parent.get_scale()); + if (ImGui::BBLBeginCombo("## Font collection", std::to_string(selected).c_str())) { + for (unsigned int i = 0; i < ff.font_file->infos.size(); ++i) { + ImGui::PushID(1 << (10 + i)); + bool is_selected = (i == selected); + if (ImGui::BBLSelectable(std::to_string(i).c_str(), is_selected)) { + if (i == 0) current_prop.collection_number.reset(); + else current_prop.collection_number = i; + exist_change = true; + } + ImGui::PopID(); + } + ImGui::EndCombo(); + } else if (ImGui::IsItemHovered()) { + tooltip = _u8L("Select from True Type Collection."); + } + ImGuiWrapper::pop_combo_style(); + if (!tooltip.empty()) + m_imgui->tooltip(tooltip, m_gui_cfg->max_tooltip_width); + } + + if (exist_change) { + m_style_manager.clear_glyphs_cache(); + if (m_style_manager.get_font_prop().per_glyph) + reinit_text_lines(); + else + m_text_lines.reset(); + process(); + } + + if (ImGui::Button(_u8L("Set text to face camera").c_str())) { + assert(get_selected_volume(m_parent.get_selection()) == m_volume); + 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(); + } else if (ImGui::IsItemHovered()) { + m_imgui->tooltip(_u8L("Orient the text towards the camera."), m_gui_cfg->max_tooltip_width); + } + + //ImGui::SameLine(); if (ImGui::Button("Re-emboss")) GLGizmoEmboss::re_emboss(*m_volume); + +#ifdef ALLOW_DEBUG_MODE + ImGui::Text("family = %s", (current_prop.family.has_value() ? + current_prop.family->c_str() : + " --- ")); + ImGui::Text("face name = %s", (current_prop.face_name.has_value() ? + current_prop.face_name->c_str() : + " --- ")); + ImGui::Text("style = %s", + (current_prop.style.has_value() ? current_prop.style->c_str() : + " --- ")); + ImGui::Text("weight = %s", (current_prop.weight.has_value() ? + current_prop.weight->c_str() : + " --- ")); + + std::string descriptor = style.path; + ImGui::Text("descriptor = %s", descriptor.c_str()); +#endif // ALLOW_DEBUG_MODE +} + +#ifdef ALLOW_ADD_FONT_BY_OS_SELECTOR +bool GLGizmoEmboss::choose_font_by_wxdialog() +{ + wxFontData data; + data.EnableEffects(false); + data.RestrictSelection(wxFONTRESTRICT_SCALABLE); + // set previous selected font + EmbossStyle &selected_style = m_style_manager.get_style(); + if (selected_style.type == WxFontUtils::get_current_type()) { + std::optional selected_font = WxFontUtils::load_wxFont( + selected_style.path); + if (selected_font.has_value()) data.SetInitialFont(*selected_font); + } + + wxFontDialog font_dialog(wxGetApp().mainframe, data); + if (font_dialog.ShowModal() != wxID_OK) return false; + + data = font_dialog.GetFontData(); + wxFont wx_font = data.GetChosenFont(); + size_t font_index = m_style_manager.get_fonts().size(); + EmbossStyle emboss_style = WxFontUtils::create_emboss_style(wx_font); + + // Check that deserialization NOT influence font + // false - use direct selected wxFont in dialog + // true - use font item (serialize and deserialize wxFont) + bool use_deserialized_font = false; + + // Try load and use new added font + if ((use_deserialized_font && !m_style_manager.load_style(font_index)) || + (!use_deserialized_font && !m_style_manager.load_style(emboss_style, wx_font))) { + m_style_manager.erase(font_index); + wxString message = GUI::format_wxstr( + "Font \"%1%\" can't be used. Please select another.", + emboss_style.name); + wxString title = "Selected font is NOT True-type."; + MessageDialog not_loaded_font_message(nullptr, message, title, wxOK); + not_loaded_font_message.ShowModal(); + return choose_font_by_wxdialog(); + } + + // fix dynamic creation of italic font + const auto& cn = m_style_manager.get_font_prop().collection_number; + unsigned int font_collection = cn.has_value() ? *cn : 0; + const auto&ff = m_style_manager.get_font_file_with_cache(); + if (WxFontUtils::is_italic(wx_font) && + !Emboss::is_italic(*ff.font_file, font_collection)) { + m_style_manager.get_font_prop().skew = 0.2; + } + return true; +} +#endif // ALLOW_ADD_FONT_BY_OS_SELECTOR + +#if defined ALLOW_ADD_FONT_BY_FILE or defined ALLOW_DEBUG_MODE +namespace priv { +static 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); +} +} // namespace priv +#endif // ALLOW_ADD_FONT_BY_FILE || ALLOW_DEBUG_MODE + +#ifdef ALLOW_ADD_FONT_BY_FILE +bool GLGizmoEmboss::choose_true_type_file() +{ + wxArrayString input_files; + wxString fontDir = wxEmptyString; + wxString selectedFile = wxEmptyString; + wxFileDialog dialog(nullptr, "Choose one or more files (TTF, TTC):", + fontDir, selectedFile, file_wildcards(FT_FONTS), + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() == wxID_OK) dialog.GetPaths(input_files); + if (input_files.IsEmpty()) return false; + size_t index = m_style_manager.get_fonts().size(); + // use first valid font + for (auto &input_file : input_files) { + std::string path = std::string(input_file.c_str()); + std::string name = get_file_name(path); + //make_unique_name(name, m_font_list); + const FontProp& prop = m_style_manager.get_font_prop(); + EmbossStyle style{ name, path, EmbossStyle::Type::file_path, prop }; + m_style_manager.add_font(style); + // set first valid added font as active + if (m_style_manager.load_style(index)) return true; + m_style_manager.erase(index); + } + return false; +} +#endif // ALLOW_ADD_FONT_BY_FILE + +void GLGizmoEmboss::create_notification_not_valid_font( + const TextConfiguration &tc) +{ + const EmbossStyle &es = m_style_manager.get_style(); + const auto &face_name_opt = es.prop.face_name; + const std::string &face_name_3mf = tc.style.prop.face_name.value_or(tc.style.path); + + std::optional face_name_by_wx; + if (!face_name_opt.has_value()) { + const wxFont& wx_font = m_style_manager.get_wx_font(); + if (wx_font.IsOk()) { + wxString wx_face_name = wx_font.GetFaceName(); + if (!wx_face_name.empty()) + face_name_by_wx = std::string(wx_face_name.ToUTF8().data()); + } + } + const std::string &face_name = face_name_opt.value_or(face_name_by_wx.value_or(es.path)); + std::string text = + GUI::format(_L("Can't load exactly same font(\"%1%\"). " + "Aplication selected a similar one(\"%2%\"). " + "You have to specify font for enable edit text."), + face_name_3mf, face_name); + create_notification_not_valid_font(text); +} + +void GLGizmoEmboss::create_notification_not_valid_font(const std::string &text) { + // not neccessary, but for sure that old notification doesnt exist + if (m_is_unknown_font) + remove_notification_not_valid_font(); + m_is_unknown_font = true; + + auto type = NotificationType::UnknownFont; + auto level = NotificationManager::NotificationLevel::WarningNotificationLevel; + auto notification_manager = wxGetApp().plater()->get_notification_manager(); + notification_manager->push_notification(type, level, text); +} + +void GLGizmoEmboss::remove_notification_not_valid_font() +{ + if (!m_is_unknown_font) return; + m_is_unknown_font = false; + auto type = NotificationType::UnknownFont; + auto notification_manager = wxGetApp().plater()->get_notification_manager(); + notification_manager->close_notification_of_type(type); +} + +void GLGizmoEmboss::init_icons() +{ + // icon order has to match the enum IconType + std::vector filenames{ + "edit_button.svg", + "delete.svg", + "add_copies.svg", + "save.svg", + "undo.svg", + "make_italic.svg", + "make_unitalic.svg", + "make_bold.svg", + "make_unbold.svg", + "search.svg", + "open.svg", + "exclamation.svg", + "lock_closed.svg", // lock, + "lock_closed_f.svg",// lock_bold, + "lock_open.svg", // unlock, + "lock_open_f.svg", // unlock_bold, + "align_horizontal_left.svg", + "align_horizontal_center.svg", + "align_horizontal_right.svg", + "align_vertical_top.svg", + "align_vertical_center.svg", + "align_vertical_bottom.svg" + }; + assert(filenames.size() == static_cast(IconType::_count)); + std::string path = resources_dir() + "/images/"; + for (std::string &filename : filenames) filename = path + filename; + + ImVec2 size(m_gui_cfg->icon_width, m_gui_cfg->icon_width); + auto type = IconManager::RasterType::color_wite_gray; + m_icons = m_icon_manager.init(filenames, size, type); +} + +static std::size_t hash_value(wxString const &s){ + boost::hash hasher; + return hasher(s.ToStdString()); +} + +// increase number when change struct FacenamesSerializer +constexpr std::uint32_t FACENAMES_VERSION = 1; +struct FacenamesSerializer +{ + // hash number for unsorted vector of installed font into system + size_t hash = 0; + // assumption that is loadable + std::vector good; + // Can't load for some reason + std::vector bad; +}; + +template void save(Archive &archive, wxString const &d) +{ std::string s(d.ToUTF8().data()); archive(s);} +template void load(Archive &archive, wxString &d) +{ std::string s; archive(s); d = s;} +template void serialize(Archive &ar, FacenamesSerializer &t, const std::uint32_t version) +{ + // When performing a load, the version associated with the class + // is whatever it was when that data was originally serialized + // When we save, we'll use the version that is defined in the macro + if (version != FACENAMES_VERSION) return; + ar(t.hash, t.good, t.bad); +} +CEREAL_CLASS_VERSION(::FacenamesSerializer, FACENAMES_VERSION); // register class version + +///////////// +// private namespace implementation +/////////////// +namespace { + +const IconManager::Icon &get_icon(const IconManager::VIcons& icons, IconType type, IconState state) { + return *icons[(unsigned) type][(unsigned) state]; } + +bool draw_button(const IconManager::VIcons &icons, IconType type, bool disable){ + return Slic3r::GUI::button( + get_icon(icons, type, IconState::activable), + get_icon(icons, type, IconState::hovered), + get_icon(icons, type, IconState::disabled), + disable);} + +TextDataBase::TextDataBase(DataBase &&parent, + const FontFileWithCache &font_file, + TextConfiguration &&text_configuration, + const EmbossProjection &projection) + : DataBase(std::move(parent)), m_font_file(font_file) /* copy */, m_text_configuration(std::move(text_configuration)) +{ + assert(m_font_file.has_value()); + shape.projection = projection; // copy + + const FontProp &fp = m_text_configuration.style.prop; + const FontFile &ff = *m_font_file.font_file; + shape.scale = get_text_shape_scale(fp, ff); +} + +EmbossShape &TextDataBase::create_shape() +{ + if (!shape.shapes_with_ids.empty()) + return shape; + + // create shape by configuration + const char *text = m_text_configuration.text.c_str(); + std::wstring text_w = boost::nowide::widen(text); + const FontProp &fp = m_text_configuration.style.prop; + auto was_canceled = [&c = cancel](){ return c->load(); }; + + shape.shapes_with_ids = text2vshapes(m_font_file, text_w, fp, was_canceled); + return shape; +} + +void TextDataBase::write(ModelVolume &volume) const +{ + DataBase::write(volume); + volume.text_configuration = m_text_configuration; // copy + assert(volume.emboss_shape.has_value()); +} + +std::unique_ptr create_emboss_data_base(const std::string &text, + StyleManager &style_manager, + TextLinesModel &text_lines, + const Selection &selection, + ModelVolumeType type, + std::shared_ptr> &cancel) +{ + // create volume_name + std::string volume_name = text; // copy + // contain_enter? + if (volume_name.find('\n') != std::string::npos) + // change enters to space + std::replace(volume_name.begin(), volume_name.end(), '\n', ' '); + + if (!style_manager.is_active_font()) { + style_manager.load_valid_style(); + assert(style_manager.is_active_font()); + if (!style_manager.is_active_font()) + return {}; // no active font in style, should never happend !!! + } + + const StyleManager::Style &style = style_manager.get_style(); + // actualize font path - during changes in gui it could be corrupted + // volume must store valid path + assert(style_manager.get_wx_font().IsOk()); + assert(style.path.compare(WxFontUtils::store_wxFont(style_manager.get_wx_font())) == 0); + + if (style.prop.per_glyph) { + if (!text_lines.is_init()) + init_text_lines(text_lines, selection, style_manager); + } else + text_lines.reset(); + + bool is_outside = (type == ModelVolumeType::MODEL_PART); + + // 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); + + DataBase base(volume_name, cancel); + base.is_outside = is_outside; + base.text_lines = text_lines.get_lines(); + base.from_surface = style.distance; + + FontFileWithCache &font = style_manager.get_font_file_with_cache(); + TextConfiguration tc{static_cast(style), text}; + return std::make_unique(std::move(base), font, std::move(tc), style.projection); +} + +CreateVolumeParams create_input(GLCanvas3D &canvas, const StyleManager::Style &style, RaycastManager& raycaster, ModelVolumeType volume_type) +{ + auto gizmo = static_cast(GLGizmosManager::Emboss); + const GLVolume *gl_volume = get_first_hovered_gl_volume(canvas); + Plater *plater = wxGetApp().plater(); + return CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), + plater->get_ui_job_worker(), volume_type, raycaster, gizmo, gl_volume, style.distance, style.angle}; +} + +ImVec2 calc_fine_position(const Selection &selection, const ImVec2 &windows_size, const Size &canvas_size) +{ + const Selection::IndicesList indices = selection.get_volume_idxs(); + // no selected volume + if (indices.empty()) + return {}; + const GLVolume *volume = selection.get_volume(*indices.begin()); + // bad volume selected (e.g. deleted one) + if (volume == nullptr) + return {}; + + const Camera &camera = wxGetApp().plater()->get_camera(); + Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *volume); + + ImVec2 c_size(canvas_size.get_width(), canvas_size.get_height()); + ImVec2 offset = ImGuiWrapper::suggest_location(windows_size, hull, c_size); + return offset; +} + +std::string concat(std::vector data) { + std::stringstream ss; + for (const auto &d : data) + ss << d.c_str() << ", "; + return ss.str(); +} + +boost::filesystem::path get_fontlist_cache_path(){ + return boost::filesystem::path(data_dir()) / "cache" / "fonts.cereal"; +} + +bool store(const Facenames &facenames) { + std::string cache_path = get_fontlist_cache_path().string(); + boost::nowide::ofstream file(cache_path, std::ios::binary); + ::cereal::BinaryOutputArchive archive(file); + std::vector good; + good.reserve(facenames.faces.size()); + for (const FaceName &face : facenames.faces) good.push_back(face.wx_name); + FacenamesSerializer data = {facenames.hash, good, facenames.bad}; + + assert(std::is_sorted(data.bad.begin(), data.bad.end())); + assert(std::is_sorted(data.good.begin(), data.good.end())); + + try { + archive(data); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to write fontlist cache - " << cache_path << ex.what(); + return false; + } + return true; +} + +bool load(Facenames &facenames) { + boost::filesystem::path path = get_fontlist_cache_path(); + std::string path_str = path.string(); + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "Fontlist cache - '" << path_str << "' does not exists."; + return false; + } + boost::nowide::ifstream file(path_str, std::ios::binary); + cereal::BinaryInputArchive archive(file); + + FacenamesSerializer data; + try { + archive(data); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to load fontlist cache - '" << path_str << "'. Exception: " << ex.what(); + return false; + } + + assert(std::is_sorted(data.bad.begin(), data.bad.end())); + assert(std::is_sorted(data.good.begin(), data.good.end())); + + facenames.hash = data.hash; + facenames.faces.reserve(data.good.size()); + for (const wxString &face : data.good) + facenames.faces.push_back({face}); + facenames.bad = data.bad; + return true; +} + +void init_truncated_names(Facenames &face_names, float max_width) +{ + for (FaceName &face : face_names.faces) { + std::string name_str(face.wx_name.ToUTF8().data()); + face.name_truncated = ImGuiWrapper::trunc(name_str, max_width); + } + face_names.has_truncated_names = true; +} + +void init_face_names(Facenames &face_names) +{ + Timer t("enumerate_fonts"); + if (face_names.is_init) return; + face_names.is_init = true; + + // to reload fonts from system, when install new one + wxFontEnumerator::InvalidateCache(); + + // try load cache + // Only not OS enumerated face has hash value 0 + if (face_names.hash == 0) { + load(face_names); + face_names.has_truncated_names = false; + } + + using namespace std::chrono; + steady_clock::time_point enumerate_start = steady_clock::now(); + ScopeGuard sg([&enumerate_start, &face_names = face_names]() { + steady_clock::time_point enumerate_end = steady_clock::now(); + long long enumerate_duration = duration_cast(enumerate_end - enumerate_start).count(); + BOOST_LOG_TRIVIAL(info) << "OS enumerate " << face_names.faces.size() << " fonts " + << "(+ " << face_names.bad.size() << " can't load " + << "= " << face_names.faces.size() + face_names.bad.size() << " fonts) " + << "in " << enumerate_duration << " ms\n" << concat(face_names.bad); + }); + wxArrayString facenames = wxFontEnumerator::GetFacenames(face_names.encoding); + size_t hash = boost::hash_range(facenames.begin(), facenames.end()); + // Zero value is used as uninitialized hash + if (hash == 0) hash = 1; + // check if it is same as last time + if (face_names.hash == hash) { + // no new installed font + BOOST_LOG_TRIVIAL(info) << "Same FontNames hash, cache is used. " + << "For clear cache delete file: " << get_fontlist_cache_path().string(); + return; + } + + BOOST_LOG_TRIVIAL(info) << ((face_names.hash == 0) ? + "FontName list is generate from scratch." : + "Hash are different. Only previous bad fonts are used and set again as bad"); + face_names.hash = hash; + + // validation lambda + auto is_valid_font = [encoding = face_names.encoding, bad = face_names.bad /*copy*/](const wxString &name) { + if (name.empty()) return false; + + // vertical font start with @, we will filter it out + // Not sure if it is only in Windows so filtering is on all platforms + if (name[0] == '@') return false; + + // previously detected bad font + auto it = std::lower_bound(bad.begin(), bad.end(), name); + if (it != bad.end() && *it == name) return false; + + wxFont wx_font(wxFontInfo().FaceName(name).Encoding(encoding)); + //* + // Faster chech if wx_font is loadable but not 100% + // names could contain not loadable font + if (!WxFontUtils::can_load(wx_font)) return false; + + /*/ + // Slow copy of font files to try load font + // After this all files are loadable + auto font_file = WxFontUtils::create_font_file(wx_font); + if (font_file == nullptr) + return false; // can't create font file + // */ + return true; + }; + + face_names.faces.clear(); + face_names.faces_names.clear(); + face_names.bad.clear(); + face_names.faces.reserve(facenames.size()); + face_names.faces_names.reserve(facenames.size()); + std::sort(facenames.begin(), facenames.end()); + for (const wxString &name : facenames) { + if (is_valid_font(name)) { + face_names.faces.push_back({name}); + face_names.faces_names.push_back(name.utf8_string()); + }else{ + face_names.bad.push_back(name); + } + } + assert(std::is_sorted(face_names.bad.begin(), face_names.bad.end())); + face_names.has_truncated_names = false; + store(face_names); +} + +void draw_font_preview(FaceName &face, const std::string& text, Facenames &faces, const GuiCfg &cfg, bool is_visible){ + // Limit for opened font files at one moment + unsigned int &count_opened_fonts = faces.count_opened_font_files; + // Size of texture + ImVec2 size(cfg.face_name_size.x(), cfg.face_name_size.y()); + float count_cached_textures_f = static_cast(faces.count_cached_textures); + std::string state_text; + // uv0 and uv1 set to pixel 0,0 in texture + ImVec2 uv0(0.f, 0.f), uv1(1.f / size.x, 1.f / size.y / count_cached_textures_f); + if (face.is_created != nullptr) { + // not created preview + if (*face.is_created) { + // Already created preview + size_t texture_index = face.texture_index; + uv0 = ImVec2(0.f, texture_index / count_cached_textures_f); + uv1 = ImVec2(1.f, (texture_index + 1) / count_cached_textures_f); + } else { + // Not finished preview + if (is_visible) { + // when not canceled still loading + state_text = (face.cancel->load()) ? + " " + _u8L("No symbol"): + " ... " + _u8L("Loading"); + } else { + // not finished and not visible cancel job + face.is_created = nullptr; + face.cancel->store(true); + } + } + } else if (is_visible && count_opened_fonts < cfg.max_count_opened_font_files) { + ++count_opened_fonts; + face.cancel = std::make_shared(false); + face.is_created = std::make_shared(false); + + const unsigned char gray_level = 5; + // format type and level must match to texture data + const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; + const GLint level = 0; + // select next texture index + size_t texture_index = (faces.texture_index + 1) % faces.count_cached_textures; + + // set previous cach as deleted + for (FaceName &f : faces.faces) + if (f.texture_index == texture_index) { + if (f.cancel != nullptr) f.cancel->store(true); + f.is_created = nullptr; + } + + faces.texture_index = texture_index; + face.texture_index = texture_index; + + // render text to texture + FontImageData data{ + text, + face.wx_name, + faces.encoding, + faces.texture_id, + faces.texture_index, + cfg.face_name_size, + gray_level, + format, + type, + level, + &count_opened_fonts, + face.cancel, // copy + face.is_created // copy + }; + auto job = std::make_unique(std::move(data)); + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::move(job)); + } else { + // cant start new thread at this moment so wait in queue + state_text = " ... " + _u8L("In queue"); + } + + if (!state_text.empty()) { + ImGui::SameLine(cfg.face_name_texture_offset_x); + ImGui::Text("%s", state_text.c_str()); + } + + ImGui::SameLine(cfg.face_name_texture_offset_x); + ImTextureID tex_id = (void *) (intptr_t) faces.texture_id; + ImVec4 tint_color = ImGui::GetStyleColorVec4(ImGuiCol_Text); + ImGui::Image(tex_id, size, uv0, uv1, tint_color); +} + +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; + const ImGuiStyle &style = ImGui::GetStyle(); + + cfg.max_style_name_width = ImGui::CalcTextSize("Maximal font name, extended").x; + + cfg.icon_width = static_cast(std::ceil(line_height)); + // make size pair number + if (cfg.icon_width % 2 != 0) ++cfg.icon_width; + + cfg.delete_pos_x = cfg.max_style_name_width + space; + const float count_line_of_text = 3.f; + cfg.text_size = ImVec2(-FLT_MIN, line_height_with_spacing * count_line_of_text); + 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; + GuiCfg::Translations &tr = cfg.translations; + + // TRN - Input label. Be short as possible + // Select look of letter shape + tr.font = _u8L("Font"); + // TRN - Input label. Be short as possible + // Height of one text line - Font Ascent + tr.height = _u8L("Height"); + // TRN - Input label. Be short as possible + // Size in emboss direction + tr.depth = _u8L("Depth"); + + float max_text_width = std::max({ + ImGui::CalcTextSize(tr.font.c_str()).x, + ImGui::CalcTextSize(tr.height.c_str()).x, + ImGui::CalcTextSize(tr.depth.c_str()).x}); + cfg.indent = static_cast(cfg.icon_width); + cfg.input_offset = style.WindowPadding.x + cfg.indent + max_text_width + space; + + // TRN - Input label. Be short as possible + // Copy surface of model on surface of the embossed text + tr.use_surface = _u8L("Use surface"); + // TRN - Input label. Be short as possible + // Option to change projection on curved surface + // for each character(glyph) in text separately + tr.per_glyph = _u8L("Per glyph"); + // TRN - Input label. Be short as possible + // Align Top|Middle|Bottom and Left|Center|Right + tr.alignment = _u8L("Alignment"); + // TRN - Input label. Be short as possible + tr.char_gap = _u8L("Char gap"); + // TRN - Input label. Be short as possible + tr.line_gap = _u8L("Line gap"); + // TRN - Input label. Be short as possible + tr.boldness = _u8L("Boldness"); + + // TRN - Input label. Be short as possible + // Like Font italic + tr.skew_ration = _u8L("Skew ratio"); + + // TRN - Input label. Be short as possible + // Distance from model surface to be able + // move text as part fully into not flat surface + // move text as modifier fully out of not flat surface + tr.from_surface = _u8L("From surface"); + + // TRN - Input label. Be short as possible + // Angle between Y axis and text line direction. + tr.rotation = _u8L("Rotation"); + + // TRN - Input label. Be short as possible + // Keep vector from bottom to top of text aligned with printer Y axis + tr.keep_up = _u8L("Keep up"); + + // TRN - Input label. Be short as possible. + // Some Font file contain multiple fonts inside and + // this is numerical selector of font inside font collections + tr.collection = _u8L("Collection"); + + float max_advanced_text_width = std::max({ + ImGui::CalcTextSize(tr.use_surface.c_str()).x, + ImGui::CalcTextSize(tr.per_glyph.c_str()).x, + ImGui::CalcTextSize(tr.alignment.c_str()).x, + ImGui::CalcTextSize(tr.char_gap.c_str()).x, + ImGui::CalcTextSize(tr.line_gap.c_str()).x, + ImGui::CalcTextSize(tr.boldness.c_str()).x, + ImGui::CalcTextSize(tr.skew_ration.c_str()).x, + ImGui::CalcTextSize(tr.from_surface.c_str()).x, + ImGui::CalcTextSize(tr.rotation.c_str()).x + cfg.icon_width + 2*space, + ImGui::CalcTextSize(tr.keep_up.c_str()).x, + ImGui::CalcTextSize(tr.collection.c_str()).x }); + cfg.advanced_input_offset = max_advanced_text_width + + 3 * space + cfg.indent; + + cfg.lock_offset = cfg.advanced_input_offset - (cfg.icon_width + space); + // calculate window size + float input_height = line_height_with_spacing + 2*style.FramePadding.y; + float separator_height = 2 + style.FramePadding.y; + + // "Text is to object" + radio buttons + cfg.height_of_volume_type_selector = separator_height + line_height_with_spacing + input_height; + + int max_style_image_width = static_cast(std::round(cfg.max_style_name_width/2 - 2 * style.FramePadding.x)); + int max_style_image_height = static_cast(std::round(input_height)); + cfg.max_style_image_size = Vec2i(max_style_image_width, line_height); + cfg.face_name_size = Vec2i(cfg.input_width, line_height_with_spacing); + cfg.face_name_texture_offset_x = cfg.face_name_size.x() + space; + + cfg.max_tooltip_width = ImGui::GetFontSize() * 20.0f; + + return cfg; +} +} // namespace diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp new file mode 100644 index 000000000..6217c3f12 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp @@ -0,0 +1,240 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_GLGizmoEmboss_hpp_ +#define slic3r_GLGizmoEmboss_hpp_ + +#include "GLGizmoBase.hpp" +#include "GLGizmoRotate.hpp" +#include "slic3r/GUI/IconManager.hpp" +#include "slic3r/GUI/SurfaceDrag.hpp" +#include "slic3r/GUI/I18N.hpp" // TODO: not needed +#include "slic3r/GUI/TextLines.hpp" +#include "slic3r/Utils/RaycastManager.hpp" +#include "slic3r/Utils/EmbossStyleManager.hpp" + +#include +#include +#include + +#include "libslic3r/Emboss.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/TextConfiguration.hpp" + +#include +#include + +class wxFont; +namespace Slic3r{ + class AppConfig; + class GLVolume; + enum class ModelVolumeType : int; +} + +namespace Slic3r::GUI { +class GLGizmoEmboss : public GLGizmoBase +{ +public: + explicit GLGizmoEmboss(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id); + + /// + /// Create new embossed text volume by type on position of mouse + /// + /// Object part / Negative volume / Modifier + /// Define position of new volume + bool create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); + + /// + /// Create new text without given position + /// + /// Object part / Negative volume / Modifier + bool create_volume(ModelVolumeType volume_type); + + /// + /// Handle pressing of shortcut + /// + void on_shortcut_key(); + + /// + /// Mirroring from object manipulation panel + /// !! Emboss gizmo must be active + /// + /// Axis for mirroring must be one of {0,1,2} + /// True on success start job otherwise False + bool do_mirror(size_t axis); + + + /// + /// Call on change inside of object conatining projected volume + /// + /// Way to stop re_emboss job + /// True on success otherwise False + static bool re_emboss(const ModelVolume &text, std::shared_ptr> job_cancel = nullptr); + +protected: + bool on_init() override; + std::string on_get_name() const override; + void on_render() override; + void on_register_raycasters_for_picking() override; + void on_unregister_raycasters_for_picking() override; + void on_render_input_window(float x, float y, float bottom_limit) override; + void on_set_state() override; + void data_changed(bool is_serializing) override; // selection changed + void on_set_hover_id() override{ m_rotate_gizmo.set_hover_id(m_hover_id); } + void on_enable_grabber(unsigned int id) override { m_rotate_gizmo.enable_grabber(); } + void on_disable_grabber(unsigned int id) override { m_rotate_gizmo.disable_grabber(); } + void on_start_dragging() override; + void on_stop_dragging() override; + void on_dragging(const UpdateData &data) override; + void push_button_style(bool pressed); + void pop_button_style(); + + /// + /// Rotate by text on dragging rotate grabers + /// + /// Information about mouse + /// Propagete normaly return false. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + bool wants_enter_leave_snapshots() const override; + std::string get_gizmo_entering_text() const override; + std::string get_gizmo_leaving_text() const override; + std::string get_action_snapshot_name() const override; + +private: + void volume_transformation_changing(); + void volume_transformation_changed(); + + static EmbossStyles create_default_styles(); + // localized default text + bool init_create(ModelVolumeType volume_type); + + void set_volume_by_selection(); + void reset_volume(); + + // create volume from text - main functionality + bool process(); + void close(); + void draw_window(); + void draw_text_input(); + void draw_model_type(); + void draw_style_list(); + void draw_delete_style_button(); + void draw_style_rename_popup(); + void draw_style_rename_button(); + void draw_style_save_button(bool is_modified); + void draw_style_save_as_popup(); + void draw_style_add_button(); + void init_font_name_texture(); + void draw_font_list_line(); + void draw_font_list(); + void draw_height(bool use_inch); + void draw_depth(bool use_inch); + + // call after set m_style_manager.get_style().prop.size_in_mm + bool set_height(); + + bool draw_italic_button(); + bool draw_bold_button(); + void draw_advanced(); + + bool select_facename(const wxString& facename); + + template bool rev_input_mm(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, bool use_inch, const std::optional& scale) const; + + /// + /// Reversible input float with option to restor default value + /// TODO: make more general, static and move to ImGuiWrapper + /// + /// True when value changed otherwise FALSE. + template bool rev_input(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, T step, T step_fast, const char *format, ImGuiInputTextFlags flags = 0) const; + bool rev_checkbox(const std::string &name, bool &value, const bool* default_value, const std::string &undo_tooltip) const; + bool rev_slider(const std::string &name, std::optional& value, const std::optional *default_value, + const std::string &undo_tooltip, int v_min, int v_max, const std::string &format, const wxString &tooltip) const; + bool rev_slider(const std::string &name, std::optional& value, const std::optional *default_value, + const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip) const; + bool rev_slider(const std::string &name, float &value, const float *default_value, + const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip) const; + template bool revertible(const std::string &name, T &value, const T *default_value, + const std::string &undo_tooltip, float undo_offset, Draw draw) const; + + // process mouse event + bool on_mouse_for_rotation(const wxMouseEvent &mouse_event); + bool on_mouse_for_translate(const wxMouseEvent &mouse_event); + void on_mouse_change_selection(const wxMouseEvent &mouse_event); + + // When open text loaded from .3mf it could be written with unknown font + bool m_is_unknown_font = false; + void create_notification_not_valid_font(const TextConfiguration& tc); + void create_notification_not_valid_font(const std::string& text); + void remove_notification_not_valid_font(); + + struct GuiCfg; + std::unique_ptr m_gui_cfg; + + // Is open tree with advanced options + bool m_is_advanced_edit_style = false; + + // Keep information about stored styles and loaded actual style to compare with + Emboss::StyleManager m_style_manager; + + // pImpl to hide implementation of FaceNames to .cpp file + struct Facenames; // forward declaration + std::unique_ptr m_face_names; + + // Text to emboss + std::string m_text; // Sequence of Unicode UTF8 symbols + + // When true keep up vector otherwise relative rotation + bool m_keep_up = true; + + // current selected volume + // NOTE: Be carefull could be uninitialized (removed from Model) + ModelVolume *m_volume = 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; + + // True when m_text contain character unknown by selected font + bool m_text_contain_unknown_glyph = false; + + // cancel for previous update of volume to cancel finalize part + std::shared_ptr> m_job_cancel = nullptr; + + // Keep information about curvature of text line around surface + TextLinesModel m_text_lines; + void reinit_text_lines(unsigned count_lines=0); + + // Rotation gizmo + GLGizmoRotate m_rotate_gizmo; + // Value is set only when dragging rotation to calculate actual angle + std::optional m_rotate_start_angle; + + // Keep data about dragging only during drag&drop + std::optional m_surface_drag; + + // Keep old scene triangle data in AABB trees, + // all the time it need actualize before use. + RaycastManager m_raycast_manager; + + // For text on scaled objects + std::optional m_scale_height; + std::optional m_scale_depth; + void calculate_scale(); + + // drawing icons + IconManager m_icon_manager; + IconManager::VIcons m_icons; + void init_icons(); + + // only temporary solution + static const std::string M_ICON_FILENAME; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_GLGizmoEmboss_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 1a6ad83ca..ccf3956f3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -513,7 +513,7 @@ void GLGizmoFdmSupports::show_tooltip_information(float caption_max, float x, fl ImTextureID normal_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP); ImTextureID hover_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER); - caption_max += m_imgui->calc_text_size(": ").x + 15.f; + caption_max += m_imgui->calc_text_size(std::string_view{": "}).x + 15.f; float font_size = ImGui::GetFontSize(); ImVec2 button_size = ImVec2(font_size * 1.8, font_size * 1.3); @@ -584,7 +584,7 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) ++mesh_id; - const Transform3d trafo_matrix = mi->get_matrix(true) * mv->get_matrix(true); + const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp index e4a4f895e..a3632b6ed 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp @@ -165,7 +165,7 @@ void GLGizmoFlatten::update_planes() ch = ch.convex_hull_3d(); m_planes.clear(); on_unregister_raycasters_for_picking(); - const Transform3d& inst_matrix = mo->instances.front()->get_matrix(true); + const Transform3d &inst_matrix = mo->instances.front()->get_matrix_no_offset(); // Following constants are used for discarding too small polygons. const float minimal_area = 5.f; // in square mm (world coordinates) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp index 0c8b42fe1..9293a8187 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp @@ -2042,7 +2042,7 @@ void GLGizmoMeasure::show_tooltip_information(float caption_max, float x, float ImTextureID normal_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP); ImTextureID hover_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER); - caption_max += m_imgui->calc_text_size(": ").x + 35.f; + caption_max += m_imgui->calc_text_size(std::string_view{": "}).x + 35.f; float font_size = ImGui::GetFontSize(); ImVec2 button_size = ImVec2(font_size * 1.8, font_size * 1.3); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp index c40367618..e0af77fee 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp @@ -451,7 +451,7 @@ void GLGizmoMeshBoolean::generate_new_volume(bool delete_input, const TriangleMe new_volume->set_material_id(old_volume->material_id()); new_volume->set_offset(old_volume->get_transformation().get_offset()); //Vec3d translate_z = { 0,0, (new_volume->source.mesh_offset - old_volume->source.mesh_offset).z() }; - //new_volume->translate(new_volume->get_transformation().get_matrix(true) * translate_z); + //new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * translate_z); //new_volume->supported_facets.assign(old_volume->supported_facets); //new_volume->seam_facets.assign(old_volume->seam_facets); //new_volume->mmu_segmentation_facets.assign(old_volume->mmu_segmentation_facets); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index c6ee8f4e0..e44b80cc7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -341,7 +341,7 @@ void GLGizmoMmuSegmentation::show_tooltip_information(float caption_max, float x ImTextureID normal_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP); ImTextureID hover_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER); - caption_max += m_imgui->calc_text_size(": ").x + 15.f; + caption_max += m_imgui->calc_text_size(std::string_view{": "}).x + 15.f; float font_size = ImGui::GetFontSize(); ImVec2 button_size = ImVec2(font_size * 1.8, font_size * 1.3); @@ -408,7 +408,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float filter_btn_width = m_imgui->calc_text_size(m_desc.at("perform")).x + m_imgui->scaled(1.f); const float buttons_width = remove_btn_width + filter_btn_width + m_imgui->scaled(1.f); const float minimal_slider_width = m_imgui->scaled(4.f); - const float color_button_width = m_imgui->calc_text_size("").x + m_imgui->scaled(1.75f); + const float color_button_width = m_imgui->calc_text_size(std::string_view{""}).x + m_imgui->scaled(1.75f); float caption_max = 0.f; float total_text_max = 0.f; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 91913914a..1049e61f5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -107,7 +107,15 @@ void GLGizmoMove3D::on_dragging(const UpdateData& data) m_displacement.z() = calc_projection(data); Selection &selection = m_parent.get_selection(); - selection.translate(m_displacement); + TransformationType trafo_type; + trafo_type.set_relative(); + switch (wxGetApp().obj_manipul()->get_coordinates_type()) + { + case ECoordinatesType::Instance: { trafo_type.set_instance(); break; } + case ECoordinatesType::Local: { trafo_type.set_local(); break; } + default: { break; } + } + selection.translate(m_displacement, trafo_type); } void GLGizmoMove3D::on_render() diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 015e47cbd..599c49088 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -245,7 +245,7 @@ void GLGizmoPainterBase::render_cursor_sphere(const Transform3d& trafo) const if (shader == nullptr) return; - const Transform3d complete_scaling_matrix_inverse = Geometry::Transformation(trafo).get_matrix(true, true, false, true).inverse(); + const Transform3d complete_scaling_matrix_inverse = Geometry::Transformation(trafo).get_matrix_no_scaling_factor().inverse(); // BBS ColorRGBA render_color = this->get_cursor_hover_color(); @@ -536,8 +536,8 @@ std::vector GLGizmoPainterBase::get_pr mi->get_assemble_transformation().get_matrix() : mi->get_transformation().get_matrix(); const Transform3d instance_trafo_not_translate = m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - mi->get_assemble_transformation().get_matrix(true) : - mi->get_transformation().get_matrix(true); + mi->get_assemble_transformation().get_matrix_no_offset() : + mi->get_transformation().get_matrix_no_offset(); for (int mesh_idx = 0; mesh_idx < part_volumes.size(); mesh_idx++) { if (mesh_idx == m_rr.mesh_id) @@ -605,8 +605,8 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const ModelObject *mo = m_c->selection_info()->model_object(); const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; const Transform3d trafo_matrix_not_translate = m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - mi->get_assemble_transformation().get_matrix(true) * mo->volumes[m_rr.mesh_id]->get_matrix(true) : - mi->get_transformation().get_matrix(true) * mo->volumes[m_rr.mesh_id]->get_matrix(true); + mi->get_assemble_transformation().get_matrix_no_offset() * mo->volumes[m_rr.mesh_id]->get_matrix_no_offset() : + mi->get_transformation().get_matrix_no_offset() * mo->volumes[m_rr.mesh_id]->get_matrix_no_offset(); const Transform3d trafo_matrix = m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView ? mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); @@ -675,8 +675,8 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() : mi->get_transformation().get_matrix(); Transform3d instance_trafo_not_translate = m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - mi->get_assemble_transformation().get_matrix(true) : - mi->get_transformation().get_matrix(true); + mi->get_assemble_transformation().get_matrix_no_offset() : + mi->get_transformation().get_matrix_no_offset(); std::vector part_volumes; // Precalculate transformations of individual meshes. @@ -692,7 +692,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous else { trafo_matrices.emplace_back(instance_trafo* mv->get_matrix()); } - trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix(true)); + trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix_no_offset()); part_volumes.push_back(mv); } @@ -824,8 +824,8 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() : mi->get_transformation().get_matrix(); const Transform3d instance_trafo_not_translate = m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - mi->get_assemble_transformation().get_matrix(true) : - mi->get_transformation().get_matrix(true); + mi->get_assemble_transformation().get_matrix_no_offset() : + mi->get_transformation().get_matrix_no_offset(); // Precalculate transformations of individual meshes. std::vector trafo_matrices; @@ -840,7 +840,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous else { trafo_matrices.emplace_back(instance_trafo * mv->get_matrix()); } - trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix(true)); + trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix_no_offset()); } // Now "click" into all the prepared points and spill paint around them. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 1b412c847..5a02e6666 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -6,14 +6,14 @@ #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp" -#include - #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" + #include "libslic3r/PresetBundle.hpp" -#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" +#include namespace Slic3r { namespace GUI { @@ -28,10 +28,16 @@ const float GLGizmoRotate::ScaleLongTooth = 0.1f; // in percent of radius const unsigned int GLGizmoRotate::SnapRegionsCount = 8; const float GLGizmoRotate::GrabberOffset = 0.15f; // in percent of radius - GLGizmoRotate::GLGizmoRotate(GLCanvas3D& parent, GLGizmoRotate::Axis axis) : GLGizmoBase(parent, "", -1) , m_axis(axis) + , m_angle(0.0) + , m_center(0.0, 0.0, 0.0) + , m_radius(0.0f) + , m_snap_coarse_in_radius(0.0f) + , m_snap_coarse_out_radius(0.0f) + , m_snap_fine_in_radius(0.0f) + , m_snap_fine_out_radius(0.0f) , m_drag_color(DEFAULT_DRAG_COLOR) , m_highlight_color(DEFAULT_HIGHLIGHT_COLOR) { @@ -94,19 +100,12 @@ bool GLGizmoRotate::on_init() void GLGizmoRotate::on_start_dragging() { - const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); - m_center = m_custom_center == Vec3d::Zero() ? box.center() : m_custom_center; - m_radius = Offset + box.radius(); - m_snap_coarse_in_radius = m_radius / 3.0f; - m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; - m_snap_fine_in_radius = m_radius; - m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; + init_data_from_selection(m_parent.get_selection()); } void GLGizmoRotate::on_dragging(const UpdateData &data) { - const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray, m_parent.get_selection())); - + const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane(data.mouse_ray)); const Vec2d orig_dir = Vec2d::UnitX(); const Vec2d new_dir = mouse_pos.normalized(); @@ -141,16 +140,8 @@ void GLGizmoRotate::on_render() return; const Selection& selection = m_parent.get_selection(); - const BoundingBoxf3& box = selection.get_bounding_box(); - - if (m_hover_id != 0 && !m_grabbers.front().dragging) { - m_center = m_custom_center == Vec3d::Zero() ? box.center() : m_custom_center; - m_radius = Offset + box.radius(); - m_snap_coarse_in_radius = m_radius / 3.0f; - m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; - m_snap_fine_in_radius = m_radius; - m_snap_fine_out_radius = m_radius * (1.0f + ScaleLongTooth); - } + if (m_hover_id != 0 && !m_grabbers.front().dragging) + init_data_from_selection(selection); const double grabber_radius = (double)m_radius * (1.0 + (double)GrabberOffset); m_grabbers.front().center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0); @@ -168,15 +159,14 @@ void GLGizmoRotate::on_render() shader->start_using(); const Camera& camera = wxGetApp().plater()->get_camera(); - Transform3d view_model_matrix = camera.get_view_matrix() * m_grabbers.front().matrix; - + const Transform3d view_model_matrix = camera.get_view_matrix() * m_grabbers.front().matrix; shader->set_uniform("view_model_matrix", view_model_matrix); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); const bool radius_changed = std::abs(m_old_radius - m_radius) > EPSILON; m_old_radius = m_radius; - ColorRGBA color((m_hover_id != -1) ? m_drag_color : m_highlight_color); + const ColorRGBA color = (m_hover_id != -1) ? m_drag_color : m_highlight_color; render_circle(color, radius_changed); if (m_hover_id != -1) { const bool hover_radius_changed = std::abs(m_old_hover_radius - m_radius) > EPSILON; @@ -192,7 +182,23 @@ void GLGizmoRotate::on_render() shader->stop_using(); } - render_grabber(box); + render_grabber(m_bounding_box); +} + +void GLGizmoRotate::init_data_from_selection(const Selection& selection) +{ + const auto [box, box_trafo] = m_force_local_coordinate ? + selection.get_bounding_box_in_reference_system(ECoordinatesType::Local) : selection.get_bounding_box_in_current_reference_system(); + m_bounding_box = box; + const std::pair sphere = selection.get_bounding_sphere(); + m_center = sphere.first; + m_radius = Offset + sphere.second; + m_orient_matrix = box_trafo; + m_orient_matrix.translation() = m_center; + m_snap_coarse_in_radius = m_radius / 3.0f; + m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; + m_snap_fine_in_radius = m_radius; + m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; } //BBS: add input window for move @@ -422,12 +428,12 @@ Transform3d GLGizmoRotate::local_transform(const Selection& selection) const { case X: { - ret = Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()); + ret = Geometry::rotation_transform(0.5 * PI * Vec3d::UnitY()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()); break; } case Y: { - ret = Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitY()); + ret = Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitY()); break; } default: @@ -438,13 +444,10 @@ Transform3d GLGizmoRotate::local_transform(const Selection& selection) const } } - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) - ret = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true) * ret; - - return Geometry::assemble_transform(m_center) * ret; + return m_orient_matrix * ret; } -Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const +Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray) const { const double half_pi = 0.5 * double(PI); @@ -472,8 +475,7 @@ Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray, cons } } - if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) - m = m * selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true).inverse(); + m = m * Geometry::Transformation(m_orient_matrix).get_matrix_no_offset().inverse(); m.translate(-m_center); @@ -496,19 +498,32 @@ Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray, cons //BBS: GUI refactor: add obj manipulation GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id, GizmoObjectManipulation* obj_manipulation) : GLGizmoBase(parent, icon_filename, sprite_id) - , m_gizmos({ GLGizmoRotate(parent, GLGizmoRotate::X), GLGizmoRotate(parent, GLGizmoRotate::Y), GLGizmoRotate(parent, GLGizmoRotate::Z) }) - //BBS: GUI refactor: add obj manipulation + , m_gizmos({ + GLGizmoRotate(parent, GLGizmoRotate::X), + GLGizmoRotate(parent, GLGizmoRotate::Y), + GLGizmoRotate(parent, GLGizmoRotate::Z) }) + //BBS: GUI refactor: add obj manipulation , m_object_manipulation(obj_manipulation) { load_rotoptimize_state(); } -bool GLGizmoRotate3D::on_mouse(const wxMouseEvent &mouse_event) { - +bool GLGizmoRotate3D::on_mouse(const wxMouseEvent &mouse_event) +{ if (mouse_event.Dragging() && m_dragging) { // Apply new temporary rotations - TransformationType transformation_type( - TransformationType::World_Relative_Joint); + TransformationType transformation_type; + if (m_parent.get_selection().is_wipe_tower()) + transformation_type = TransformationType::World_Relative_Joint; + else { + switch (wxGetApp().obj_manipul()->get_coordinates_type()) + { + default: + case ECoordinatesType::World: { transformation_type = TransformationType::World_Relative_Joint; break; } + case ECoordinatesType::Instance: { transformation_type = TransformationType::Instance_Relative_Joint; break; } + case ECoordinatesType::Local: { transformation_type = TransformationType::Local_Relative_Joint; break; } + } + } if (mouse_event.AltDown()) transformation_type.set_independent(); m_parent.get_selection().rotate(get_rotation(), transformation_type); @@ -669,11 +684,12 @@ GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, ImVec2 button_sz = {btn_txt_sz.x + padding.x, btn_txt_sz.y + padding.y}; ImGui::SetCursorPosX(padding.x + sz.x - button_sz.x); - if (wxGetApp().plater()->is_any_job_running()) + if (!wxGetApp().plater()->get_ui_job_worker().is_idle()) imgui->disabled_begin(true); if ( imgui->button(btn_txt) ) { - wxGetApp().plater()->optimize_rotation(); + replace_job(wxGetApp().plater()->get_ui_job_worker(), + std::make_unique()); } imgui->disabled_end(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index edafc5ae2..bc846f0ad 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -6,14 +6,13 @@ #define slic3r_GLGizmoRotate_hpp_ #include "GLGizmoBase.hpp" -#include "../Jobs/RotoptimizeJob.hpp" //BBS: add size adjust related #include "GizmoObjectManipulation.hpp" - namespace Slic3r { namespace GUI { class Selection; + class GLGizmoRotate : public GLGizmoBase { static const float Offset; @@ -36,16 +35,14 @@ public: private: Axis m_axis; double m_angle{ 0.0 }; - Vec3d m_custom_center{Vec3d::Zero()}; Vec3d m_center{ Vec3d::Zero() }; float m_radius{ 0.0f }; float m_snap_coarse_in_radius{ 0.0f }; float m_snap_coarse_out_radius{ 0.0f }; float m_snap_fine_in_radius{ 0.0f }; float m_snap_fine_out_radius{ 0.0f }; - - ColorRGBA m_drag_color; - ColorRGBA m_highlight_color; + BoundingBoxf3 m_bounding_box; + Transform3d m_orient_matrix{ Transform3d::Identity() }; GLModel m_circle; GLModel m_scale; @@ -62,6 +59,12 @@ private: float m_old_hover_radius{ 0.0f }; float m_old_angle{ 0.0f }; + // emboss need to draw rotation gizmo in local coordinate systems + bool m_force_local_coordinate{ false }; + + ColorRGBA m_drag_color; + ColorRGBA m_highlight_color; + public: GLGizmoRotate(GLCanvas3D& parent, Axis axis); virtual ~GLGizmoRotate() = default; @@ -71,7 +74,8 @@ public: std::string get_tooltip() const override; - void set_center(const Vec3d &point) { m_custom_center = point; } + void set_group_id(int group_id) { m_group_id = group_id; } + void set_force_local_coordinate(bool use) { m_force_local_coordinate = use; } void start_dragging(); void stop_dragging(); @@ -109,7 +113,9 @@ private: Transform3d local_transform(const Selection& selection) const; // returns the intersection of the mouse ray with the plane perpendicular to the gizmo axis, in local coordinate - Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const; + Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray) const; + + void init_data_from_selection(const Selection& selection); }; class GLGizmoRotate3D : public GLGizmoBase @@ -138,13 +144,6 @@ public: return tooltip; } - void set_center(const Vec3d &point) - { - m_gizmos[X].set_center(point); - m_gizmos[Y].set_center(point); - m_gizmos[Z].set_center(point); - } - /// /// Postpone to Rotation /// @@ -164,7 +163,14 @@ protected: for (int i = 0; i < 3; ++i) m_gizmos[i].set_hover_id((m_hover_id == i) ? 0 : -1); } - + void on_enable_grabber(unsigned int id) override { + if (id < 3) + m_gizmos[id].enable_grabber(); + } + void on_disable_grabber(unsigned int id) override { + if (id < 3) + m_gizmos[id].disable_grabber(); + } bool on_is_activable() const override; void on_start_dragging() override; void on_stop_dragging() override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp new file mode 100644 index 000000000..806ca3f28 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp @@ -0,0 +1,2239 @@ +#include "GLGizmoSVG.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.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/Plater.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/CameraUtils.hpp" +#include "slic3r/GUI/Jobs/EmbossJob.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/Emboss.hpp" // heal_shape + +#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 // measure enumeration of fonts +#include // save for svg +#include +#include + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + +GLGizmoSVG::GLGizmoSVG(GLCanvas3D &parent) + : GLGizmoBase(parent, M_ICON_FILENAME, -3) + , 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); +} + +// Private functions to create emboss volume +namespace{ + +// 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; + +// Store path to directory with svg for import and export svg's +wxString last_used_directory = wxEmptyString; + +/// +/// Open file dialog with svg files +/// +/// File path to svg +std::string choose_svg_file(); + +constexpr double get_tesselation_tolerance(double scale){ + constexpr double tesselation_tolerance_in_mm = .1; //8e-2; + constexpr double tesselation_tolerance_scaled = (tesselation_tolerance_in_mm*tesselation_tolerance_in_mm) / SCALING_FACTOR / SCALING_FACTOR; + return tesselation_tolerance_scaled / scale / scale; +} + +/// +/// Let user to choose file with (S)calable (V)ector (G)raphics - SVG. +/// Than let select contour +/// +/// SVG file path, when empty promt user to select one +/// EmbossShape to create +EmbossShape select_shape(std::string_view filepath = "", double tesselation_tolerance_in_mm = get_tesselation_tolerance(1.)); + +/// +/// Create new embos data +/// +/// Cancel for previous job +/// To distiquish whether it is outside of model +/// SVG file path +/// Base data for emboss SVG +DataBasePtr create_emboss_data_base(std::shared_ptr> &cancel, ModelVolumeType volume_type, std::string_view filepath = ""); + +/// +/// Separate file name from file path. +/// String after last delimiter and before last point +/// +/// path return by file dialog +/// File name without directory path +std::string get_file_name(const std::string &file_path); + +/// +/// Create volume name from shape information +/// +/// File path +/// Name for volume +std::string volume_name(const EmbossShape& shape); + +/// +/// Create input for volume creation +/// +/// parent of gizmo +/// Keep scene +/// Type of volume to be created +/// Params +CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager &raycaster, ModelVolumeType volume_type); + +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 +}; +// Do not forgot add loading of file in funtion: +// IconManager::Icons init_icons( + +// 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]; } + +// 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(); + +} // namespace + +// use private definition +struct GLGizmoSVG::GuiCfg: public ::GuiCfg{}; + +bool GLGizmoSVG::create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type); + if (!base) return false; // Uninterpretable svg + return start_create_volume(input, std::move(base), mouse_pos); +} + +bool GLGizmoSVG::create_volume(ModelVolumeType volume_type) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel,volume_type); + if (!base) return false; // Uninterpretable svg + return start_create_volume_without_position(input, std::move(base)); +} + +bool GLGizmoSVG::create_volume(std::string_view svg_file, ModelVolumeType volume_type){ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + DataBasePtr base = create_emboss_data_base(m_job_cancel, volume_type, svg_file); + if (!base) return false; // Uninterpretable svg + return start_create_volume_without_position(input, std::move(base)); +} + +bool GLGizmoSVG::create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type) +{ + CreateVolumeParams input = create_input(m_parent, m_raycast_manager, volume_type); + 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(const ModelVolume &volume) { + return volume.emboss_shape.has_value(); +} + +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::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(); + + // 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::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(); + } 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(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 (on_mouse_for_rotation(mouse_event)) return true; + if (on_mouse_for_translate(mouse_event)) return true; + + return false; +} + +bool GLGizmoSVG::wants_enter_leave_snapshots() const { return true; } +std::string GLGizmoSVG::get_gizmo_entering_text() const { return _u8L("Enter SVG gizmo"); } +std::string GLGizmoSVG::get_gizmo_leaving_text() const { return _u8L("Leave SVG gizmo"); } +std::string GLGizmoSVG::get_action_snapshot_name() const { return _u8L("SVG actions"); } + +bool GLGizmoSVG::on_init() +{ + m_rotate_gizmo.init(); + ColorRGBA gray_color(.6f, .6f, .6f, .3f); + m_rotate_gizmo.set_highlight_color(gray_color); + // Set rotation gizmo upwardrotate + m_rotate_gizmo.set_angle(PI / 2); + return true; +} + +std::string GLGizmoSVG::on_get_name() const { return _u8L("SVG"); } + +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; + + 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; + if (is_rotate_by_grabbers || + (!is_surface_dragging && !is_parent_dragging)) { + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + m_rotate_gizmo.render(); + } +} + +void GLGizmoSVG::on_register_raycasters_for_picking(){ + m_rotate_gizmo.register_raycasters_for_picking(); +} +void GLGizmoSVG::on_unregister_raycasters_for_picking(){ + m_rotate_gizmo.unregister_raycasters_for_picking(); +} + +namespace{ +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{ + "undo.svg", // reset_value + "refresh.svg", // refresh + "open.svg", // changhe_file + "burn.svg", // bake + "save.svg", // save + "exclamation.svg", // exclamation + "lock_closed.svg", // lock + "lock_open.svg", // unlock + "reflection_x.svg", // reflection_x + "reflection_y.svg", // reflection_y + }; + + assert(init_types.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); + + //IconManager::VIcons vicons = mng.init(init_types); + // + //// flatten icons + //IconManager::Icons icons; + //icons.reserve(vicons.size()); + //for (IconManager::Icons &i : vicons) + // icons.push_back(i.front()); + //return icons; +} +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); +} + +} // namespace + +void GLGizmoSVG::on_render_input_window(float x, float y, float bottom_limit) +{ + set_volume_by_selection(); + + double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor(); + + // Orca + ImGuiWrapper::push_toolbar_style(m_parent.get_scale()); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0, 5.0) * screen_scale); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 4.0f * screen_scale); + + // 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) + } + + // Draw origin position of text during dragging + if (m_surface_drag.has_value()) { + ImVec2 mouse_pos = ImGui::GetMousePos(); + ImVec2 center( + mouse_pos.x + m_surface_drag->mouse_offset.x(), + mouse_pos.y + m_surface_drag->mouse_offset.y()); + ImU32 color = ImGui::GetColorU32( + m_surface_drag->exist_hit ? + ImVec4(1.f, 1.f, 1.f, .75f) : // transparent white + ImVec4(1.f, .3f, .3f, .75f) + ); // Warning color + const float radius = 16.f; + ImGuiWrapper::draw_cross_hair(center, radius, color); + } + + static float last_y = 0.0f; + static float last_h = 0.0f; + + // adjust window position to avoid overlap the view toolbar + 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; + } + + GizmoImguiBegin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); + + draw_window(); + + GizmoImguiEnd(); + + // Orca + ImGui::PopStyleVar(2); + ImGuiWrapper::pop_toolbar_style(); +} + +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) { + reset_volume(); + } else if (GLGizmoBase::m_state == GLGizmoBase::On) { + // Try(when exist) set text configuration by volume + set_volume_by_selection(); + } +} + +void GLGizmoSVG::data_changed(bool is_serializing) { + set_volume_by_selection(); + if (!is_serializing && m_volume == nullptr) + close(); +} + +void GLGizmoSVG::on_start_dragging() { m_rotate_gizmo.start_dragging(); } +void GLGizmoSVG::on_stop_dragging() +{ + 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(L("SVG-Rotate")); + 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(); +} +void GLGizmoSVG::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(data); } + +#include "slic3r/GUI/BitmapCache.hpp" +#include "nanosvg/nanosvgrast.h" +#include "libslic3r/AABBTreeLines.hpp" // aabb lines for draw filled expolygon + +namespace{ + +// 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; + } + } + } +} + +#ifdef MORE_DRAWING +// Wu's line algorithm - https://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm +void wu_draw_line(Linef line, + const std::function& plot) { + auto ipart = [](float x) -> int {return int(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()); + if (steep) { + std::swap(line.a.x(), line.a.y()); + std::swap(line.b.x(), line.b.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 = line.b - line.a; + 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.5); + xpx11 = int(xend); + const int ypx11 = ipart(yend); + if (steep) { + plot(ypx11, xpx11, rfpart(yend) * xgap); + plot(ypx11 + 1, xpx11, fpart(yend) * xgap); + } else { + plot(xpx11, ypx11, rfpart(yend) * xgap); + plot(xpx11, ypx11 + 1, 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.5); + xpx12 = int(xend); + const int ypx12 = ipart(yend); + if (steep) { + plot(ypx12, xpx12, rfpart(yend) * xgap); + plot(ypx12 + 1, xpx12, fpart(yend) * xgap); + } else { + plot(xpx12, ypx12, rfpart(yend) * xgap); + plot(xpx12, ypx12 + 1, fpart(yend) * xgap); + } + } + + if (steep) { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(ipart(intery), x, rfpart(intery)); + plot(ipart(intery) + 1, x, fpart(intery)); + intery += gradient; + } + } else { + for (int x = xpx11 + 1; x < xpx12; x++) { + plot(x, ipart(intery), rfpart(intery)); + plot(x, ipart(intery) + 1, fpart(intery)); + intery += gradient; + } + } +} + +void draw(const ExPolygonsWithIds &shapes_with_ids, unsigned max_size) +{ + ImVec2 actual_pos = ImGui::GetCursorPos(); + // draw shapes + BoundingBox bb; + for (const ExPolygonsWithId &shape : shapes_with_ids) + bb.merge(get_extents(shape.expoly)); + + Point bb_size = bb.size(); + double scale = max_size / (double) std::max(bb_size.x(), bb_size.y()); + ImVec2 win_offset = ImGui::GetWindowPos(); + Point offset(win_offset.x + actual_pos.x, win_offset.y + actual_pos.y); + offset += bb_size / 2 * scale; + auto draw_polygon = [&scale, offset](Slic3r::Polygon p) { + p.scale(scale, -scale); // Y mirror + p.translate(offset); + ImGuiWrapper::draw(p); + }; + + for (const ExPolygonsWithId &shape : shapes_with_ids) { + for (const ExPolygon &expoly : shape.expoly) { + draw_polygon(expoly.contour); + for (const Slic3r::Polygon &hole : expoly.holes) + draw_polygon(hole); + } + } +} + +#endif // MORE_DRAWING + +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 init_texture(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; +} + +/// +/// 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 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; +} + + +} // namespace + +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 + + 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_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); +} +namespace { +void delete_texture(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_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([&texture = m_texture]() { delete_texture(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); +} + +float GLGizmoSVG::get_scale_for_tolerance(){ + return std::max(m_scale_width.value_or(1.f), m_scale_height.value_or(1.f)); } + +bool GLGizmoSVG::process() +{ + // 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; + DataUpdate data{std::move(base), m_volume_id}; + 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(); + 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(); + } +} + +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_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; + m_volume_shape.svg_file = {}; // clear data + m_volume_shape.svg_file->path = new_path; + } + } 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)); + //ImGui::SameLine(); + //if (ImGui::Selectable(_u8L("Bake 2 ©").c_str())) { + // EmbossShape::SvgFile &svg = m_volume_shape.svg_file; + // std::stringstream ss; + // Slic3r::save(*svg.image, ss); + // svg.file_data = std::make_unique(ss.str()); + // svg.image = nsvgParse(*svg.file_data); + // assert(svg.image.get() != NULL); + // if (svg.image.get() != NULL) { + // m_volume->emboss_shape->svg_file = svg; // copy - write changes into volume + // } else { + // svg = m_volume->emboss_shape->svg_file; // revert changes + // } + //} else if (ImGui::IsItemHovered()) { + // ImGui::SetTooltip("%s", _u8L("Use only paths from svg - recreate svg").c_str()); + //} + + 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").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(); + } +} + +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 = "%.1f 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(); + } + } 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); + if (use_inch) width *= GizmoObjectManipulation::mm_to_in; + 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; + if (m_keep_ratio) { + std::stringstream ss; + ss << std::setprecision(2) << std::fixed << width << " x " << height << " " << (use_inch ? "in" : "mm"); + + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + + const MinMax &minmax = use_inch ? limits.ui_size_in : limits.ui_size; + // convert to float for slider + float width_f = static_cast(width); + if (m_imgui->slider_float("##width_size_slider", &width_f, minmax.min, minmax.max, ss.str().c_str(), 1.f, 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.); + } + } + } 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)) { + 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.); + } + } + 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)) { + 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.); + } + } + 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.); + } 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); + + m_parent.do_scale(L("Resize")); + wxGetApp().obj_manipul()->set_dirty(); + // should be the almost same + calculate_scale(); + + 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 + process(); + } + } +} + +void GLGizmoSVG::draw_use_surface() +{ + bool 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(); + m_imgui->disabled_begin(!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 (m_imgui->bbl_checkbox("##useSurface", m_volume_shape.projection.use_surface)) + process(); +} + +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)) { + 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)) + is_moved = true; + } + + bool can_reset = m_distance.has_value(); + if (can_reset) { + if (reset_button(m_icons)) { + m_distance.reset(); + is_moved = true; + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Reset distance"), m_gui_cfg->max_tooltip_width); + } + + if (is_moved) + do_local_z_move(m_parent, m_distance.value_or(.0f) - prev_distance); +} + +void GLGizmoSVG::draw_rotation() +{ + 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, 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(); + } + + // Reset button + if (m_angle.has_value()) { + if (reset_button(m_icons)) { + do_local_z_rotate(m_parent, -(*m_angle)); + m_angle.reset(); + + // recalculate for surface cut + if (m_volume->emboss_shape->projection.use_surface) + process(); + } else if (ImGui::IsItemHovered()) + m_imgui->tooltip(_u8L("Reset rotation"), m_gui_cfg->max_tooltip_width); + } + + // 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); + } +} + +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(); + } +} + +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(); + + // 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 + } +} + + +///////////// +// private namespace implementation +/////////////// +namespace { + +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"; +} + +CreateVolumeParams create_input(GLCanvas3D &canvas, RaycastManager& raycaster, ModelVolumeType volume_type) +{ + auto gizmo = static_cast(GLGizmosManager::Svg); + const GLVolume *gl_volume = get_first_hovered_gl_volume(canvas); + Plater *plater = wxGetApp().plater(); + return CreateVolumeParams{canvas, plater->get_camera(), plater->build_volume(), + plater->get_ui_job_worker(), volume_type, raycaster, gizmo, gl_volume}; +} + +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, + 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 + 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; +} + +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; +} + +EmbossShape select_shape(std::string_view filepath, double tesselation_tolerance) +{ + 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; +} + +DataBasePtr 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 /*copy*/, std::move(shape)); + result->is_outside = volume_type == ModelVolumeType::MODEL_PART; + return result; +} +} // namespace + +// any existing icon filename to not influence GUI +const std::string GLGizmoSVG::M_ICON_FILENAME = "cut.svg"; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp new file mode 100644 index 000000000..0a2dedbdd --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSVG.hpp @@ -0,0 +1,200 @@ +#ifndef slic3r_GLGizmoSVG_hpp_ +#define slic3r_GLGizmoSVG_hpp_ + +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, +// which overrides our localization "L" macro. +#include "GLGizmoBase.hpp" +#include "GLGizmoRotate.hpp" +#include "slic3r/GUI/SurfaceDrag.hpp" +#include "slic3r/GUI/GLTexture.hpp" +#include "slic3r/Utils/RaycastManager.hpp" +#include "slic3r/GUI/IconManager.hpp" + +#include +#include +#include + +#include "libslic3r/Emboss.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Model.hpp" + +#include +#include + +namespace Slic3r{ +class ModelVolume; +enum class ModelVolumeType : int; +} + +namespace Slic3r::GUI { + +struct Texture{ + unsigned id{0}; + unsigned width{0}; + unsigned height{0}; +}; + +class GLGizmoSVG : public GLGizmoBase +{ +public: + explicit GLGizmoSVG(GLCanvas3D &parent); + + /// + /// Create new embossed text volume by type on position of mouse + /// + /// Object part / Negative volume / Modifier + /// Define position of new volume + /// True on succesfull start creation otherwise False + bool create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); // first open file dialog + + /// + /// Create new text without given position + /// + /// Object part / Negative volume / Modifier + /// True on succesfull start creation otherwise False + bool create_volume(ModelVolumeType volume_type); // first open file dialog + + /// + /// Create volume from already selected svg file + /// + /// File path + /// Position on screen where to create volume + /// Object part / Negative volume / Modifier + /// True on succesfull start creation otherwise False + bool create_volume(std::string_view svg_file, const Vec2d &mouse_pos, ModelVolumeType volume_type = ModelVolumeType::MODEL_PART); + bool create_volume(std::string_view svg_file, 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); + +protected: + bool on_init() override; + std::string on_get_name() const override; + void on_render() override; + void on_register_raycasters_for_picking() override; + void on_unregister_raycasters_for_picking() override; + void on_render_input_window(float x, float y, float bottom_limit) override; + bool on_is_activable() const override { return true; } + bool on_is_selectable() const override { return false; } + void on_set_state() override; + void data_changed(bool is_serializing) override; // selection changed + void on_set_hover_id() override{ m_rotate_gizmo.set_hover_id(m_hover_id); } + void on_enable_grabber(unsigned int id) override { m_rotate_gizmo.enable_grabber(); } + void on_disable_grabber(unsigned int id) override { m_rotate_gizmo.disable_grabber(); } + void on_start_dragging() override; + void on_stop_dragging() override; + void on_dragging(const UpdateData &data) override; + + /// + /// Rotate by text on dragging rotate grabers + /// + /// Information about mouse + /// Propagete normaly return false. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + bool wants_enter_leave_snapshots() const override; + std::string get_gizmo_entering_text() const override; + std::string get_gizmo_leaving_text() const override; + std::string get_action_snapshot_name() const override; +private: + void set_volume_by_selection(); + void reset_volume(); + + // create volume from text - main functionality + bool process(); + void close(); + void draw_window(); + 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(); + + // process mouse event + bool on_mouse_for_rotation(const wxMouseEvent &mouse_event); + bool on_mouse_for_translate(const wxMouseEvent &mouse_event); + + void volume_transformation_changed(); + + struct GuiCfg; + std::unique_ptr m_gui_cfg; + + // actual selected only one volume - with emboss data + ModelVolume *m_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; + + // 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; + + // cancel for previous update of volume to cancel finalize part + std::shared_ptr> m_job_cancel = nullptr; + + // Rotation gizmo + GLGizmoRotate m_rotate_gizmo; + std::optional m_angle; + std::optional m_distance; + + // 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; + + // 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; + void calculate_scale(); + float get_scale_for_tolerance(); + + // keep SVG data rendered on GPU + Texture m_texture; + + // bounding box of shape + // Note: Scaled mm to int value by m_volume_shape.scale + BoundingBox m_shape_bb; + + std::string m_filename_preview; + + IconManager m_icon_manager; + IconManager::VIcons m_icons; + + // only temporary solution + static const std::string M_ICON_FILENAME; +}; +} // namespace Slic3r::GUI + +#endif // slic3r_GLGizmoSVG_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp index 93fbf7d9b..147c80ded 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp @@ -48,7 +48,7 @@ std::string GLGizmoScale3D::get_tooltip() const const Selection& selection = m_parent.get_selection(); bool single_instance = selection.is_single_full_instance(); - bool single_volume = selection.is_single_modifier() || selection.is_single_volume(); + bool single_volume = selection.is_single_volume_or_modifier(); Vec3f scale = 100.0f * Vec3f::Ones(); if (single_instance) @@ -85,13 +85,21 @@ bool GLGizmoScale3D::on_mouse(const wxMouseEvent &mouse_event) if (mouse_event.Dragging()) { if (m_dragging) { // Apply new temporary scale factors - TransformationType transformation_type(TransformationType::Local_Absolute_Joint); + Selection& selection = m_parent.get_selection(); + TransformationType transformation_type; + if (selection.is_single_full_instance()) { + transformation_type.set_instance(); + } else if (selection.is_single_volume_or_modifier()) { + transformation_type.set_local(); + } + + transformation_type.set_relative(); + if (mouse_event.AltDown()) transformation_type.set_independent(); - Selection& selection = m_parent.get_selection(); selection.scale(m_scale, transformation_type); - if (mouse_event.CmdDown()) selection.translate(m_offset, true); + if (mouse_event.CmdDown()) selection.translate(m_offset, transformation_type); } } return use_grabbers(mouse_event); @@ -100,24 +108,11 @@ bool GLGizmoScale3D::on_mouse(const wxMouseEvent &mouse_event) void GLGizmoScale3D::data_changed(bool is_serializing) { const Selection &selection = m_parent.get_selection(); bool enable_scale_xyz = selection.is_single_full_instance() || - selection.is_single_volume() || - selection.is_single_modifier(); + selection.is_single_volume_or_modifier(); for (unsigned int i = 0; i < 6; ++i) m_grabbers[i].enabled = enable_scale_xyz; - if (enable_scale_xyz) { - // all volumes in the selection belongs to the same instance, any of - // them contains the needed data, so we take the first - const GLVolume *volume = selection.get_first_volume(); - if (selection.is_single_full_instance()) { - set_scale(volume->get_instance_scaling_factor()); - } else if (selection.is_single_volume() || - selection.is_single_modifier()) { - set_scale(volume->get_volume_scaling_factor()); - } - } else { - set_scale(Vec3d::Ones()); - } + set_scale(Vec3d::Ones()); } bool GLGizmoScale3D::on_init() @@ -193,7 +188,7 @@ void GLGizmoScale3D::on_render() const Selection& selection = m_parent.get_selection(); bool single_instance = selection.is_single_full_instance(); - bool single_volume = selection.is_single_modifier() || selection.is_single_volume(); + bool single_volume = selection.is_single_volume_or_modifier(); glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp index 09e999ec5..e534b2b3c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -151,7 +151,7 @@ void GLGizmoSeam::show_tooltip_information(float caption_max, float x, float y) ImTextureID normal_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP); ImTextureID hover_id = m_parent.get_gizmos_manager().get_icon_texture_id(GLGizmosManager::MENU_ICON_NAME::IC_TOOLBAR_TOOLTIP_HOVER); - caption_max += m_imgui->calc_text_size(": ").x + 35.f; + caption_max += m_imgui->calc_text_size(std::string_view{": "}).x + 35.f; float font_size = ImGui::GetFontSize(); ImVec2 button_size = ImVec2(font_size * 1.8, font_size * 1.3); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index ca1575bba..2fa96de16 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -582,7 +582,7 @@ void GLGizmoSimplify::on_set_state() void GLGizmoSimplify::create_gui_cfg() { if (m_gui_cfg.has_value()) return; - int space_size = m_imgui->calc_text_size(":MM").x; + int space_size = m_imgui->calc_text_size(std::string_view{":MM"}).x; GuiCfg cfg; cfg.top_left_width = std::max(m_imgui->calc_text_size(tr_mesh_name).x, m_imgui->calc_text_size(tr_triangles).x) diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 3b29a986d..7ab41bb4f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -26,8 +26,9 @@ #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMeasure.hpp" -#include "slic3r/GUI/Gizmos/GLGizmoText.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp" #include "libslic3r/format.hpp" @@ -152,7 +153,7 @@ void GLGizmosManager::switch_gizmos_icon_filename() case(EType::Seam): gizmo->set_icon_filename(m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg"); break; - case(EType::Text): + case(EType::Emboss): gizmo->set_icon_filename(m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg"); break; case(EType::MmuSegmentation): @@ -198,7 +199,8 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoMeshBoolean(m_parent, m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg", EType::MeshBoolean)); 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 GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss)); + m_gizmos.emplace_back(new GLGizmoSVG(m_parent)); 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 GLGizmoSimplify(m_parent, "reduce_triangles.svg", EType::Simplify)); @@ -251,27 +253,6 @@ bool GLGizmosManager::init_icon_textures() else return false; - - if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/text_B.svg", 20, 20, texture_id)) - icon_list.insert(std::make_pair((int)IC_TEXT_B, texture_id)); - else - return false; - - if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/text_B_dark.svg", 20, 20, texture_id)) - icon_list.insert(std::make_pair((int)IC_TEXT_B_DARK, texture_id)); - else - return false; - - if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/text_T.svg", 20, 20, texture_id)) - icon_list.insert(std::make_pair((int)IC_TEXT_T, texture_id)); - else - return false; - - if (IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/text_T_dark.svg", 20, 20, texture_id)) - icon_list.insert(std::make_pair((int)IC_TEXT_T_DARK, texture_id)); - else - return false; - return true; } @@ -328,7 +309,8 @@ void GLGizmosManager::reset_all_states() open_gizmo(current); activate_gizmo(Undefined); - m_hover = Undefined; + // Orca: do not clear hover state, as Emboss gizmo can be used without selection + //m_hover = Undefined; } bool GLGizmosManager::open_gizmo(EType type) @@ -447,8 +429,6 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[Seam].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == MmuSegmentation) 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 == Measure) return dynamic_cast(m_gizmos[Measure].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == Cut) @@ -609,7 +589,12 @@ bool GLGizmosManager::gizmos_toolbar_on_mouse(const wxMouseEvent &mouse_event) { // mouse is above toolbar if (mouse_event.LeftDown() || mouse_event.LeftDClick()) { mc.left = true; - open_gizmo(gizmo); + if (gizmo == Emboss) { + GLGizmoBase *gizmo_emboss = m_gizmos[Emboss].get(); + dynamic_cast(gizmo_emboss)->on_shortcut_key(); + } else { + open_gizmo(gizmo); + } return true; } else if (mouse_event.RightDown()) { @@ -1129,12 +1114,14 @@ void GLGizmosManager::do_render_overlay() const const float v_bottom = v_top + dv - v_offset; GLTexture::render_sub_texture(icons_texture_id, top_x, top_x + icons_size_x, top_y - icons_size_y, 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 == Emboss)) { //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 //gizmo->render_input_window(width, 0.5f * cnv_h - zoomed_top_y * zoom, toolbar_top); - gizmo->render_input_window(0.5 * cnv_w + 0.5f * top_x * cnv_w, get_scaled_total_height(), cnv_h); + m_gizmos[m_current]->render_input_window(0.5 * cnv_w + 0.5f * top_x * cnv_w, get_scaled_total_height(), cnv_h); is_render_current = true; } @@ -1337,7 +1324,7 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type) return "FdmSupports"; case GLGizmosManager::EType::Seam: return "Seam"; - case GLGizmosManager::EType::Text: + case GLGizmosManager::EType::Emboss: return "Text"; case GLGizmosManager::EType::MmuSegmentation: return "Color Painting"; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 02169e08d..cc6ca682e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -85,8 +85,8 @@ public: MeshBoolean, FdmSupports, Seam, - // BBS - Text, + Emboss, + Svg, MmuSegmentation, Measure, Simplify, @@ -163,10 +163,6 @@ public: IC_TOOLBAR_RESET_HOVER, IC_TOOLBAR_TOOLTIP, IC_TOOLBAR_TOOLTIP_HOVER, - IC_TEXT_B, - IC_TEXT_B_DARK, - IC_TEXT_T, - IC_TEXT_T_DARK, IC_NAME_COUNT, }; diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp index 1a5b89159..776830bbe 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Pavel Mikuš @Godrak, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv, Vojtěch Král @vojtechkral +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "slic3r/GUI/ImGuiWrapper.hpp" #include @@ -19,7 +23,7 @@ #include #define MAX_NUM 9999.99 -#define MAX_SIZE "9999.99" +#define MAX_SIZE std::string_view{"9999.99"} namespace Slic3r { @@ -81,7 +85,7 @@ void GizmoObjectManipulation::update_settings_value(const Selection& selection) m_new_rotate_label_string = L("Rotation"); m_new_scale_label_string = L("Scale ratios"); - m_world_coordinates = true; + m_coordinates_type = ECoordinatesType::World; ObjectList* obj_list = wxGetApp().obj_list(); if (selection.is_single_full_instance()) { @@ -89,7 +93,7 @@ void GizmoObjectManipulation::update_settings_value(const Selection& selection) const GLVolume* volume = selection.get_first_volume(); m_new_position = volume->get_instance_offset(); - if (m_world_coordinates) { + if (is_world_coordinates()) { m_new_rotate_label_string = L("Rotate"); m_new_rotation = volume->get_instance_rotation() * (180. / M_PI); m_new_size = selection.get_scaled_instance_bounding_box().size(); @@ -260,7 +264,15 @@ void GizmoObjectManipulation::change_position_value(int axis, double value) Selection& selection = m_glcanvas.get_selection(); selection.setup_cache(); - selection.translate(position - m_cache.position, selection.requires_local_axes()); + TransformationType trafo_type; + trafo_type.set_relative(); + switch (m_coordinates_type) + { + case ECoordinatesType::Instance: { trafo_type.set_instance(); break; } + case ECoordinatesType::Local: { trafo_type.set_local(); break; } + default: { break; } + } + selection.translate(position - m_cache.position, trafo_type); m_glcanvas.do_move(L("Set Position")); m_cache.position = position; @@ -278,14 +290,16 @@ void GizmoObjectManipulation::change_rotation_value(int axis, double value) Selection& selection = m_glcanvas.get_selection(); - TransformationType transformation_type(TransformationType::World_Relative_Joint); - if (selection.is_single_full_instance() || selection.requires_local_axes()) - transformation_type.set_independent(); - if (selection.is_single_full_instance() && ! m_world_coordinates) { - //FIXME Selection::rotate() does not process absoulte rotations correctly: It does not recognize the axis index, which was changed. - // transformation_type.set_absolute(); - transformation_type.set_local(); - } + TransformationType transformation_type; + transformation_type.set_relative(); + if (selection.is_single_full_instance()) + transformation_type.set_independent(); + + if (is_local_coordinates()) + transformation_type.set_local(); + + if (is_instance_coordinates()) + transformation_type.set_instance(); selection.setup_cache(); selection.rotate( @@ -336,7 +350,7 @@ void GizmoObjectManipulation::change_size_value(int axis, double value) ref_size = Vec3d(instance_scale[0] * ref_size[0], instance_scale[1] * ref_size[1], instance_scale[2] * ref_size[2]); } else if (selection.is_single_full_instance()) - ref_size = m_world_coordinates ? + ref_size = is_world_coordinates() ? selection.get_unscaled_instance_bounding_box().size() : wxGetApp().model().objects[selection.get_first_volume()->object_idx()]->raw_mesh_bounding_box().size(); @@ -355,7 +369,7 @@ void GizmoObjectManipulation::do_scale(int axis, const Vec3d &scale) const TransformationType transformation_type(TransformationType::World_Relative_Joint); if (selection.is_single_full_instance()) { transformation_type.set_absolute(); - if (! m_world_coordinates) + if (! is_world_coordinates()) transformation_type.set_local(); } @@ -364,7 +378,7 @@ void GizmoObjectManipulation::do_scale(int axis, const Vec3d &scale) const scaling_factor = scale(axis) * Vec3d::Ones(); selection.setup_cache(); - selection.scale(scaling_factor * 0.01, transformation_type); + selection.scale_legacy(scaling_factor * 0.01, transformation_type); m_glcanvas.do_scale(L("Set Scale")); } @@ -386,6 +400,51 @@ void GizmoObjectManipulation::on_change(const std::string& opt_key, int axis, do change_size_value(axis, new_value); } +void GizmoObjectManipulation::set_uniform_scaling(const bool new_value) +{ + const Selection &selection = m_glcanvas.get_selection(); + if (selection.is_single_full_instance() && is_world_coordinates() && !new_value) { + // Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible. + // all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one + const GLVolume* volume = selection.get_first_volume(); + // Is the angle close to a multiple of 90 degrees? + + if (! Geometry::is_rotation_ninety_degrees(volume->get_instance_rotation())) { + // Cannot apply scaling in the world coordinate system. + // BBS: remove tilt prompt dialog + + // Bake the rotation into the meshes of the object. + wxGetApp().model().objects[volume->composite_id.object_id]->bake_xy_rotation_into_meshes(volume->composite_id.instance_id); + // Update the 3D scene, selections etc. + wxGetApp().plater()->update(); + // Recalculate cached values at this panel, refresh the screen. + this->UpdateAndShow(true); + } + } + m_uniform_scale = new_value; +} + +void GizmoObjectManipulation::set_coordinates_type(ECoordinatesType type) +{ + if (wxGetApp().get_mode() == comSimple) + type = ECoordinatesType::World; + + if (m_coordinates_type == type) + return; + + m_coordinates_type = type; + this->UpdateAndShow(true); + GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); + canvas->get_gizmos_manager().update_data(); + canvas->set_as_dirty(); + canvas->request_extra_frame(); +} + +ECoordinatesType GizmoObjectManipulation::get_coordinates_type() const +{ + return m_coordinates_type; +} + void GizmoObjectManipulation::reset_position_value() { Selection& selection = m_glcanvas.get_selection(); @@ -427,7 +486,7 @@ void GizmoObjectManipulation::reset_rotation_value() return; // Update rotation at the GLVolumes. - selection.synchronize_unselected_instances(Selection::SYNC_ROTATION_GENERAL); + selection.synchronize_unselected_instances(Selection::SyncRotationType::GENERAL); selection.synchronize_unselected_volumes(); // Copy rotation values from GLVolumes into Model (ModelInstance / ModelVolume), trigger background processing. m_glcanvas.do_rotate(L("Reset Rotation")); @@ -444,30 +503,6 @@ void GizmoObjectManipulation::reset_scale_value() change_scale_value(2, 100.); } -void GizmoObjectManipulation::set_uniform_scaling(const bool new_value) -{ - const Selection &selection = m_glcanvas.get_selection(); - if (selection.is_single_full_instance() && m_world_coordinates && !new_value) { - // Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible. - // all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one - const GLVolume* volume = selection.get_first_volume(); - // Is the angle close to a multiple of 90 degrees? - - if (! Geometry::is_rotation_ninety_degrees(volume->get_instance_rotation())) { - // Cannot apply scaling in the world coordinate system. - // BBS: remove tilt prompt dialog - - // Bake the rotation into the meshes of the object. - wxGetApp().model().objects[volume->composite_id.object_id]->bake_xy_rotation_into_meshes(volume->composite_id.instance_id); - // Update the 3D scene, selections etc. - wxGetApp().plater()->update(); - // Recalculate cached values at this panel, refresh the screen. - this->UpdateAndShow(true); - } - } - m_uniform_scale = new_value; -} - static const char* label_values[2][3] = { { "##position_x", "##position_y", "##position_z"}, { "##rotation_x", "##rotation_y", "##rotation_z"} diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp index 39ceee5db..3a3dc145c 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp @@ -6,6 +6,8 @@ #include "libslic3r/Point.hpp" #include +#include "slic3r/GUI/GUI_Geometry.hpp" + //#include "slic3r/GUI/GLCanvas3D.hpp" namespace Slic3r { @@ -81,8 +83,7 @@ public: Vec3d m_buffered_size; bool m_new_enabled {true}; bool m_uniform_scale {true}; - // Does the object manipulation panel work in World or Local coordinates? - bool m_world_coordinates = true; + ECoordinatesType m_coordinates_type{ ECoordinatesType::World }; bool m_show_clear_rotation { false }; bool m_show_clear_scale { false }; @@ -107,9 +108,12 @@ public: void set_uniform_scaling(const bool uniform_scale); bool get_uniform_scaling() const { return m_uniform_scale; } - // Does the object manipulation panel work in World or Local coordinates? - void set_world_coordinates(const bool world_coordinates) { m_world_coordinates = world_coordinates; this->UpdateAndShow(true); } - bool get_world_coordinates() const { return m_world_coordinates; } + + void set_coordinates_type(ECoordinatesType type); + ECoordinatesType get_coordinates_type() const; + bool is_world_coordinates() const { return m_coordinates_type == ECoordinatesType::World; } + bool is_instance_coordinates() const { return m_coordinates_type == ECoordinatesType::Instance; } + bool is_local_coordinates() const { return m_coordinates_type == ECoordinatesType::Local; } void reset_cache() { m_cache.reset(); } diff --git a/src/slic3r/GUI/IconManager.cpp b/src/slic3r/GUI/IconManager.cpp new file mode 100644 index 000000000..896c5203f --- /dev/null +++ b/src/slic3r/GUI/IconManager.cpp @@ -0,0 +1,410 @@ +#include "IconManager.hpp" +#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; + float offset_y = (line_height - s.y) / 2; + 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..24ec3f7a9 --- /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 835e718bd..c385662f9 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -520,9 +520,24 @@ void ImGuiWrapper::render() m_new_frame_open = false; } +ImVec2 ImGuiWrapper::calc_text_size(std::string_view text, + bool hide_text_after_double_hash, + float wrap_width) +{ + return ImGui::CalcTextSize(text.data(), text.data() + text.length(), + hide_text_after_double_hash, wrap_width); +} + +ImVec2 ImGuiWrapper::calc_text_size(const std::string& text, + bool hide_text_after_double_hash, + float wrap_width) +{ + return ImGui::CalcTextSize(text.c_str(), NULL, hide_text_after_double_hash, wrap_width); +} + ImVec2 ImGuiWrapper::calc_text_size(const wxString &text, bool hide_text_after_double_hash, - float wrap_width) const + float wrap_width) { auto text_utf8 = into_u8(text); ImVec2 size = ImGui::CalcTextSize(text_utf8.c_str(), NULL, hide_text_after_double_hash, wrap_width); @@ -585,8 +600,8 @@ bool ImGuiWrapper::bbl_combo_with_filter(const char* label, const std::string& p static char pattern_buffer[256] = { 0 }; auto simple_match = [](const char* pattern, const char* str) { - wxString sub_str = wxString(pattern).Lower(); - wxString main_str = wxString(str).Lower(); + wxString sub_str = wxString::FromUTF8(pattern).Lower(); + wxString main_str = wxString::FromUTF8(str).Lower(); return main_str.Find(sub_str); }; @@ -911,13 +926,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::text_colored(const ImVec4& color, const char* label) @@ -927,13 +942,13 @@ void ImGuiWrapper::text_colored(const ImVec4& color, const char* label) void ImGuiWrapper::text_colored(const ImVec4& color, const std::string& label) { - this->text_colored(color, label.c_str()); + ImGuiWrapper::text_colored(color, label.c_str()); } void ImGuiWrapper::text_colored(const ImVec4& color, const wxString& label) { auto label_utf8 = into_u8(label); - this->text_colored(color, label_utf8.c_str()); + ImGuiWrapper::text_colored(color, label_utf8.c_str()); } void ImGuiWrapper::text_wrapped(const char *label, float wrap_width) @@ -1940,6 +1955,364 @@ ColorRGBA ImGuiWrapper::from_ImVec4(const ImVec4& color) return { color.x, color.y, color.z, color.w }; } +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 + float average_letter_width = calc_text_size(std::string_view("n")).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); + text_width = calc_text_size(result_text).x; + if (text_width < allowed_width) { + // increase letter count + while (count_letter < text.length()) { + ++count_letter; + std::string_view 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 std::string(result_text) + 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); +} + +ImVec2 ImGuiWrapper::suggest_location(const ImVec2 &dialog_size, + const Slic3r::Polygon &interest, + const ImVec2 &canvas_size) +{ + // IMPROVE 1: do not select place over menu + // BoundingBox top_menu; + // GLGizmosManager &gizmo_mng = canvas->get_gizmos_manager(); + // BoundingBox side_menu; // gizmo_mng.get_size(); + // BoundingBox left_bottom_menu; // is permanent? + // NotificationManager *notify_mng = plater->get_notification_manager(); + // BoundingBox notifications; // notify_mng->get_size(); + // m_window_width, m_window_height + position + + // IMPROVE 2: use polygon of interest not only bounding box + BoundingBox bb(interest.points); + Point center = bb.center(); // interest.centroid(); + + // area size + Point window_center(canvas_size.x / 2, canvas_size.y / 2); + + // mov on side + Point bb_half_size = (bb.max - bb.min) / 2 + Point(1,1); + Point diff_center = window_center - center; + Vec2d diff_norm(diff_center.x() / (double) bb_half_size.x(), + diff_center.y() / (double) bb_half_size.y()); + if (diff_norm.x() > 1.) diff_norm.x() = 1.; + if (diff_norm.x() < -1.) diff_norm.x() = -1.; + if (diff_norm.y() > 1.) diff_norm.y() = 1.; + if (diff_norm.y() < -1.) diff_norm.y() = -1.; + + Vec2d abs_diff(abs(diff_norm.x()), abs(diff_norm.y())); + if (abs_diff.x() < 1. && abs_diff.y() < 1.) { + if (abs_diff.x() > abs_diff.y()) + diff_norm.x() = (diff_norm.x() < 0.) ? (-1.) : 1.; + else + diff_norm.y() = (diff_norm.y() < 0.) ? (-1.) : 1.; + } + + Point half_dialog_size(dialog_size.x / 2., dialog_size.y / 2.); + Point move_size = bb_half_size + half_dialog_size; + Point offseted_center = center - half_dialog_size; + Vec2d offset(offseted_center.x() + diff_norm.x() * move_size.x(), + offseted_center.y() + diff_norm.y() * move_size.y()); + + // move offset close to center + Points window_polygon = {offset.cast(), + Point(offset.x(), offset.y() + dialog_size.y), + Point(offset.x() + dialog_size.x, + offset.y() + dialog_size.y), + Point(offset.x() + dialog_size.x, offset.y())}; + // check that position by Bounding box is not intersecting + assert(Slic3r::intersection(interest, Polygon(window_polygon)).empty()); + + double allowed_space = 10; // in px + double allowed_space_sq = allowed_space * allowed_space; + Vec2d move_vec = (center - (offset.cast() + half_dialog_size)) + .cast(); + Vec2d result_move(0, 0); + do { + move_vec = move_vec / 2.; + Point move_point = (move_vec + result_move).cast(); + Points moved_polygon = window_polygon; // copy + for (Point &p : moved_polygon) p += move_point; + if (Slic3r::intersection(interest, Polygon(moved_polygon)).empty()) + result_move += move_vec; + + } while (move_vec.squaredNorm() >= allowed_space_sq); + offset += result_move; + + return ImVec2(offset.x(), offset.y()); +} + +void ImGuiWrapper::draw( + const Polygon &polygon, + ImDrawList * draw_list /* = ImGui::GetOverlayDrawList()*/, + ImU32 color /* = ImGui::GetColorU32(COL_ORANGE_LIGHT)*/, + float thickness /* = 3.f*/) +{ + // minimal one line consist of 2 points + if (polygon.size() < 2) return; + // need a place to draw + if (draw_list == nullptr) return; + + const Point *prev_point = &polygon.points.back(); + for (const Point &point : polygon.points) { + ImVec2 p1(prev_point->x(), prev_point->y()); + ImVec2 p2(point.x(), point.y()); + draw_list->AddLine(p1, p2, color, thickness); + prev_point = &point; + } +} + +void ImGuiWrapper::draw_cross_hair(const ImVec2 &position, float radius, ImU32 color, int num_segments, float thickness) { + auto draw_list = ImGui::GetOverlayDrawList(); + draw_list->AddCircle(position, radius, color, num_segments, thickness); + auto dirs = {ImVec2{0, 1}, ImVec2{1, 0}, ImVec2{0, -1}, ImVec2{-1, 0}}; + for (const ImVec2 &dir : dirs) { + ImVec2 start(position.x + dir.x * 0.5 * radius, position.y + dir.y * 0.5 * radius); + ImVec2 end(position.x + dir.x * 1.5 * radius, position.y + dir.y * 1.5 * radius); + draw_list->AddLine(start, end, color, thickness); + } +} + +bool ImGuiWrapper::contain_all_glyphs(const ImFont *font, + const std::string &text) +{ + if (font == nullptr) return false; + if (!font->IsLoaded()) return false; + const ImFontConfig *fc = font->ConfigData; + if (fc == nullptr) return false; + if (text.empty()) return true; + return is_chars_in_ranges(fc->GlyphRanges, text.c_str()); +} + +bool ImGuiWrapper::is_char_in_ranges(const ImWchar *ranges, + unsigned int letter) +{ + for (const ImWchar *range = ranges; range[0] && range[1]; range += 2) { + ImWchar from = range[0]; + ImWchar to = range[1]; + if (from <= letter && letter <= to) return true; + if (letter < to) return false; // ranges should be sorted + } + return false; +}; + +bool ImGuiWrapper::is_chars_in_ranges(const ImWchar *ranges, + const char *chars_ptr) +{ + while (*chars_ptr) { + unsigned int c = 0; + // UTF-8 to 32-bit character need imgui_internal + int c_len = ImTextCharFromUtf8(&c, chars_ptr, NULL); + chars_ptr += c_len; + if (c_len == 0) break; + if (!is_char_in_ranges(ranges, c)) return false; + } + return true; +} + + #ifdef __APPLE__ static const ImWchar ranges_keyboard_shortcuts[] = { @@ -2204,20 +2577,20 @@ void ImGuiWrapper::push_combo_style(const float scale) ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 1.0f * scale); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f * scale); ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BG_DARK); - ImGui::PushStyleColor(ImGuiCol_BorderActive, ImVec4(0.00f, 0.68f, 0.26f, 1.00f)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.00f, 0.68f, 0.26f, 0.0f)); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.00f, 0.68f, 0.26f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.00f, 0.68f, 0.26f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_BorderActive, COL_ORCA); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, to_ImVec4(to_rgba(ColorRGB::ORCA(), 0.5f))); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, COL_ORCA); + ImGui::PushStyleColor(ImGuiCol_Header, COL_ORCA); ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImGuiWrapper::COL_WINDOW_BG_DARK); ImGui::PushStyleColor(ImGuiCol_Button, {1.00f, 1.00f, 1.00f, 0.0f}); } else { ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 1.0f * scale); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f * scale); ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BG); - ImGui::PushStyleColor(ImGuiCol_BorderActive, ImVec4(0.00f, 0.68f, 0.26f, 1.00f)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.00f, 0.68f, 0.26f, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.00f, 0.68f, 0.26f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.00f, 0.68f, 0.26f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_BorderActive, COL_ORCA); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, to_ImVec4(to_rgba(ColorRGB::ORCA(), 0.5f))); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, COL_ORCA); + ImGui::PushStyleColor(ImGuiCol_Header, COL_ORCA); ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImGuiWrapper::COL_WINDOW_BG); ImGui::PushStyleColor(ImGuiCol_Button, {1.00f, 1.00f, 1.00f, 0.0f}); } @@ -2229,6 +2602,20 @@ void ImGuiWrapper::pop_combo_style() ImGui::PopStyleColor(7); } +void ImGuiWrapper::push_radio_style() +{ + if (m_is_dark_mode) { + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.00f, 1.00f, 1.00f, 1.00f)); + } else { + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.00f, 0.00f, 0.00f, 1.00f)); + } +} + +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 f3e8e3467..07ca50990 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -13,10 +13,11 @@ #include #include "libslic3r/Point.hpp" +#include "libslic3r/Color.hpp" +#include "libslic3r/Polygon.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" namespace Slic3r { -class ColorRGBA; namespace Search { struct OptionViewParameters; } // namespace Search @@ -95,13 +96,19 @@ public: float get_font_size() const { return m_font_size; } float get_style_scaling() const { return m_style_scaling; } + const ImWchar *get_glyph_ranges() const { return m_glyph_ranges; } // language specific void new_frame(); void render(); 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, bool hide_text_after_double_hash = false, float wrap_width = -1.0f) const; + /// + /// Extend ImGui::CalcTextSize to use string_view + /// + static ImVec2 calc_text_size(std::string_view text, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + static ImVec2 calc_text_size(const std::string& text, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + static ImVec2 calc_text_size(const wxString &text, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); ImVec2 calc_button_size(const wxString &text, const ImVec2 &button_size = ImVec2(0, 0)) const; ImVec2 get_item_spacing() const; @@ -137,12 +144,12 @@ 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); - void text_colored(const ImVec4& color, const char* label); - void text_colored(const ImVec4& color, const std::string& label); - void text_colored(const ImVec4& color, const wxString& label); + static void text(const char *label); + static void text(const std::string &label); + static void text(const wxString &label); + static void text_colored(const ImVec4& color, const char* label); + static void text_colored(const ImVec4& color, const std::string& label); + static void text_colored(const ImVec4& color, const wxString& label); void text_wrapped(const char *label, float wrap_width); void text_wrapped(const std::string &label, float wrap_width); void text_wrapped(const wxString &label, float wrap_width); @@ -196,11 +203,109 @@ public: bool want_text_input() const; bool want_any_input() const; -#if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT + // 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); + + /// + /// Suggest loacation of dialog window, + /// dependent on actual visible thing on platter + /// like Gizmo menu size, notifications, ... + /// To be near of polygon interest and not over it. + /// And also not out of visible area. + /// + /// Define width and height of diaog window + /// Area of interest. Result should be close to it + /// Available space a.k.a GLCanvas3D::get_current_canvas3D() + /// Suggestion for dialog offest + static ImVec2 suggest_location(const ImVec2 &dialog_size, + const Slic3r::Polygon &interest, + const ImVec2 &canvas_size); + + /// + /// Visualization of polygon + /// + /// Define what to draw + /// Define where to draw it + /// Color of polygon + /// Width of polygon line + static void draw(const Polygon &polygon, + ImDrawList * draw_list = ImGui::GetOverlayDrawList(), + ImU32 color = ImGui::GetColorU32(COL_ORANGE_LIGHT), + float thickness = 3.f); + + /// + /// Draw symbol of cross hair + /// + /// Center of cross hair + /// Circle radius + /// Color of symbol + /// Precission of circle + /// Thickness of Line in symbol + static void draw_cross_hair(const ImVec2 &position, + float radius = 16.f, + ImU32 color = ImGui::GetColorU32(ImVec4(1.f, 1.f, 1.f, .75f)), + int num_segments = 0, + float thickness = 4.f); + + /// + /// Check that font ranges contain all chars in string + /// (rendered Unicodes are stored in GlyphRanges) + /// + /// Contain glyph ranges + /// Vector of character to check + /// True when all glyphs from text are in font ranges + static bool contain_all_glyphs(const ImFont *font, const std::string &text); + static bool is_chars_in_ranges(const ImWchar *ranges, const char *chars_ptr); + static bool is_char_in_ranges(const ImWchar *ranges, unsigned int letter); + bool requires_extra_frame() const { return m_requires_extra_frame; } void set_requires_extra_frame() { m_requires_extra_frame = true; } void reset_requires_extra_frame() { m_requires_extra_frame = false; } -#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT void disable_background_fadeout_animation(); @@ -248,6 +353,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/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index e4c45f429..5b73b6294 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 Tomáš Mészáros @tamasmeszaros, Enrico Turri @enricoturri1966, Vojtěch Bubník @bubnikv, David Kocík @kocikdav, Filip Sykala @Jony01, Lukáš Matěna @lukasmatena +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "ArrangeJob.hpp" #include "libslic3r/BuildVolume.hpp" @@ -509,21 +513,12 @@ void ArrangeJob::check_unprintable() } } -void ArrangeJob::on_exception(const std::exception_ptr &eptr) +void ArrangeJob::process(Ctl &ctl) { - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (libnest2d::GeometryException &) { - show_error(m_plater, _(L("Arrange failed. " - "Found some exceptions when processing object geometries."))); - } catch (std::exception &) { - PlaterJob::on_exception(eptr); - } -} + static const auto arrangestr = _u8L("Arranging"); + ctl.update_status(0, arrangestr); + ctl.call_on_main_thread([this]{ prepare(); }).wait();; -void ArrangeJob::process() -{ auto & partplate_list = m_plater->get_partplate_list(); const Slic3r::DynamicPrintConfig& global_config = wxGetApp().preset_bundle->full_config(); @@ -543,10 +538,10 @@ void ArrangeJob::process() BOOST_LOG_TRIVIAL(debug) << "arrange bedpts:" << bedpts[0].transpose() << ", " << bedpts[1].transpose() << ", " << bedpts[2].transpose() << ", " << bedpts[3].transpose(); - params.stopcondition = [this]() { return was_canceled(); }; + params.stopcondition = [&ctl]() { return ctl.was_canceled(); }; - params.progressind = [this](unsigned num_finished, std::string str = "") { - update_status(num_finished, _L("Arranging") + " "+ wxString::FromUTF8(str)); + params.progressind = [this, &ctl](unsigned num_finished, std::string str = "") { + ctl.update_status(num_finished * 100 / status_range(), _u8L("Arranging") + str); }; { @@ -590,11 +585,13 @@ void ArrangeJob::process() } // finalize just here. - update_status(status_range(), - was_canceled() ? _(L("Arranging canceled.")) : - we_have_unpackable_items ? _(L("Arranging is done but there are unpacked items. Reduce spacing and try again.")) : _(L("Arranging done."))); + ctl.update_status(100, + ctl.was_canceled() ? _u8L("Arranging canceled.") : + we_have_unpackable_items ? _u8L("Arranging is done but there are unpacked items. Reduce spacing and try again.") : _u8L("Arranging done.")); } +ArrangeJob::ArrangeJob() : m_plater{wxGetApp().plater()} { } + static std::string concat_strings(const std::set &strings, const std::string &delim = "\n") { @@ -605,9 +602,20 @@ static std::string concat_strings(const std::set &strings, }); } -void ArrangeJob::finalize() { - // Ignore the arrange result if aborted. - if (was_canceled()) return; +void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) { + try { + if (eptr) + std::rethrow_exception(eptr); + } catch (libnest2d::GeometryException &) { + show_error(m_plater, _(L("Arrange failed. " + "Found some exceptions when processing object geometries."))); + eptr = nullptr; + } catch (...) { + eptr = std::current_exception(); + } + + if (canceled || eptr) + return; // Unprintable items go to the last virtual bed int beds = 0; @@ -716,7 +724,6 @@ void ArrangeJob::finalize() { m_plater->update(); - Job::finalize(); m_plater->m_arrange_running.store(false); } diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index 717fd38b2..6b3364236 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -1,10 +1,15 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef ARRANGEJOB_HPP #define ARRANGEJOB_HPP -#include "PlaterJob.hpp" -#include "slic3r/GUI/Plater.hpp" + +#include + +#include "Job.hpp" #include "libslic3r/Arrange.hpp" -#include "libslic3r/Model.hpp" namespace Slic3r { @@ -12,7 +17,9 @@ class ModelInstance; namespace GUI { -class ArrangeJob : public PlaterJob +class Plater; + +class ArrangeJob : public Job { using ArrangePolygon = arrangement::ArrangePolygon; using ArrangePolygons = arrangement::ArrangePolygons; @@ -24,6 +31,10 @@ class ArrangeJob : public PlaterJob arrangement::ArrangeParams params; int current_plate_index = 0; Polygon bed_poly; + Plater *m_plater; + + // BBS: add flag for whether on current part plate + bool only_on_partplate{false}; // clear m_selected and m_unselected, reserve space for next usage void clear_input(); @@ -42,26 +53,23 @@ class ArrangeJob : public PlaterJob protected: - void prepare() override; - void check_unprintable(); - void on_exception(const std::exception_ptr &) override; - - void process() override; - public: - ArrangeJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} - {} - int status_range() const override + void prepare(); + + void process(Ctl &ctl) override; + + ArrangeJob(); + + int status_range() const { // ensure finalize() is called after all operations in process() is finished. return int(m_selected.size() + m_unprintable.size() + 1); } - void finalize() override; + void finalize(bool canceled, std::exception_ptr &e) override; }; std::optional get_wipe_tower_arrangepoly(const Plater &); diff --git a/src/slic3r/GUI/Jobs/BindJob.cpp b/src/slic3r/GUI/Jobs/BindJob.cpp index 9534128af..26e20d867 100644 --- a/src/slic3r/GUI/Jobs/BindJob.cpp +++ b/src/slic3r/GUI/Jobs/BindJob.cpp @@ -12,12 +12,12 @@ wxDEFINE_EVENT(EVT_BIND_MACHINE_SUCCESS, wxCommandEvent); wxDEFINE_EVENT(EVT_BIND_MACHINE_FAIL, wxCommandEvent); -static wxString waiting_auth_str = _L("Logging in"); -static wxString login_failed_str = _L("Login failed"); +static auto waiting_auth_str = _u8L("Logging in"); +static auto login_failed_str = _u8L("Login failed"); -BindJob::BindJob(std::shared_ptr pri, Plater *plater, std::string dev_id, std::string dev_ip, std::string sec_link) - : PlaterJob{std::move(pri), plater}, +BindJob::BindJob(std::string dev_id, std::string dev_ip, std::string sec_link) + : m_dev_id(dev_id), m_dev_ip(dev_ip), m_sec_link(sec_link) @@ -25,37 +25,27 @@ BindJob::BindJob(std::shared_ptr pri, Plater *plater, std::st ; } -void BindJob::on_exception(const std::exception_ptr &eptr) -{ - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (std::exception &e) { - PlaterJob::on_exception(eptr); - } -} - void BindJob::on_success(std::function success) { m_success_fun = success; } -void BindJob::update_status(int st, const wxString &msg) +void BindJob::update_status(Ctl &ctl, int st, const std::string &msg) { - GUI::Job::update_status(st, msg); + ctl.update_status(st, msg); wxCommandEvent event(EVT_BIND_UPDATE_MESSAGE); event.SetString(msg); event.SetEventObject(m_event_handle); wxPostEvent(m_event_handle, event); } -void BindJob::process() +void BindJob::process(Ctl &ctl) { int result_code = 0; std::string result_info; /* display info */ - wxString msg = waiting_auth_str; + auto msg = waiting_auth_str; int curr_percent = 0; NetworkAgent* m_agent = wxGetApp().getAgent(); @@ -67,40 +57,40 @@ void BindJob::process() std::string timezone = get_timezone_utc_hm(offset); int result = m_agent->bind(m_dev_ip, m_dev_id, m_sec_link, timezone, m_improved, - [this, &curr_percent, &msg, &result_code, &result_info](int stage, int code, std::string info) { + [this, &ctl, &curr_percent, &msg, &result_code, &result_info](int stage, int code, std::string info) { result_code = code; result_info = info; if (stage == BBL::BindJobStage::LoginStageConnect) { curr_percent = 15; - msg = _L("Logging in"); + msg = _u8L("Logging in"); } else if (stage == BBL::BindJobStage::LoginStageLogin) { curr_percent = 30; - msg = _L("Logging in"); + msg = _u8L("Logging in"); } else if (stage == BBL::BindJobStage::LoginStageWaitForLogin) { curr_percent = 45; - msg = _L("Logging in"); + msg = _u8L("Logging in"); } else if (stage == BBL::BindJobStage::LoginStageGetIdentify) { curr_percent = 60; - msg = _L("Logging in"); + msg = _u8L("Logging in"); } else if (stage == BBL::BindJobStage::LoginStageWaitAuth) { curr_percent = 80; - msg = _L("Logging in"); + msg = _u8L("Logging in"); } else if (stage == BBL::BindJobStage::LoginStageFinished) { curr_percent = 100; - msg = _L("Logging in"); + msg = _u8L("Logging in"); } else { - msg = _L("Logging in"); + msg = _u8L("Logging in"); } if (code != 0) { - msg = _L("Login failed"); + msg = _u8L("Login failed"); if (code == BAMBU_NETWORK_ERR_TIMEOUT) { - msg += _L("Please check the printer network connection."); + msg += _u8L("Please check the printer network connection."); } } - update_status(curr_percent, msg); + update_status(ctl, curr_percent, msg); } ); @@ -138,11 +128,18 @@ void BindJob::process() return; } -void BindJob::finalize() +void BindJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (was_canceled()) return; + try { + if (eptr) + std::rethrow_exception(eptr); + eptr = nullptr; + } catch (...) { + eptr = std::current_exception(); + } - Job::finalize(); + if (canceled || eptr) + return; } void BindJob::set_event_handle(wxWindow *hanle) diff --git a/src/slic3r/GUI/Jobs/BindJob.hpp b/src/slic3r/GUI/Jobs/BindJob.hpp index 0215834fa..b0bc4d818 100644 --- a/src/slic3r/GUI/Jobs/BindJob.hpp +++ b/src/slic3r/GUI/Jobs/BindJob.hpp @@ -3,14 +3,14 @@ #include #include -#include "PlaterJob.hpp" +#include "Job.hpp" namespace fs = boost::filesystem; namespace Slic3r { namespace GUI { -class BindJob : public PlaterJob +class BindJob : public Job { wxWindow * m_event_handle{nullptr}; std::function m_success_fun{nullptr}; @@ -21,12 +21,10 @@ class BindJob : public PlaterJob int m_print_job_completed_id = 0; bool m_improved{false}; -protected: - void on_exception(const std::exception_ptr &) override; public: - BindJob(std::shared_ptr pri, Plater *plater, std::string dev_id, std::string dev_ip, std::string sec_link); + BindJob(std::string dev_id, std::string dev_ip, std::string sec_link); - int status_range() const override + int status_range() const { return 100; } @@ -34,9 +32,9 @@ public: bool is_finished() { return m_job_finished; } void on_success(std::function success); - void update_status(int st, const wxString &msg); - void process() override; - void finalize() override; + void update_status(Ctl &ctl, int st, const std::string &msg); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; void set_event_handle(wxWindow* hanle); void post_fail_event(int code, std::string info); void set_improved(bool improved){m_improved = improved;}; diff --git a/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp new file mode 100644 index 000000000..f0ebf7baf --- /dev/null +++ b/src/slic3r/GUI/Jobs/BoostThreadWorker.cpp @@ -0,0 +1,186 @@ +///|/ Copyright (c) Prusa Research 2021 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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..a4176dd09 --- /dev/null +++ b/src/slic3r/GUI/Jobs/BoostThreadWorker.hpp @@ -0,0 +1,159 @@ +///|/ Copyright (c) Prusa Research 2021 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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 Job::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..e2067fbe6 --- /dev/null +++ b/src/slic3r/GUI/Jobs/BusyCursorJob.hpp @@ -0,0 +1,57 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef BUSYCURSORJOB_HPP +#define BUSYCURSORJOB_HPP + +#include "Job.hpp" + +#include +#include + +namespace Slic3r { namespace GUI { + +struct CursorSetterRAII +{ + Job::Ctl &ctl; + CursorSetterRAII(Job::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 Job { + 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/CreateFontNameImageJob.cpp b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp new file mode 100644 index 000000000..3c080148f --- /dev/null +++ b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp @@ -0,0 +1,170 @@ +///|/ Copyright (c) Prusa Research 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "CreateFontNameImageJob.hpp" + +#include "libslic3r/Emboss.hpp" +// rasterization of ExPoly +#include "libslic3r/SLA/AGGRaster.hpp" + +#include "slic3r/Utils/WxFontUtils.hpp" +#include "slic3r/GUI/3DScene.hpp" // ::glsafe + +// ability to request new frame after finish rendering +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" + +#include "wx/fontenum.h" + +#include + +using namespace Slic3r; +using namespace Slic3r::GUI; + +const std::string CreateFontImageJob::default_text = "AaBbCc 123"; + +CreateFontImageJob::CreateFontImageJob(FontImageData &&input) + : m_input(std::move(input)) +{ + assert(wxFontEnumerator::IsValidFacename(m_input.font_name)); + assert(m_input.gray_level > 0 && m_input.gray_level < 255); + assert(m_input.texture_id != 0); +} + +void CreateFontImageJob::process(Ctl &ctl) +{ + if (!wxFontEnumerator::IsValidFacename(m_input.font_name)) return; + // Select font + wxFont wx_font( + wxFontInfo().FaceName(m_input.font_name).Encoding(m_input.encoding)); + if (!wx_font.IsOk()) return; + + std::unique_ptr font_file = + WxFontUtils::create_font_file(wx_font); + if (font_file == nullptr) return; + + Emboss::FontFileWithCache font_file_with_cache(std::move(font_file)); + // use only first line of text + std::string& text = m_input.text; + if (text.empty()) + text = default_text; // copy + + size_t enter_pos = text.find('\n'); + if (enter_pos < text.size()) { + // text start with enter + if (enter_pos == 0) return; + // exist enter, soo delete all after enter + text = text.substr(0, enter_pos); + } + + std::function was_canceled = [&ctl, cancel = m_input.cancel]() -> bool { + if (ctl.was_canceled()) return true; + if (cancel->load()) return true; + return false; + }; + + FontProp fp; // create default font parameters + ExPolygons shapes = Emboss::text2shapes(font_file_with_cache, text.c_str(), fp, was_canceled); + + // select some character from font e.g. default text + if (shapes.empty()) + shapes = Emboss::text2shapes(font_file_with_cache, default_text.c_str(), fp, was_canceled); + if (shapes.empty()) { + m_input.cancel->store(true); + return; + } + + // normalize height of font + BoundingBox bounding_box; + for (const ExPolygon &shape : shapes) + bounding_box.merge(BoundingBox(shape.contour.points)); + if (bounding_box.size().x() < 1 || bounding_box.size().y() < 1) { + m_input.cancel->store(true); + return; + } + double scale = m_input.size.y() / (double) bounding_box.size().y(); + BoundingBoxf bb2(bounding_box.min.cast(), + bounding_box.max.cast()); + bb2.scale(scale); + Vec2d size_f = bb2.size(); + m_tex_size = Point(std::ceil(size_f.x()), std::ceil(size_f.y())); + // crop image width + if (m_tex_size.x() > m_input.size.x()) m_tex_size.x() = m_input.size.x(); + if (m_tex_size.y() > m_input.size.y()) m_tex_size.y() = m_input.size.y(); + + // Set up result + unsigned bit_count = 4; // RGBA + m_result = std::vector(m_tex_size.x() * m_tex_size.y() * bit_count, {255}); + + sla::Resolution resolution(m_tex_size.x(), m_tex_size.y()); + double pixel_dim = SCALING_FACTOR / scale; + sla::PixelDim dim(pixel_dim, pixel_dim); + double gamma = 1.; + std::unique_ptr r = + sla::create_raster_grayscale_aa(resolution, dim, gamma); + for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min); + for (const ExPolygon &shape : shapes) r->draw(shape); + + // copy rastered data to pixels + sla::RasterEncoder encoder = + [&pix = m_result, w = m_tex_size.x(), h = m_tex_size.y(), + gray_level = m_input.gray_level] + (const void *ptr, size_t width, size_t height, size_t num_components) { + size_t size {static_cast(w*h)}; + const unsigned char *ptr2 = (const unsigned char *) ptr; + for (size_t x = 0; x < width; ++x) + for (size_t y = 0; y < height; ++y) { + size_t index = y*w + x; + assert(index < size); + if (index >= size) continue; + pix[3+4*index] = ptr2[y * width + x] / gray_level; + } + return sla::EncodedRaster(); + }; + r->encode(encoder); +} + +void CreateFontImageJob::finalize(bool canceled, std::exception_ptr &) +{ + if (m_input.count_opened_font_files) + --(*m_input.count_opened_font_files); + if (canceled || m_input.cancel->load()) return; + + *m_input.is_created = true; + + // Exist result bitmap with preview? + // (not valid input. e.g. not loadable font) + if (m_result.empty()) { + // TODO: write text cannot load into texture + m_result = std::vector(m_tex_size.x() * m_tex_size.y() * 4, {255}); + } + + // upload texture on GPU + const GLenum target = GL_TEXTURE_2D; + glsafe(::glBindTexture(target, m_input.texture_id)); + + GLsizei w = m_tex_size.x(), h = m_tex_size.y(); + GLint xoffset = m_input.size.x() - m_tex_size.x(), // arrange right + yoffset = m_input.size.y() * m_input.index; + glsafe(::glTexSubImage2D(target, m_input.level, xoffset, yoffset, w, h, + m_input.format, m_input.type, m_result.data())); + + // clear rest of texture + std::vector empty_data(xoffset * h * 4, {0}); + glsafe(::glTexSubImage2D(target, m_input.level, 0, yoffset, xoffset, h, + m_input.format, m_input.type, empty_data.data())); + + // bind default texture + GLuint no_texture_id = 0; + glsafe(::glBindTexture(target, no_texture_id)); + + // show rendered texture + wxGetApp().plater()->canvas3D()->schedule_extra_frame(0); + + BOOST_LOG_TRIVIAL(info) + << "Generate Preview font('" << m_input.font_name << "' id:" << m_input.index << ") " + << "with text: '" << m_input.text << "' " + << "texture_size " << m_input.size.x() << " x " << m_input.size.y(); +} \ No newline at end of file diff --git a/src/slic3r/GUI/Jobs/CreateFontNameImageJob.hpp b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.hpp new file mode 100644 index 000000000..09d3ec6e3 --- /dev/null +++ b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.hpp @@ -0,0 +1,85 @@ +///|/ Copyright (c) Prusa Research 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_CreateFontNameImageJob_hpp_ +#define slic3r_CreateFontNameImageJob_hpp_ + +#include +#include +#include +#include +#include +#include "Job.hpp" +#include "libslic3r/Point.hpp" // Vec2i + +namespace Slic3r::GUI { + +/// +/// Keep data for rasterization of text by font face +/// +struct FontImageData +{ + // Text to rasterize + std::string text; + // Define font face + wxString font_name; + wxFontEncoding encoding; + // texture for copy result to + // texture MUST BE initialized + GLuint texture_id; + // Index of face name, define place in texture + size_t index; + // Height of each text + // And Limit for width + Vec2i size; // in px + + // bigger value create darker image + // divide value 255 + unsigned char gray_level = 5; + + // texture meta data + GLenum format = GL_ALPHA, type = GL_UNSIGNED_BYTE; + GLint level = 0; + + // prevent opening too much files + // it is decreased in finalize phase + unsigned int *count_opened_font_files = nullptr; + + std::shared_ptr> cancel = nullptr; + std::shared_ptr is_created = nullptr; +}; + +/// +/// Create image for face name +/// +class CreateFontImageJob : public Job +{ + FontImageData m_input; + std::vector m_result; + Point m_tex_size; +public: + CreateFontImageJob(FontImageData &&input); + /// + /// Rasterize text into image (result) + /// + /// Check for cancelation + void process(Ctl &ctl) override; + + /// + /// Copy image data into OpenGL texture + /// + /// + /// + void finalize(bool canceled, std::exception_ptr &) override; + + /// + /// Text used for generate preview for empty text + /// and when no glyph for given m_input.text + /// + static const std::string default_text; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_CreateFontNameImageJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp new file mode 100644 index 000000000..e429aca43 --- /dev/null +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp @@ -0,0 +1,157 @@ +///|/ Copyright (c) Prusa Research 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "CreateFontStyleImagesJob.hpp" + +// rasterization of ExPoly +#include "libslic3r/SLA/AGGRaster.hpp" +#include "slic3r/GUI/3DScene.hpp" // ::glsafe + +// ability to request new frame after finish rendering +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + + +CreateFontStyleImagesJob::CreateFontStyleImagesJob(StyleManager::StyleImagesData &&input) + : m_input(std::move(input)), m_width(0), m_height(0) +{ + assert(m_input.result != nullptr); + assert(!m_input.styles.empty()); + assert(!m_input.text.empty()); + assert(m_input.max_size.x() > 1); + assert(m_input.max_size.y() > 1); + assert(m_input.ppm > 1e-5); +} + +void CreateFontStyleImagesJob::process(Ctl &ctl) +{ + // create shapes and calc size (bounding boxes) + std::vector name_shapes(m_input.styles.size()); + std::vector scales(m_input.styles.size()); + m_images = std::vector(m_input.styles.size()); + + auto was_canceled = []() { return false; }; + for (auto &item : m_input.styles) { + size_t index = &item - &m_input.styles.front(); + ExPolygons &shapes = name_shapes[index]; + shapes = text2shapes(item.font, m_input.text.c_str(), item.prop, was_canceled); + + // create image description + StyleManager::StyleImage &image = m_images[index]; + BoundingBox &bounding_box = image.bounding_box; + for (ExPolygon &shape : shapes) + bounding_box.merge(BoundingBox(shape.contour.points)); + for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min); + + // calculate conversion from FontPoint to screen pixels by size of font + double scale = get_text_shape_scale(item.prop, *item.font.font_file); + scales[index] = scale; + + //double scale = font_prop.size_in_mm * SCALING_FACTOR; + BoundingBoxf bb2(bounding_box.min.cast(), + bounding_box.max.cast()); + bb2.scale(scale); + image.tex_size.x = std::ceil(bb2.max.x() - bb2.min.x()); + image.tex_size.y = std::ceil(bb2.max.y() - bb2.min.y()); + + // crop image width + if (image.tex_size.x > m_input.max_size.x()) + image.tex_size.x = m_input.max_size.x(); + // crop image height + if (image.tex_size.y > m_input.max_size.y()) + image.tex_size.y = m_input.max_size.y(); + } + + // arrange bounding boxes + int offset_y = 0; + m_width = 0; + for (StyleManager::StyleImage &image : m_images) { + image.offset.y() = offset_y; + offset_y += image.tex_size.y+1; + if (m_width < image.tex_size.x) + m_width = image.tex_size.x; + } + m_height = offset_y; + for (StyleManager::StyleImage &image : m_images) { + const Point &o = image.offset; + const ImVec2 &s = image.tex_size; + image.uv0 = ImVec2(o.x() / (double) m_width, + o.y() / (double) m_height); + image.uv1 = ImVec2((o.x() + s.x) / (double) m_width, + (o.y() + s.y) / (double) m_height); + } + + // Set up result + m_pixels = std::vector(4 * m_width * m_height, {255}); + + // upload sub textures + for (StyleManager::StyleImage &image : m_images) { + sla::Resolution resolution(image.tex_size.x, image.tex_size.y); + size_t index = &image - &m_images.front(); + double pixel_dim = SCALING_FACTOR / scales[index]; + sla::PixelDim dim(pixel_dim, pixel_dim); + double gamma = 1.; + std::unique_ptr r = + sla::create_raster_grayscale_aa(resolution, dim, gamma); + for (const ExPolygon &shape : name_shapes[index]) r->draw(shape); + + // copy rastered data to pixels + sla::RasterEncoder encoder = [&offset = image.offset, &pix = m_pixels, w=m_width,h=m_height] + (const void *ptr, size_t width, size_t height, size_t num_components) { + // bigger value create darker image + unsigned char gray_level = 1; + size_t size {static_cast(w*h)}; + assert((offset.x() + width) <= (size_t)w); + assert((offset.y() + height) <= (size_t)h); + const unsigned char *ptr2 = (const unsigned char *) ptr; + for (size_t x = 0; x < width; ++x) + for (size_t y = 0; y < height; ++y) { + size_t index = (offset.y() + y)*w + offset.x() + x; + assert(index < size); + if (index >= size) continue; + pix[4*index+3] = ptr2[y * width + x] / gray_level; + } + return sla::EncodedRaster(); + }; + r->encode(encoder); + } +} + +void CreateFontStyleImagesJob::finalize(bool canceled, std::exception_ptr &) +{ + if (canceled) return; + // upload texture on GPU + GLuint tex_id; + GLenum target = GL_TEXTURE_2D, format = GL_RGBA, type = GL_UNSIGNED_BYTE; + GLint level = 0, border = 0; + glsafe(::glGenTextures(1, &tex_id)); + glsafe(::glBindTexture(target, tex_id)); + glsafe(::glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); + glsafe(::glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); + GLint w = m_width, h = m_height; + glsafe(::glTexImage2D(target, level, GL_RGBA, w, h, border, format, type, + (const void *) m_pixels.data())); + + // set up texture id + void *texture_id = (void *) (intptr_t) tex_id; + for (StyleManager::StyleImage &image : m_images) + image.texture_id = texture_id; + + // move to result + m_input.result->styles = std::move(m_input.styles); + m_input.result->images = std::move(m_images); + + // bind default texture + GLuint no_texture_id = 0; + glsafe(::glBindTexture(target, no_texture_id)); + + // show rendered texture + wxGetApp().plater()->canvas3D()->schedule_extra_frame(0); +} \ No newline at end of file diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp new file mode 100644 index 000000000..af7a2f827 --- /dev/null +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp @@ -0,0 +1,40 @@ +///|/ Copyright (c) Prusa Research 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_CreateFontStyleImagesJob_hpp_ +#define slic3r_CreateFontStyleImagesJob_hpp_ + +#include +#include +#include +#include "slic3r/Utils/EmbossStyleManager.hpp" +#include "Job.hpp" + +namespace Slic3r::GUI::Emboss { + +/// +/// Create texture with name of styles written by its style +/// NOTE: Access to glyph cache is possible only from job +/// +class CreateFontStyleImagesJob : public Job +{ + StyleManager::StyleImagesData m_input; + + // Output data + // texture size + int m_width, m_height; + // texture data + std::vector m_pixels; + // descriptors of sub textures + std::vector m_images; + +public: + CreateFontStyleImagesJob(StyleManager::StyleImagesData &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &) override; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_CreateFontStyleImagesJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp new file mode 100644 index 000000000..471f202bb --- /dev/null +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -0,0 +1,1568 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Oleksandra Iushchenko @YuSanka, Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "EmbossJob.hpp" + +#include +#include +#include + +#include +#include // load_obj for default mesh +#include // use surface cuts +#include // create object +#include + +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/GUI/GLCanvas3D.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" +#include "slic3r/Utils/RaycastManager.hpp" + +// #define EXECUTE_UPDATE_ON_MAIN_THREAD // debug execution on main thread + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + +// Private implementation for create volume and objects jobs +namespace { +/// +/// 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 + GLGizmosManager::EType gizmo; +}; + +// Offset of clossed side to model +constexpr float SAFE_SURFACE_OFFSET = 0.015f; // [in mm] + +/// +/// Create new TextVolume on the surface of ModelObject +/// Should not be stopped +/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! +/// +class CreateVolumeJob : public Job +{ + 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; +}; + +/// +/// Hold neccessary data to create ModelObject in job +/// Object is placed on bed under screen coor +/// OR to center of scene when it is out of bed shape +/// +struct DataCreateObject +{ + // Hold data about shape + DataBasePtr base; + + // define position on screen where to create object + Vec2d screen_coor; + + // projection property + Camera camera; + + // shape of bed in case of create volume on bed + std::vector bed_shape; + + // Define which gizmo open on the success + GLGizmosManager::EType gizmo; +}; + +/// +/// Create new TextObject on the platter +/// Should not be stopped +/// +class CreateObjectJob : public Job +{ + 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; +}; + +/// +/// 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 + GLGizmosManager::EType gizmo; +}; + +/// +/// Cut surface from object and create cutted volume +/// Should not be stopped +/// +class CreateSurfaceVolumeJob : public Job +{ + 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; +}; + +/// +/// Assert check of inputs data +/// +bool check(const DataBase &input, bool check_fontfile = true, bool use_surface = false); +bool check(GLGizmosManager::EType gizmo); +bool check(const CreateVolumeParams& input); +bool check(const DataCreateVolume &input, bool is_main_thread = false); +bool check(const DataCreateObject &input); +bool check(const DataUpdate &input, bool is_main_thread = false, bool use_surface = false); +bool check(const CreateSurfaceVolumeData &input, bool is_main_thread = false); +bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread = false); + +template static ExPolygons create_shape(DataBase &input, Fnc was_canceled); + +// create sure that emboss object is bigger than source object [in mm] +constexpr float safe_extension = 1.0f; + +// +/// Try to create mesh from text +/// +/// Text to convert on mesh +/// + Shape of characters + Property of font +/// Font file with cache +/// NOTE: Cache glyphs is changed +/// To check if process was canceled +/// Triangle mesh model +template TriangleMesh try_create_mesh(DataBase &input, const Fnc& was_canceled); +template TriangleMesh create_mesh(DataBase &input, const Fnc& was_canceled, Job::Ctl &ctl); + +/// +/// Create default mesh for embossed text +/// +/// Not empty model(index trinagle set - its) +TriangleMesh create_default_mesh(); + +/// +/// Must be called on main thread +/// +/// New mesh data +/// Text configuration, ... +/// Transformation of volume +void update_volume(TriangleMesh &&mesh, const DataUpdate &data, const Transform3d *tr = nullptr); + +/// +/// Update name in right panel +/// +/// Right panel data +/// Volume with just changed name +void update_name_in_list(const ObjectList &object_list, const ModelVolume &volume); + +/// +/// Add new volume to object +/// +/// triangles of new volume +/// Object where to add volume +/// Type of new volume +/// Transformation of volume inside of object +/// Text configuration and New VolumeName +/// Gizmo to open +void create_volume(TriangleMesh &&mesh, const ObjectID& object_id, const ModelVolumeType type, + const std::optional& trmat, const DataBase &data, GLGizmosManager::EType gizmo); + +/// +/// Create projection for cut surface from mesh +/// +/// Volume transformation in object +/// Convert shape to milimeters +/// Bounding box 3d of model volume for projection ranges +/// Orthogonal cut_projection +OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range); + +/// +/// Create tranformation for emboss Cutted surface +/// +/// True .. raise, False .. engrave +/// Depth of embossing +/// Text voliume transformation inside object +/// Cutted surface from model +/// Projection +OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut); + +/// +/// Cut surface into triangle mesh +/// +/// (can't be const - cache of font) +/// SurfaceVolume data +/// Check to interupt execution +/// Extruded object from cuted surace +template +TriangleMesh cut_surface(/*const*/ DataBase &input1, const SurfaceVolumeData &input2, const Fnc& was_canceled); + +/// +/// 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 = {}); + +void create_message(const std::string &message); // only in finalize +bool process(std::exception_ptr &eptr); +bool finalize(bool canceled, std::exception_ptr &eptr, const DataBase &input); + +class JobException : public std::runtime_error { +public: using std::runtime_error::runtime_error;}; +auto was_canceled(const Job::Ctl &ctl, const DataBase &base){ + return [&ctl, &cancel = base.cancel]() { + if (cancel->load()) + return true; + return ctl.was_canceled(); + }; +} + +} // namespace + +void Slic3r::GUI::Emboss::DataBase::write(ModelVolume &volume) const{ + volume.name = volume_name; + volume.emboss_shape = shape; + volume.emboss_shape->fix_3mf_tr.reset(); +} + +///////////////// +/// Create Volume +CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input): m_input(std::move(input)){ assert(check(m_input, true)); } + +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, was_canceled(ctl, *m_input.base), ctl); +} +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); +} + + +///////////////// +/// Create Object +CreateObjectJob::CreateObjectJob(DataCreateObject &&input): m_input(std::move(input)){ assert(check(m_input)); } + +void CreateObjectJob::process(Ctl &ctl) +{ + if (!check(m_input)) + 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, was_canceled, ctl); + if (was_canceled()) return; + + // 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); +} + +void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + if (!::finalize(canceled, eptr, *m_input.base)) + return; + + // only for sure + if (m_result.empty()) + return create_message("Can't create empty object."); + + 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) + manager.open_gizmo(m_input.gizmo); + + // redraw scene + canvas->reload_scene(true); +} + +///////////////// +/// Update Volume +UpdateJob::UpdateJob(DataUpdate&& input): m_input(std::move(input)){ assert(check(m_input, true)); } + +void UpdateJob::process(Ctl &ctl) +{ + if (!check(m_input)) + throw JobException("Bad input data for EmbossUpdateJob."); + + auto was_canceled = ::was_canceled(ctl, *m_input.base); + m_result = ::try_create_mesh(*m_input.base, was_canceled); + if (was_canceled()) 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); +} + +///////////////// +/// Create Surface volume +CreateSurfaceVolumeJob::CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input) + : m_input(std::move(input)) +{ + assert(check(m_input, true)); +} + +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); +} + +///////////////// +/// Cut Surface +UpdateSurfaceVolumeJob::UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input) + : m_input(std::move(input)) +{ + assert(check(m_input, true)); +} + +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)); +} + +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); +} + +namespace { +/// +/// Check if volume type is possible use for new text volume +/// +/// Type +/// True when allowed otherwise false +bool is_valid(ModelVolumeType volume_type); + +/// +/// Start job for add new volume to object with given transformation +/// +/// Define where to queue the job. e.g. wxGetApp().plater()->get_ui_job_worker() +/// Define where to add +/// Wanted volume transformation, when not set will be calculated after creation to be near the object +/// Define what to emboss - shape +/// Type of volume: Part, negative, modifier +/// Define which gizmo open on the success +/// Nullptr when job is sucessfully add to worker otherwise return data to be processed different way +bool start_create_volume_job(Worker &worker, + const ModelObject &object, + const std::optional &volume_tr, + DataBasePtr data, + ModelVolumeType volume_type, + GLGizmosManager::EType gizmo); + +/// +/// Find volume in selected objects with closest convex hull to screen center. +/// +/// Define where to search for closest +/// Canvas center(dependent on camera settings) +/// Actual objects +/// OUT: coordinate of controid of closest volume +/// closest volume when exists otherwise nullptr +const GLVolume *find_closest( + const Selection &selection, const Vec2d &screen_center, const Camera &camera, const ModelObjectPtrs &objects, Vec2d *closest_center); + +/// +/// Start job for add object with text into scene +/// +/// Contain worker, build shape, gizmo +/// Define params for create volume +/// Screen coordinat, where to create new object laying on bed +/// True when can add job to worker otherwise FALSE +bool start_create_object_job(const CreateVolumeParams &input, DataBasePtr emboss_data, const Vec2d &coor); + +/// +/// Start job to create volume on the surface of object +/// +/// Variabless needed to create volume +/// Describe what to emboss - shape +/// Where to add +/// True .. try to create volume without screen_coor, +/// False .. +/// Nullptr when job is sucessfully add to worker otherwise return data to be processed different way +bool start_create_volume_on_surface_job(CreateVolumeParams &input, DataBasePtr data, const Vec2d &screen_coor, bool try_no_coor); + +} // namespace + +namespace Slic3r::GUI::Emboss { + +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume &text_volume) +{ + const ModelVolumePtrs &volumes = text_volume.get_object()->volumes; + // no other volume in object + if (volumes.size() <= 1) + return {}; + return ::create_sources(volumes, text_volume.id().id); +} + +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) + // object is not under mouse position soo create object on plater + return ::start_create_object_job(input, std::move(data), mouse_pos); + + bool try_no_coor = true; + return ::start_create_volume_on_surface_job(input, std::move(data), mouse_pos, try_no_coor); +} + +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); + + bool try_no_coor = false; + return ::start_create_volume_on_surface_job(input, std::move(data), coor, try_no_coor); +} + +#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 Job::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 + +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(); + 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 +} + +} // namespace Slic3r::GUI::Emboss + +//////////////////////////// +/// private namespace implementation +namespace { +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(GLGizmosManager::EType gizmo) +{ + assert(gizmo == GLGizmosManager::Emboss || gizmo == GLGizmosManager::Svg); + return gizmo == GLGizmosManager::Emboss || gizmo == GLGizmosManager::Svg; +} + +bool check(const CreateVolumeParams &input) +{ + bool res = is_valid(input.volume_type); + auto gizmo_type = static_cast(input.gizmo); + res &= ::check(gizmo_type); + 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); + assert(!input.base->shape.projection.use_surface); + res &= !input.base->shape.projection.use_surface; + 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); + 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); + res &= 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); + 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; +} + +template +ExPolygons create_shape(DataBase &input, Fnc was_canceled) { + 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); +} + +//#define STORE_SAMPLING +#ifdef STORE_SAMPLING +#include "libslic3r/SVG.hpp" +#endif // STORE_SAMPLING + +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; +} + +template TriangleMesh create_mesh_per_glyph(DataBase &input, Fnc was_canceled) +{ + // 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.; + 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 < 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) && was_canceled()) + 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)); +} + +template +TriangleMesh try_create_mesh(DataBase &input, const Fnc& was_canceled) +{ + if (!input.text_lines.empty()) { + TriangleMesh tm = create_mesh_per_glyph(input, was_canceled); + if (was_canceled()) return {}; + if (!tm.empty()) return tm; + } + + ExPolygons shapes = create_shape(input, was_canceled); + if (shapes.empty()) return {}; + if (was_canceled()) 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); + if (was_canceled()) return {}; + return TriangleMesh(polygons2model(shapes, project)); +} + +template +TriangleMesh create_mesh(DataBase &input, const Fnc& was_canceled, Job::Ctl& ctl) +{ + // It is neccessary to create some shape + // Emboss text window is opened by creation new emboss text object + TriangleMesh result = try_create_mesh(input, was_canceled); + if (was_canceled()) + return {}; + + if (result.its.empty()) { + result = create_default_mesh(); + if (was_canceled()) + return {}; + // 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; +} + +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; + if (!load_obj(path.c_str(), &triangle_mesh, message)) { + // when can't load mesh use cube + return TriangleMesh(its_make_cube(36., 4., 2.5)); + } + return triangle_mesh; +} + +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); +} + +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); + + // TRN: This is the name of the action appearing in undo/redo stack. + std::string snap_name = _u8L("Text/SVG 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); +} + +void create_volume(TriangleMesh &&mesh, + const ObjectID &object_id, + const ModelVolumeType type, + const std::optional &trmat, + const DataBase &data, + GLGizmosManager::EType gizmo) +{ + 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) + manager.open_gizmo(gizmo); + + // 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, std::function was_canceled) { + 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 = 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 = 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 (Vec3i &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, std::function was_canceled) +{ + // 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, was_canceled); + // 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) && was_canceled()) + return {}; + } + s_i_offset += line_bbs.size(); + } + + if (was_canceled()) return {}; + if (result.empty()) + 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 +template +TriangleMesh cut_surface(DataBase& input1, const SurfaceVolumeData& input2, const Fnc& was_canceled) +{ + if (!input1.text_lines.empty()) + return cut_per_glyph_surface(input1, input2, was_canceled); + + ExPolygons shapes = create_shape(input1, was_canceled); + 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, was_canceled); + if (was_canceled()) return {}; + if (its.empty()) + throw JobException(_u8L("There is no valid surface for text projection.").c_str()); + + return TriangleMesh(std::move(its)); +} + +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; +} + +bool 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; +} + +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 !process(eptr); +} + +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 start_create_volume_job(Worker &worker, + const ModelObject &object, + const std::optional &volume_tr, + DataBasePtr data, + ModelVolumeType volume_type, + GLGizmosManager::EType gizmo) +{ + 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}; + 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}; + job = std::make_unique(std::move(create_volume_data)); + } + return queue_job(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_object_job(const CreateVolumeParams &input, DataBasePtr emboss_data, const Vec2d &coor) +{ + const Pointfs &bed_shape = input.build_volume.printable_area(); + auto gizmo_type = static_cast(input.gizmo); + DataCreateObject data{std::move(emboss_data), coor, input.camera, bed_shape, gizmo_type}; + auto job = std::make_unique(std::move(data)); + return queue_job(input.worker, std::move(job)); +} + +bool start_create_volume_on_surface_job(CreateVolumeParams &input, DataBasePtr data, const Vec2d &screen_coor, bool try_no_coor) +{ + auto on_bad_state = [&input, try_no_coor](DataBasePtr data_, const ModelObject *object = nullptr) { + if (try_no_coor) { + // Can't create on coordinate try to create somewhere + return start_create_volume_without_position(input, std::move(data_)); + } else { + // 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; + + if (object == nullptr) + return false; + + auto gizmo_type = static_cast(input.gizmo); + return start_create_volume_job(input.worker, *object, {}, std::move(data_), input.volume_type, gizmo_type); + } + }; + + assert(input.gl_volume != nullptr); + if (input.gl_volume == nullptr) + return on_bad_state(std::move(data)); + + const Model *model = input.canvas.get_model(); + + assert(model != nullptr); + if (model == nullptr) + return on_bad_state(std::move(data)); + + const ModelObjectPtrs &objects = model->objects; + const ModelVolume *volume = get_model_volume(*input.gl_volume, objects); + assert(volume != nullptr); + if (volume == nullptr) + return on_bad_state(std::move(data)); + + const ModelInstance *instance = get_model_instance(*input.gl_volume, objects); + assert(instance != nullptr); + if (instance == nullptr) + return on_bad_state(std::move(data)); + + const ModelObject *object = volume->get_object(); + assert(object != nullptr); + if (object == nullptr) + return on_bad_state(std::move(data)); + + auto cond = RaycastManager::AllowVolumes({volume->id().id}); + RaycastManager::Meshes meshes = create_meshes(input.canvas, cond); + input.raycaster.actualize(*instance, &cond, &meshes); + std::optional hit = ray_from_camera(input.raycaster, screen_coor, input.camera, &cond); + + // 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()) + // When model is broken. It could appear that hit miss the object. + // So add part near by in simmilar manner as right panel do + 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); + // 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); +} + +void create_message(const std::string &message) { + show_error(nullptr, message.c_str()); +} + +} // namespace diff --git a/src/slic3r/GUI/Jobs/EmbossJob.hpp b/src/slic3r/GUI/Jobs/EmbossJob.hpp new file mode 100644 index 000000000..8dec29572 --- /dev/null +++ b/src/slic3r/GUI/Jobs/EmbossJob.hpp @@ -0,0 +1,265 @@ +///|/ Copyright (c) Prusa Research 2021 - 2022 Oleksandra Iushchenko @YuSanka, Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_EmbossJob_hpp_ +#define slic3r_EmbossJob_hpp_ + +#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 "Job.hpp" + +// forward declarations +namespace Slic3r { +class TriangleMesh; +class ModelVolume; +enum class ModelVolumeType : int; +class BuildVolume; +namespace GUI { +class RaycastManager; +class Plater; +class GLCanvas3D; +class Worker; +class Selection; +}} + +namespace Slic3r::GUI::Emboss { + +/// +/// Base data hold data for create emboss shape +/// +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; +}; + +/// +/// 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 : 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; +}; + +/// +/// Update text shape in existing text volume +/// Predict that there is only one runnig(not canceled) instance of it +/// +class UpdateJob : public Job +{ + 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); +}; + +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{}; + +/// +/// Update text volume to use surface from object +/// +class UpdateSurfaceVolumeJob : public Job +{ + 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; +}; + +/// +/// 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); + +/// +/// shorten params for start_crate_volume functions +/// +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; + + // New created volume type + ModelVolumeType volume_type; + + // Contain AABB trees from scene + RaycastManager &raycaster; + + // Define which gizmo open on the success + unsigned char gizmo; // GLGizmosManager::EType + + // Volume define object to add new volume + const GLVolume *gl_volume; + + // 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 = {}; +}; + +/// +/// Create new volume on position of mouse cursor +/// +/// canvas + camera + bed shape + +/// Shape of emboss +/// New created volume type +/// Knows object in scene +/// Define which gizmo open on the success - enum GLGizmosManager::EType +/// Define position where to create volume +/// Wanted additionl move in Z(emboss) direction of new created volume +/// Wanted additionl rotation around Z of new created volume +/// True on success otherwise False +bool start_create_volume(CreateVolumeParams &input, DataBasePtr data, const Vec2d &mouse_pos); + +/// +/// Same as previous function but without mouse position +/// Need to suggest position or put near the selection +/// +bool start_create_volume_without_position(CreateVolumeParams &input, DataBasePtr data); + +/// +/// Start job for update embossed volume +/// +/// define update data +/// Volume to be updated +/// Keep model and gl_volumes - when start use surface volume must be selected +/// Could cast ray to scene +/// True when start job otherwise false +bool start_update_volume(DataUpdate &&data, const ModelVolume &volume, const Selection &selection, RaycastManager &raycaster); + +} // namespace Slic3r::GUI + +#endif // slic3r_EmbossJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index 37dfd1386..267fbee82 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "FillBedJob.hpp" #include "libslic3r/Model.hpp" @@ -198,8 +202,12 @@ void FillBedJob::prepare() p.translation(X) -= p.bed_idx * stride;*/ } -void FillBedJob::process() +void FillBedJob::process(Ctl &ctl) { + auto statustxt = _u8L("Filling"); + ctl.call_on_main_thread([this] { prepare(); }).wait(); + ctl.update_status(0, statustxt); + if (m_object_idx == -1 || m_selected.empty()) return; update_arrange_params(params, m_plater->config(), m_selected); @@ -217,13 +225,13 @@ void FillBedJob::process() update_unselected_items_inflation(m_unselected, m_plater->config(), params); bool do_stop = false; - params.stopcondition = [this, &do_stop]() { - return was_canceled() || do_stop; + params.stopcondition = [&ctl, &do_stop]() { + return ctl.was_canceled() || do_stop; }; - params.progressind = [this](unsigned st,std::string str="") { + params.progressind = [this, &ctl, &statustxt](unsigned st,std::string str="") { if (st > 0) - update_status(st, _L("Filling") + " " + wxString::FromUTF8(str)); + ctl.update_status(st * 100 / status_range(), statustxt + " " + str); }; params.on_packed = [&do_stop] (const ArrangePolygon &ap) { @@ -235,15 +243,18 @@ void FillBedJob::process() arrangement::arrange(m_selected, m_unselected, m_bedpts, params); // finalize just here. - update_status(m_status_range, was_canceled() ? - _L("Bed filling canceled.") : - _L("Bed filling done.")); + ctl.update_status(100, ctl.was_canceled() ? + _u8L("Bed filling canceled.") : + _u8L("Bed filling done.")); } -void FillBedJob::finalize() +FillBedJob::FillBedJob() : m_plater{wxGetApp().plater()} {} + +void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) { // Ignore the arrange result if aborted. - if (was_canceled()) return; + if (canceled || eptr) + return; if (m_object_idx == -1) return; @@ -304,8 +315,6 @@ void FillBedJob::finalize() m_plater->update(); } - - Job::finalize(); } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/FillBedJob.hpp b/src/slic3r/GUI/Jobs/FillBedJob.hpp index f78b1f9f2..76413434a 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.hpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef FILLBEDJOB_HPP #define FILLBEDJOB_HPP @@ -7,7 +11,7 @@ namespace Slic3r { namespace GUI { class Plater; -class FillBedJob : public PlaterJob +class FillBedJob : public Job { int m_object_idx = -1; @@ -24,23 +28,21 @@ class FillBedJob : public PlaterJob arrangement::ArrangeParams params; int m_status_range = 0; - -protected: - - void prepare() override; - void process() override; + Plater *m_plater; public: - FillBedJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} - {} - int status_range() const override + void prepare(); + void process(Ctl &ctl) override; + + FillBedJob(); + + int status_range() const { return m_status_range; } - void finalize() override; + void finalize(bool canceled, std::exception_ptr &e) override; }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/Job.cpp b/src/slic3r/GUI/Jobs/Job.cpp deleted file mode 100644 index 19c93643a..000000000 --- a/src/slic3r/GUI/Jobs/Job.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include -#include - -#include "Job.hpp" -#include -#include - -namespace Slic3r { - -void GUI::Job::run(std::exception_ptr &eptr) -{ - m_running.store(true); - try { - process(); - } catch (...) { - eptr = std::current_exception(); - } - - m_running.store(false); - - // ensure to call the last status to finalize the job - update_status(status_range(), ""); -} - -void GUI::Job::update_status(int st, const wxString &msg) -{ - auto evt = new wxThreadEvent(wxEVT_THREAD, m_thread_evt_id); - evt->SetInt(st); - evt->SetString(msg); - wxQueueEvent(this, evt); -} - -void GUI::Job::update_percent_finish() -{ - m_progress->clear_percent(); -} - -void GUI::Job::show_error_info(wxString msg, int code, wxString description, wxString extra) -{ - m_progress->show_error_info(msg, code, description, extra); -} - -GUI::Job::Job(std::shared_ptr pri) - : m_progress(std::move(pri)) -{ - m_thread_evt_id = wxNewId(); - - Bind(wxEVT_THREAD, [this](const wxThreadEvent &evt) { - if (m_finalizing) return; - - auto msg = evt.GetString(); - if (!msg.empty() && !m_worker_error) - m_progress->set_status_text(msg.ToUTF8().data()); - - if (m_finalized) return; - - m_progress->set_progress(evt.GetInt()); - if (evt.GetInt() == status_range() || m_worker_error) { - // set back the original range and cancel callback - m_progress->set_range(m_range); - // Make sure progress indicators get the last value of their range - // to make sure they close, fade out, whathever - m_progress->set_progress(m_range); - m_progress->set_cancel_callback(); - wxEndBusyCursor(); - - if (m_worker_error) { - m_finalized = true; - m_progress->set_status_text(""); - m_progress->set_progress(m_range); - on_exception(m_worker_error); - } - else { - // This is an RAII solution to remember that finalization is - // running. The run method calls update_status(status_range(), "") - // at the end, which queues up a call to this handler in all cases. - // If process also calls update_status with maxed out status arg - // it will call this handler twice. It is not a problem unless - // yield is called inside the finilize() method, which would - // jump out of finalize and call this handler again. - struct Finalizing { - bool &flag; - Finalizing (bool &f): flag(f) { flag = true; } - ~Finalizing() { flag = false; } - } fin(m_finalizing); - - finalize(); - } - - // dont do finalization again for the same process - m_finalized = true; - } - }, m_thread_evt_id); -} - -void GUI::Job::start() -{ // Start the job. No effect if the job is already running - if (!m_running.load()) { - prepare(); - - // Save the current status indicatior range and push the new one - m_range = m_progress->get_range(); - m_progress->set_range(status_range()); - - // init cancellation flag and set the cancel callback - m_canceled.store(false); - m_progress->set_cancel_callback( - [this]() { m_canceled.store(true); }); - - m_finalized = false; - m_finalizing = false; - - // Changing cursor to busy - wxBeginBusyCursor(); - - try { // Execute the job - m_worker_error = nullptr; - m_thread = create_thread([this] { this->run(m_worker_error); }); - } catch (std::exception &) { - update_status(status_range(), - _(L("Error! Unable to create thread!"))); - } - - // The state changes will be undone when the process hits the - // last status value, in the status update handler (see ctor) - } -} - -bool GUI::Job::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 false; - - return true; -} - -void GUI::ExclusiveJobGroup::start(size_t jid) { - assert(jid < m_jobs.size()); - stop_all(); - m_jobs[jid]->start(); -} - -void GUI::ExclusiveJobGroup::join_all(int wait_ms) -{ - std::vector aborted(m_jobs.size(), false); - - for (size_t jid = 0; jid < m_jobs.size(); ++jid) - aborted[jid] = m_jobs[jid]->join(wait_ms); - - if (!std::all_of(aborted.begin(), aborted.end(), [](bool t) { return t; })) - BOOST_LOG_TRIVIAL(error) << "Could not abort a job!"; -} - -bool GUI::ExclusiveJobGroup::is_any_running() const -{ - return std::any_of(m_jobs.begin(), m_jobs.end(), - [](const std::unique_ptr &j) { - return j->is_running(); - }); -} - -} - diff --git a/src/slic3r/GUI/Jobs/Job.hpp b/src/slic3r/GUI/Jobs/Job.hpp index a92dfbfbd..6c251cac3 100644 --- a/src/slic3r/GUI/Jobs/Job.hpp +++ b/src/slic3r/GUI/Jobs/Job.hpp @@ -1,130 +1,68 @@ +///|/ Copyright (c) Prusa Research 2019 - 2021 Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef JOB_HPP #define JOB_HPP #include #include +#include #include "libslic3r/libslic3r.h" - -#include - #include "ProgressIndicator.hpp" -#include - -#include - namespace Slic3r { namespace GUI { -// A class to handle UI jobs like arranging and optimizing rotation. -// These are not instant jobs, the user has to be informed about their -// state in the status progress indicator. On the other hand they are -// separated from the background slicing process. Ideally, these jobs should -// run when the background process is not running. -// -// TODO: A mechanism would be useful for blocking the plater interactions: -// objects would be frozen for the user. In case of arrange, an animation -// could be shown, or with the optimize orientations, partial results -// could be displayed. -class Job : public wxEvtHandler -{ - int m_range = 100; - int m_thread_evt_id = wxID_ANY; - boost::thread m_thread; - std::atomic m_running{false}, m_canceled{false}; - bool m_finalized = false, m_finalizing = false; - std::shared_ptr m_progress; - std::exception_ptr m_worker_error = nullptr; - - void run(std::exception_ptr &); - -protected: - // status range for a particular job - virtual int status_range() const { return 100; } - - // status update, to be used from the work thread (process() method) - void update_status(int st, const wxString &msg = ""); - - void update_percent_finish(); - - void show_error_info(wxString msg, int code, wxString description, wxString extra); - - bool was_canceled() const { return m_canceled.load(); } - - // Launched just before start(), a job can use it to prepare internals - virtual void prepare() {} - - // The method where the actual work of the job should be defined. - virtual void process() = 0; - - // Launched when the job is finished. It refreshes the 3Dscene by def. - virtual void finalize() { m_finalized = true; } - - // Exceptions occuring in process() are redirected from the worker thread - // into the main (UI) thread. This method is called from the main thread and - // can be overriden to handle these exceptions. - virtual void on_exception(const std::exception_ptr &eptr) - { - if (eptr) std::rethrow_exception(eptr); - } - +// 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 Job { public: + enum JobPrepareState { PREPARE_STATE_DEFAULT = 0, PREPARE_STATE_MENU = 1, }; - Job(std::shared_ptr pri); - - bool is_finalized() const { return m_finalized; } - - Job(const Job &) = delete; - Job(Job &&) = delete; - Job &operator=(const Job &) = delete; - Job &operator=(Job &&) = delete; - - void start(); - - // To wait for the running job and join the threads. False is - // returned if the timeout has been reached and the job is still - // running. Call cancel() before this fn if you want to explicitly - // end the job. - bool join(int timeout_ms = 0); - - bool is_running() const { return m_running.load(); } - void cancel() { m_canceled.store(true); } -}; + // 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; -// 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. -class ExclusiveJobGroup -{ - static const int ABORT_WAIT_MAX_MS = 10000; - - std::vector> m_jobs; - -protected: - virtual void before_start() {} - -public: - virtual ~ExclusiveJobGroup() = default; - - size_t add_job(std::unique_ptr &&job) - { - m_jobs.emplace_back(std::move(job)); - return m_jobs.size() - 1; - } - - void start(size_t jid); - - void cancel_all() { for (auto& j : m_jobs) j->cancel(); } - - void join_all(int wait_ms = 0); - - void stop_all() { cancel_all(); join_all(ABORT_WAIT_MAX_MS); } - - bool is_any_running() const; + // 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 ~Job() = 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 diff --git a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp index 13470f7ad..318cc066c 100644 --- a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp +++ b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2021 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "NotificationProgressIndicator.hpp" #include "slic3r/GUI/NotificationManager.hpp" @@ -22,11 +26,15 @@ 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..44fa41797 100644 --- a/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp +++ b/src/slic3r/GUI/Jobs/NotificationProgressIndicator.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2021 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef NOTIFICATIONPROGRESSINDICATOR_HPP #define NOTIFICATIONPROGRESSINDICATOR_HPP @@ -9,6 +13,7 @@ class NotificationManager; class NotificationProgressIndicator: public ProgressIndicator { NotificationManager *m_nm = nullptr; + CancelFn m_cancelfn; public: diff --git a/src/slic3r/GUI/Jobs/OrientJob.cpp b/src/slic3r/GUI/Jobs/OrientJob.cpp index c750a509a..1dcc510ee 100644 --- a/src/slic3r/GUI/Jobs/OrientJob.cpp +++ b/src/slic3r/GUI/Jobs/OrientJob.cpp @@ -140,29 +140,23 @@ void OrientJob::prepare() int state = m_plater->get_prepare_state(); m_plater->get_notification_manager()->bbl_close_plateinfo_notification(); if (state == Job::JobPrepareState::PREPARE_STATE_DEFAULT) { - only_on_partplate = false; + // only_on_partplate = false; prepare_selected(); } else if (state == Job::JobPrepareState::PREPARE_STATE_MENU) { - only_on_partplate = true; // only arrange items on current plate + // only_on_partplate = true; // only arrange items on current plate prepare_partplate(); } } -void OrientJob::on_exception(const std::exception_ptr &eptr) +void OrientJob::process(Ctl &ctl) { - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (std::exception &) { - PlaterJob::on_exception(eptr); - } -} + static const auto arrangestr = _u8L("Orienting..."); + + ctl.update_status(0, arrangestr); + ctl.call_on_main_thread([this]{ prepare(); }).wait();; -void OrientJob::process() -{ auto start = std::chrono::steady_clock::now(); - static const auto arrangestr = _(L("Orienting...")); const GLCanvas3D::OrientSettings& settings = m_plater->canvas3D()->get_orient_settings(); @@ -177,11 +171,11 @@ void OrientJob::process() } auto count = unsigned(m_selected.size() + m_unprintable.size()); - params.stopcondition = [this]() { return was_canceled(); }; + params.stopcondition = [&ctl]() { return ctl.was_canceled(); }; - params.progressind = [this, count](unsigned st, std::string orientstr) { + params.progressind = [this, count, &ctl](unsigned st, std::string orientstr) { st += m_unprintable.size(); - if (st > 0) update_status(int(st / float(count) * 100), _L("Orienting") + " " + orientstr); + if (st > 0) ctl.update_status(int(st / float(count) * 100), _u8L("Orienting") + " " + orientstr); }; orientation::orient(m_selected, m_unselected, params); @@ -194,15 +188,27 @@ void OrientJob::process() << "Orientation: " << m_selected.back().orientation.transpose() << "; v,phi: " << m_selected.back().axis.transpose() << ", " << m_selected.back().angle << "; euler: " << m_selected.back().euler_angles.transpose(); // finalize just here. - //update_status(int(count), - // was_canceled() ? _(L("Orienting canceled.")) - // : _(L(ss.str().c_str()))); - wxGetApp().plater()->show_status_message(was_canceled() ? "Orienting canceled." : ss.str()); + ctl.update_status(100, + ctl.was_canceled() ? _u8L("Orienting canceled.") + : _u8L(ss.str().c_str())); + wxGetApp().plater()->show_status_message(ctl.was_canceled() ? "Orienting canceled." : ss.str()); } -void OrientJob::finalize() { +OrientJob::OrientJob() : m_plater{wxGetApp().plater()} {} + +void OrientJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + try { + if (eptr) + std::rethrow_exception(eptr); + eptr = nullptr; + } catch (...) { + eptr = std::current_exception(); + } + // Ignore the arrange result if aborted. - if (was_canceled()) return; + if (canceled || eptr) + return; for (OrientMesh& mesh : m_selected) { @@ -214,8 +220,6 @@ void OrientJob::finalize() { // BBS //wxGetApp().obj_manipul()->set_dirty(); - - Job::finalize(); } orientation::OrientMesh OrientJob::get_orient_mesh(ModelInstance* instance) diff --git a/src/slic3r/GUI/Jobs/OrientJob.hpp b/src/slic3r/GUI/Jobs/OrientJob.hpp index 88907b6b9..968be209e 100644 --- a/src/slic3r/GUI/Jobs/OrientJob.hpp +++ b/src/slic3r/GUI/Jobs/OrientJob.hpp @@ -1,7 +1,7 @@ #ifndef ORIENTJOB_HPP #define ORIENTJOB_HPP -#include "PlaterJob.hpp" +#include "Job.hpp" #include "libslic3r/Orient.hpp" namespace Slic3r { @@ -10,12 +10,15 @@ class ModelObject; namespace GUI { -class OrientJob : public PlaterJob +class Plater; + +class OrientJob : public Job { using OrientMesh = orientation::OrientMesh; using OrientMeshs = orientation::OrientMeshs; OrientMeshs m_selected, m_unselected, m_unprintable; + Plater *m_plater; // clear m_selected and m_unselected, reserve space for next usage void clear_input(); @@ -30,18 +33,14 @@ class OrientJob : public PlaterJob //BBS:prepare the items from current selected partplate void prepare_partplate(); -protected: - void prepare() override; - void on_exception(const std::exception_ptr &) override; - public: - OrientJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} - {} + void prepare(); - void process() override; + void process(Ctl &ctl) override; + + OrientJob(); - void finalize() override; + void finalize(bool canceled, std::exception_ptr &e) override; #if 0 static orientation::OrientMesh get_orient_mesh(ModelObject* obj, const Plater* plater) diff --git a/src/slic3r/GUI/Jobs/PlaterJob.cpp b/src/slic3r/GUI/Jobs/PlaterJob.cpp deleted file mode 100644 index 902cc5738..000000000 --- a/src/slic3r/GUI/Jobs/PlaterJob.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "PlaterJob.hpp" -#include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/Plater.hpp" - -namespace Slic3r { namespace GUI { - -void PlaterJob::on_exception(const std::exception_ptr &eptr) -{ - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (std::exception &e) { - show_error(m_plater, _(L("Exception")) + ": "+ e.what()); - } -} - -}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/PlaterJob.hpp b/src/slic3r/GUI/Jobs/PlaterJob.hpp deleted file mode 100644 index fa8a19c1e..000000000 --- a/src/slic3r/GUI/Jobs/PlaterJob.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef PLATERJOB_HPP -#define PLATERJOB_HPP - -#include "Job.hpp" - -namespace Slic3r { namespace GUI { - -class Plater; - -class PlaterJob : public Job { -protected: - Plater *m_plater; - //BBS: add flag for whether on current part plate - bool only_on_partplate{false}; - - void on_exception(const std::exception_ptr &) override; - -public: - - PlaterJob(std::shared_ptr pri, Plater *plater): - Job{std::move(pri)}, m_plater{plater} {} -}; - -}} // namespace Slic3r::GUI - -#endif // PLATERJOB_HPP diff --git a/src/slic3r/GUI/Jobs/PlaterWorker.hpp b/src/slic3r/GUI/Jobs/PlaterWorker.hpp new file mode 100644 index 000000000..1a38736d9 --- /dev/null +++ b/src/slic3r/GUI/Jobs/PlaterWorker.hpp @@ -0,0 +1,165 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Oleksandra Iushchenko @YuSanka, Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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 Job { + 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); + } + }; + + void on_idle(wxIdleEvent &evt) + { + process_events(); + evt.Skip(); + } + +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. + plater->Bind(wxEVT_IDLE, &PlaterWorker::on_idle, this); + } + + ~PlaterWorker() + { + m_plater->Unbind(wxEVT_IDLE, &PlaterWorker::on_idle, this); + } + + // 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/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index 186f031f5..c6e5a837a 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -6,38 +6,39 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/format.hpp" #include "bambu_networking.hpp" namespace Slic3r { namespace GUI { -static wxString check_gcode_failed_str = _L("Abnormal print file data. Please slice again."); -static wxString printjob_cancel_str = _L("Task canceled."); -static wxString timeout_to_upload_str = _L("Upload task timed out. Please check the network status and try again."); -static wxString failed_in_cloud_service_str = _L("Cloud service connection failed. Please try again."); -static wxString file_is_not_exists_str = _L("Print file not found. please slice again."); -static wxString file_over_size_str = _L("The print file exceeds the maximum allowable size (1GB). Please simplify the model and slice again."); -static wxString print_canceled_str = _L("Task canceled."); -static wxString send_print_failed_str = _L("Failed to send the print job. Please try again."); -static wxString upload_ftp_failed_str = _L("Failed to upload file to ftp. Please try again."); +static auto check_gcode_failed_str = _u8L("Abnormal print file data. Please slice again."); +static auto printjob_cancel_str = _u8L("Task canceled."); +static auto timeout_to_upload_str = _u8L("Upload task timed out. Please check the network status and try again."); +static auto failed_in_cloud_service_str = _u8L("Cloud service connection failed. Please try again."); +static auto file_is_not_exists_str = _u8L("Print file not found. please slice again."); +static auto file_over_size_str = _u8L("The print file exceeds the maximum allowable size (1GB). Please simplify the model and slice again."); +static auto print_canceled_str = _u8L("Task canceled."); +static auto send_print_failed_str = _u8L("Failed to send the print job. Please try again."); +static auto upload_ftp_failed_str = _u8L("Failed to upload file to ftp. Please try again."); -static wxString desc_network_error = _L("Check the current status of the bambu server by clicking on the link above."); -static wxString desc_file_too_large = _L("The size of the print file is too large. Please adjust the file size and try again."); -static wxString desc_fail_not_exist = _L("Print file not found, Please slice it again and send it for printing."); -static wxString desc_upload_ftp_failed = _L("Failed to upload print file to FTP. Please check the network status and try again."); +static auto desc_network_error = _u8L("Check the current status of the bambu server by clicking on the link above."); +static auto desc_file_too_large = _u8L("The size of the print file is too large. Please adjust the file size and try again."); +static auto desc_fail_not_exist = _u8L("Print file not found, Please slice it again and send it for printing."); +static auto desc_upload_ftp_failed = _u8L("Failed to upload print file to FTP. Please check the network status and try again."); -static wxString sending_over_lan_str = _L("Sending print job over LAN"); -static wxString sending_over_cloud_str = _L("Sending print job through cloud service"); +static auto sending_over_lan_str = _u8L("Sending print job over LAN"); +static auto sending_over_cloud_str = _u8L("Sending print job through cloud service"); -static wxString wait_sending_finish = _L("Print task sending times out."); -static wxString desc_wait_sending_finish = _L("The printer timed out while receiving a print job. Please check if the network is functioning properly and send the print again."); +static auto wait_sending_finish = _u8L("Print task sending times out."); +static auto desc_wait_sending_finish = _u8L("The printer timed out while receiving a print job. Please check if the network is functioning properly and send the print again."); -PrintJob::PrintJob(std::shared_ptr pri, Plater* plater, std::string dev_id) -: PlaterJob{ std::move(pri), plater }, +PrintJob::PrintJob(std::string dev_id) +: m_plater{wxGetApp().plater()}, m_dev_id(dev_id), m_is_calibration_task(false) { - m_print_job_completed_id = plater->get_print_finished_event(); + m_print_job_completed_id = m_plater->get_print_finished_event(); } void PrintJob::prepare() @@ -52,16 +53,6 @@ void PrintJob::prepare() } } -void PrintJob::on_exception(const std::exception_ptr &eptr) -{ - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (std::exception &e) { - PlaterJob::on_exception(eptr); - } -} - void PrintJob::on_success(std::function success) { m_success_fun = success; @@ -131,22 +122,25 @@ wxString PrintJob::get_http_error_msg(unsigned int status, std::string body) return wxEmptyString; } -void PrintJob::process() +void PrintJob::process(Ctl &ctl) { /* display info */ - wxString msg; + std::string msg; wxString error_str; int curr_percent = 10; NetworkAgent* m_agent = wxGetApp().getAgent(); AppConfig* config = wxGetApp().app_config; if (this->connection_type == "lan") { - msg = _L("Sending print job over LAN"); + msg = _u8L("Sending print job over LAN"); } else { - msg = _L("Sending print job through cloud service"); + msg = _u8L("Sending print job through cloud service"); } + ctl.update_status(0, msg); + ctl.call_on_main_thread([this] { prepare(); }).wait(); + int result = -1; unsigned int http_code; std::string http_body; @@ -162,12 +156,12 @@ void PrintJob::process() /* check gcode is valid */ if (!plate->is_valid_gcode_file() && m_print_type == "from_normal") { - update_status(curr_percent, check_gcode_failed_str); + ctl.update_status(curr_percent, check_gcode_failed_str); return; } - if (was_canceled()) { - update_status(curr_percent, printjob_cancel_str); + if (ctl.was_canceled()) { + ctl.update_status(curr_percent, printjob_cancel_str); return; } } @@ -299,7 +293,7 @@ void PrintJob::process() } wxString error_text; - wxString msg_text; + std::string msg_text; const int StagePercentPoint[(int)PrintingStageFinished + 1] = { @@ -315,7 +309,7 @@ void PrintJob::process() bool is_try_lan_mode = false; bool is_try_lan_mode_failed = false; - auto update_fn = [this, + auto update_fn = [this, &ctl, &is_try_lan_mode, &is_try_lan_mode_failed, &msg, @@ -327,49 +321,49 @@ void PrintJob::process() if (stage == BBL::SendingPrintJobStage::PrintingStageCreate && !is_try_lan_mode_failed) { if (this->connection_type == "lan") { - msg = _L("Sending print job over LAN"); + msg = _u8L("Sending print job over LAN"); } else { - msg = _L("Sending print job through cloud service"); + msg = _u8L("Sending print job through cloud service"); } } else if (stage == BBL::SendingPrintJobStage::PrintingStageUpload && !is_try_lan_mode_failed) { if (code >= 0 && code <= 100 && !info.empty()) { if (this->connection_type == "lan") { - msg = _L("Sending print job over LAN"); + msg = _u8L("Sending print job over LAN"); } else { - msg = _L("Sending print job through cloud service"); + msg = _u8L("Sending print job through cloud service"); } - msg += wxString::Format("(%s)", info); + msg += format("(%s)", info); } } else if (stage == BBL::SendingPrintJobStage::PrintingStageWaiting) { if (this->connection_type == "lan") { - msg = _L("Sending print job over LAN"); + msg = _u8L("Sending print job over LAN"); } else { - msg = _L("Sending print job through cloud service"); + msg = _u8L("Sending print job through cloud service"); } } else if (stage == BBL::SendingPrintJobStage::PrintingStageRecord && !is_try_lan_mode) { - msg = _L("Sending print configuration"); + msg = _u8L("Sending print configuration"); } else if (stage == BBL::SendingPrintJobStage::PrintingStageSending && !is_try_lan_mode) { if (this->connection_type == "lan") { - msg = _L("Sending print job over LAN"); + msg = _u8L("Sending print job over LAN"); } else { - msg = _L("Sending print job through cloud service"); + msg = _u8L("Sending print job through cloud service"); } } else if (stage == BBL::SendingPrintJobStage::PrintingStageFinished) { - msg = wxString::Format(_L("Successfully sent. Will automatically jump to the device page in %ss"), info); + msg = format(_u8L("Successfully sent. Will automatically jump to the device page in %ss"), info); if (m_print_job_completed_id == wxGetApp().plater()->get_send_calibration_finished_event()) { - msg = wxString::Format(_L("Successfully sent. Will automatically jump to the next page in %ss"), info); + msg = format(_u8L("Successfully sent. Will automatically jump to the next page in %ss"), info); } - this->update_percent_finish(); + ctl.clear_percent(); } else { if (this->connection_type == "lan") { - msg = _L("Sending print job over LAN"); + msg = _u8L("Sending print job over LAN"); } else { - msg = _L("Sending print job through cloud service"); + msg = _u8L("Sending print job through cloud service"); } } @@ -386,22 +380,22 @@ void PrintJob::process() //get errors if (code > 100 || code < 0 || stage == BBL::SendingPrintJobStage::PrintingStageERROR) { if (code == BAMBU_NETWORK_ERR_PRINT_WR_FILE_OVER_SIZE || code == BAMBU_NETWORK_ERR_PRINT_SP_FILE_OVER_SIZE) { - m_plater->update_print_error_info(code, desc_file_too_large.ToStdString(), info); + m_plater->update_print_error_info(code, desc_file_too_large, info); }else if (code == BAMBU_NETWORK_ERR_PRINT_WR_FILE_NOT_EXIST || code == BAMBU_NETWORK_ERR_PRINT_SP_FILE_NOT_EXIST){ - m_plater->update_print_error_info(code, desc_fail_not_exist.ToStdString(), info); + m_plater->update_print_error_info(code, desc_fail_not_exist, info); }else if (code == BAMBU_NETWORK_ERR_PRINT_LP_UPLOAD_FTP_FAILED || code == BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED) { - m_plater->update_print_error_info(code, desc_upload_ftp_failed.ToStdString(), info); + m_plater->update_print_error_info(code, desc_upload_ftp_failed, info); }else { - m_plater->update_print_error_info(code, desc_network_error.ToStdString(), info); + m_plater->update_print_error_info(code, desc_network_error, info); } } else { - this->update_status(curr_percent, msg); + ctl.update_status(curr_percent, msg); } }; - auto cancel_fn = [this]() { - return was_canceled(); + auto cancel_fn = [&ctl]() { + return ctl.was_canceled(); }; @@ -444,7 +438,7 @@ void PrintJob::process() boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } //this->update_status(curr_percent, _L("Print task sending times out.")); - m_plater->update_print_error_info(BAMBU_NETWORK_ERR_TIMEOUT, wait_sending_finish.ToStdString(), desc_wait_sending_finish.ToStdString()); + m_plater->update_print_error_info(BAMBU_NETWORK_ERR_TIMEOUT, wait_sending_finish, desc_wait_sending_finish); BOOST_LOG_TRIVIAL(info) << "print_job: timeout, cancel the job" << obj->job_id_; /* handle tiemout */ obj->command_task_cancel(curr_job_id); @@ -475,7 +469,7 @@ void PrintJob::process() } else { BOOST_LOG_TRIVIAL(info) << "print_job: use ftp send print only"; - this->update_status(curr_percent, _L("Sending print job over LAN")); + ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); is_try_lan_mode = true; result = m_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn); if (result < 0) { @@ -492,7 +486,7 @@ void PrintJob::process() && this->has_sdcard) { // try to send local with record BOOST_LOG_TRIVIAL(info) << "print_job: try to start local print with record"; - this->update_status(curr_percent, _L("Sending print job over LAN")); + ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); result = m_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn); if (result == 0) { params.comments = ""; @@ -507,22 +501,22 @@ void PrintJob::process() is_try_lan_mode_failed = true; // try to send with cloud BOOST_LOG_TRIVIAL(warning) << "print_job: try to send with cloud"; - this->update_status(curr_percent, _L("Sending print job through cloud service")); + ctl.update_status(curr_percent, _u8L("Sending print job through cloud service")); result = m_agent->start_print(params, update_fn, cancel_fn, wait_fn); } } else { BOOST_LOG_TRIVIAL(info) << "print_job: send with cloud"; - this->update_status(curr_percent, _L("Sending print job through cloud service")); + ctl.update_status(curr_percent, _u8L("Sending print job through cloud service")); result = m_agent->start_print(params, update_fn, cancel_fn, wait_fn); } } } else { if (this->has_sdcard) { - this->update_status(curr_percent, _L("Sending print job over LAN")); + ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); result = m_agent->start_local_print(params, update_fn, cancel_fn); } else { - this->update_status(curr_percent, _L("An SD card needs to be inserted before printing via LAN.")); + ctl.update_status(curr_percent, _u8L("An SD card needs to be inserted before printing via LAN.")); return; } } @@ -542,13 +536,13 @@ void PrintJob::process() msg_text = upload_ftp_failed_str; } else if (result == BAMBU_NETWORK_ERR_CANCELED) { msg_text = print_canceled_str; - this->update_status(0, msg_text); + ctl.update_status(0, msg_text); } else { msg_text = send_print_failed_str; } if (result != BAMBU_NETWORK_ERR_CANCELED) { - this->show_error_info(msg_text, 0, "", ""); + ctl.show_error_info(msg_text, 0, "", ""); } BOOST_LOG_TRIVIAL(error) << "print_job: failed, result = " << result; @@ -574,10 +568,17 @@ void PrintJob::process() } } -void PrintJob::finalize() { - if (was_canceled()) return; +void PrintJob::finalize(bool canceled, std::exception_ptr &eptr) { + try { + if (eptr) + std::rethrow_exception(eptr); + eptr = nullptr; + } catch (...) { + eptr = std::current_exception(); + } - Job::finalize(); + if (canceled || eptr) + return; } void PrintJob::set_project_name(std::string name) @@ -601,10 +602,10 @@ void PrintJob::on_check_ip_address_success(std::function func) m_enter_ip_address_fun_success = func; } -void PrintJob::connect_to_local_mqtt() -{ - this->update_status(0, wxEmptyString); -} +// void PrintJob::connect_to_local_mqtt() +// { +// this->update_status(0, wxEmptyString); +// } void PrintJob::set_calibration_task(bool is_calibration) { diff --git a/src/slic3r/GUI/Jobs/PrintJob.hpp b/src/slic3r/GUI/Jobs/PrintJob.hpp index 9c12013a0..e703f86fb 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.hpp +++ b/src/slic3r/GUI/Jobs/PrintJob.hpp @@ -3,13 +3,15 @@ #include #include -#include "PlaterJob.hpp" +#include "Job.hpp" namespace fs = boost::filesystem; namespace Slic3r { namespace GUI { +class Plater; + #define PRINT_JOB_SENDING_TIMEOUT 25 class PrintPrepareData @@ -34,7 +36,7 @@ public: BedType bed_type = BedType::btDefault; }; -class PrintJob : public PlaterJob +class PrintJob : public Job { std::function m_success_fun{nullptr}; std::string m_dev_id; @@ -43,16 +45,14 @@ class PrintJob : public PlaterJob wxString m_completed_evt_data; std::function m_enter_ip_address_fun_fail{ nullptr }; std::function m_enter_ip_address_fun_success{ nullptr }; + Plater *m_plater; public: PrintPrepareData job_data; PlateListData plate_data; -protected: - void prepare() override; - void on_exception(const std::exception_ptr &) override; -public: - PrintJob(std::shared_ptr pri, Plater *plater, std::string dev_id = ""); + void prepare(); + PrintJob(std::string dev_id = ""); std::string m_project_name; std::string m_dev_ip; @@ -90,7 +90,7 @@ public: task_layer_inspect = layer_inspect; } - int status_range() const override + int status_range() const { return 100; } @@ -101,13 +101,13 @@ public: m_completed_evt_data = evt_data; } void on_success(std::function success); - void process() override; - void finalize() override; + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &e) override; void set_project_name(std::string name); void set_dst_name(std::string path); void on_check_ip_address_fail(std::function func); void on_check_ip_address_success(std::function func); - void connect_to_local_mqtt(); + // void connect_to_local_mqtt(); wxString get_http_error_msg(unsigned int status, std::string body); std::string truncate_string(const std::string& str, size_t maxLength); void set_calibration_task(bool is_calibration); diff --git a/src/slic3r/GUI/Jobs/ProgressIndicator.hpp b/src/slic3r/GUI/Jobs/ProgressIndicator.hpp index 7a0b88396..4aa5757d2 100644 --- a/src/slic3r/GUI/Jobs/ProgressIndicator.hpp +++ b/src/slic3r/GUI/Jobs/ProgressIndicator.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2018 - 2020 Tomáš Mészáros @tamasmeszaros, Vojtěch Bubník @bubnikv +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef IPROGRESSINDICATOR_HPP #define IPROGRESSINDICATOR_HPP diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index 8cb5dc6c3..eb5140ec1 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 Oleksandra Iushchenko @YuSanka, Tomáš Mészáros @tamasmeszaros, Lukáš Matěna @lukasmatena +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "RotoptimizeJob.hpp" #include "libslic3r/MTUtils.hpp" @@ -45,21 +49,24 @@ void RotoptimizeJob::prepare() } } -void RotoptimizeJob::process() +void RotoptimizeJob::process(Ctl &ctl) { int prev_status = 0; + auto statustxt = _u8L("Searching for optimal orientation"); + ctl.update_status(0, statustxt); + auto params = sla::RotOptimizeParams{} .accuracy(m_accuracy) .print_config(&m_default_print_cfg) - .statucb([this, &prev_status](int s) + .statucb([this, &prev_status, &ctl/*, &statustxt*/](int s) { if (s > 0 && s < 100) ; - // update_status(prev_status + s / m_selected_object_ids.size(), - // _(L("Searching for optimal orientation..."))); + // ctl.update_status(prev_status + s / m_selected_object_ids.size(), + // statustxt); - return !was_canceled(); + return !ctl.was_canceled(); }); @@ -72,16 +79,19 @@ void RotoptimizeJob::process() prev_status += 100 / m_selected_object_ids.size(); - if (was_canceled()) break; + if (ctl.was_canceled()) break; } - // update_status(100, was_canceled() ? _(L("Orientation search canceled.")) : - // _(L("Orientation found."))); + ctl.update_status(100, ctl.was_canceled() ? _u8L("Orientation search canceled.") : + _u8L("Orientation found.")); } -void RotoptimizeJob::finalize() +RotoptimizeJob::RotoptimizeJob() : m_plater{wxGetApp().plater()} { prepare(); } + +void RotoptimizeJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (was_canceled()) return; + if (canceled || eptr) + return; for (const ObjRot &objrot : m_selected_object_ids) { ModelObject *o = m_plater->model().objects[size_t(objrot.idx)]; @@ -112,10 +122,8 @@ void RotoptimizeJob::finalize() // m_plater->find_new_position(o->instances); } - if (!was_canceled()) + if (!canceled) m_plater->update(); - - Job::finalize(); } }} diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp index cdb367f23..6f6830a9b 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.hpp @@ -1,16 +1,23 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 Oleksandra Iushchenko @YuSanka, Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef ROTOPTIMIZEJOB_HPP #define ROTOPTIMIZEJOB_HPP -#include "PlaterJob.hpp" +#include "Job.hpp" #include "libslic3r/SLA/Rotfinder.hpp" #include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/I18N.hpp" namespace Slic3r { namespace GUI { -class RotoptimizeJob : public PlaterJob +class Plater; + +class RotoptimizeJob : public Job { using FindFn = std::function; @@ -44,19 +51,16 @@ class RotoptimizeJob : public PlaterJob }; std::vector m_selected_object_ids; - -protected: - - void prepare() override; - void process() override; + Plater *m_plater; public: - RotoptimizeJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater} - {} + void prepare(); + void process(Ctl &ctl) override; - void finalize() override; + RotoptimizeJob(); + + void finalize(bool canceled, std::exception_ptr &) override; static constexpr size_t get_methods_count() { return std::size(Methods); } diff --git a/src/slic3r/GUI/Jobs/SLAImportDialog.hpp b/src/slic3r/GUI/Jobs/SLAImportDialog.hpp new file mode 100644 index 000000000..14d3ef678 --- /dev/null +++ b/src/slic3r/GUI/Jobs/SLAImportDialog.hpp @@ -0,0 +1,104 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Oleksandra Iushchenko @YuSanka, Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef SLAIMPORTDIALOG_HPP +#define SLAIMPORTDIALOG_HPP + +#include "SLAImportJob.hpp" + +#include +#include +#include +#include +#include + +#include "libslic3r/AppConfig.hpp" +#include "slic3r/GUI/I18N.hpp" + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" + +// #include "libslic3r/Model.hpp" +// #include "libslic3r/PresetBundle.hpp" + +namespace Slic3r { namespace GUI { + +class SLAImportDialog : public wxDialog, public SLAImportJobView +{ + wxFilePickerCtrl *m_filepicker; + wxComboBox *m_import_dropdown, *m_quality_dropdown; + +public: + SLAImportDialog(Plater *plater) : wxDialog{plater, wxID_ANY, "Import SLA archive"} + { + auto szvert = new wxBoxSizer{wxVERTICAL}; + auto szfilepck = new wxBoxSizer{wxHORIZONTAL}; + + m_filepicker = new wxFilePickerCtrl(this, wxID_ANY, from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")), + "SL1 / SL1S archive files (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP", + wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + szfilepck->Add(new wxStaticText(this, wxID_ANY, _L("Import file") + ": "), 0, wxALIGN_CENTER); + szfilepck->Add(m_filepicker, 1); + szvert->Add(szfilepck, 0, wxALL | wxEXPAND, 5); + + auto szchoices = new wxBoxSizer{wxHORIZONTAL}; + + static const std::vector inp_choices = {_(L("Import model and profile")), _(L("Import profile only")), + _(L("Import model only"))}; + + m_import_dropdown = new wxComboBox(this, wxID_ANY, inp_choices[0], wxDefaultPosition, wxDefaultSize, inp_choices.size(), + inp_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); + + szchoices->Add(m_import_dropdown); + szchoices->Add(new wxStaticText(this, wxID_ANY, _L("Quality") + ": "), 0, wxALIGN_CENTER | wxALL, 5); + + static const std::vector qual_choices = {_(L("Accurate")), _(L("Balanced")), _(L("Quick"))}; + + m_quality_dropdown = new wxComboBox(this, wxID_ANY, qual_choices[0], wxDefaultPosition, wxDefaultSize, qual_choices.size(), + qual_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); + szchoices->Add(m_quality_dropdown); + + m_import_dropdown->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) { + if (get_selection() == Sel::profileOnly) + m_quality_dropdown->Disable(); + else + m_quality_dropdown->Enable(); + }); + + szvert->Add(szchoices, 0, wxALL, 5); + szvert->AddStretchSpacer(1); + auto szbtn = new wxBoxSizer(wxHORIZONTAL); + szbtn->Add(new wxButton{this, wxID_CANCEL}); + szbtn->Add(new wxButton{this, wxID_OK}); + szvert->Add(szbtn, 0, wxALIGN_RIGHT | wxALL, 5); + + SetSizerAndFit(szvert); + } + + Sel get_selection() const override + { + int sel = m_import_dropdown->GetSelection(); + return Sel(std::min(int(Sel::modelOnly), std::max(0, sel))); + } + + Vec2i get_marchsq_windowsize() const override + { + enum { Accurate, Balanced, Fast }; + + switch (m_quality_dropdown->GetSelection()) { + case Fast: return {8, 8}; + case Balanced: return {4, 4}; + default: + case Accurate: return {2, 2}; + } + } + + std::string get_path() const override { return m_filepicker->GetPath().ToUTF8().data(); } +}; + +}} // namespace Slic3r::GUI + +#endif // SLAIMPORTDIALOG_HPP diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index 0d42cec2d..e8210577a 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -1,9 +1,12 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Tomáš Mészáros @tamasmeszaros, Vojtěch Bubník @bubnikv, David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "SLAImportJob.hpp" #include "libslic3r/Format/SL1.hpp" #include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/NotificationManager.hpp" @@ -11,104 +14,10 @@ #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" -#include -#include -#include #include -#include namespace Slic3r { namespace GUI { -enum class Sel { modelAndProfile, profileOnly, modelOnly}; - -class ImportDlg: public wxDialog { - wxFilePickerCtrl *m_filepicker; - wxComboBox *m_import_dropdown, *m_quality_dropdown; - -public: - ImportDlg(Plater *plater) - : wxDialog{plater, wxID_ANY, "Import SLA archive"} - { - auto szvert = new wxBoxSizer{wxVERTICAL}; - auto szfilepck = new wxBoxSizer{wxHORIZONTAL}; - - m_filepicker = new wxFilePickerCtrl(this, wxID_ANY, - from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")), - "SL1 / SL1S archive files (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP", - wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - szfilepck->Add(new wxStaticText(this, wxID_ANY, _L("Import file") + ": "), 0, wxALIGN_CENTER); - szfilepck->Add(m_filepicker, 1); - szvert->Add(szfilepck, 0, wxALL | wxEXPAND, 5); - - auto szchoices = new wxBoxSizer{wxHORIZONTAL}; - - static const std::vector inp_choices = { - _(L("Import model and profile")), - _(L("Import profile only")), - _(L("Import model only")) - }; - - m_import_dropdown = new wxComboBox( - this, wxID_ANY, inp_choices[0], wxDefaultPosition, wxDefaultSize, - inp_choices.size(), inp_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); - - szchoices->Add(m_import_dropdown); - szchoices->Add(new wxStaticText(this, wxID_ANY, _L("Quality") + ": "), 0, wxALIGN_CENTER | wxALL, 5); - - static const std::vector qual_choices = { - _(L("Accurate")), - _(L("Balanced")), - _(L("Quick")) - }; - - m_quality_dropdown = new wxComboBox( - this, wxID_ANY, qual_choices[0], wxDefaultPosition, wxDefaultSize, - qual_choices.size(), qual_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); - szchoices->Add(m_quality_dropdown); - - m_import_dropdown->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) { - if (get_selection() == Sel::profileOnly) - m_quality_dropdown->Disable(); - else m_quality_dropdown->Enable(); - }); - - szvert->Add(szchoices, 0, wxALL, 5); - szvert->AddStretchSpacer(1); - auto szbtn = new wxBoxSizer(wxHORIZONTAL); - szbtn->Add(new wxButton{this, wxID_CANCEL}); - szbtn->Add(new wxButton{this, wxID_OK}); - szvert->Add(szbtn, 0, wxALIGN_RIGHT | wxALL, 5); - - SetSizerAndFit(szvert); - } - - Sel get_selection() const - { - int sel = m_import_dropdown->GetSelection(); - return Sel(std::min(int(Sel::modelOnly), std::max(0, sel))); - } - - Vec2i get_marchsq_windowsize() const - { - enum { Accurate, Balanced, Fast}; - - switch(m_quality_dropdown->GetSelection()) - { - case Fast: return {8, 8}; - case Balanced: return {4, 4}; - default: - case Accurate: - return {2, 2}; - } - } - - wxString get_path() const - { - return m_filepicker->GetPath(); - } -}; - class SLAImportJob::priv { public: Plater *plater; @@ -122,23 +31,28 @@ public: std::string err; ConfigSubstitutions config_substitutions; - ImportDlg import_dlg; + const SLAImportJobView * import_dlg; - priv(Plater *plt) : plater{plt}, import_dlg{plt} {} + priv(Plater *plt, const SLAImportJobView *view) : plater{plt}, import_dlg{view} {} }; -SLAImportJob::SLAImportJob(std::shared_ptr pri, Plater *plater) - : PlaterJob{std::move(pri), plater}, p{std::make_unique(plater)} -{} +SLAImportJob::SLAImportJob(const SLAImportJobView *view) + : p{std::make_unique(wxGetApp().plater(), view)} +{ + prepare(); +} SLAImportJob::~SLAImportJob() = default; -void SLAImportJob::process() +void SLAImportJob::process(Ctl &ctl) { - auto progr = [this](int s) { + auto statustxt = _u8L("Importing SLA archive"); + ctl.update_status(0, statustxt); + + auto progr = [&ctl, &statustxt](int s) { if (s < 100) - update_status(int(s), _(L("Importing SLA archive"))); - return !was_canceled(); + ctl.update_status(int(s), statustxt); + return !ctl.was_canceled(); }; if (p->path.empty()) return; @@ -161,15 +75,15 @@ void SLAImportJob::process() p->err = ex.what(); } - update_status(100, was_canceled() ? _(L("Importing canceled.")) : - _(L("Importing done."))); + ctl.update_status(100, ctl.was_canceled() ? _u8L("Importing canceled.") : + _u8L("Importing done.")); } void SLAImportJob::reset() { p->sel = Sel::modelAndProfile; p->mesh = {}; - p->profile = m_plater->sla_print().full_print_config(); + p->profile = p->plater->sla_print().full_print_config(); p->win = {2, 2}; p->path.Clear(); } @@ -178,22 +92,19 @@ void SLAImportJob::prepare() { reset(); - if (p->import_dlg.ShowModal() == wxID_OK) { - auto path = p->import_dlg.get_path(); - auto nm = wxFileName(path); - p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : nm.GetFullPath(); - p->sel = p->import_dlg.get_selection(); - p->win = p->import_dlg.get_marchsq_windowsize(); - p->config_substitutions.clear(); - } else { - p->path = ""; - } + auto path = p->import_dlg->get_path(); + auto nm = wxFileName(path); + p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : nm.GetFullPath(); + p->sel = p->import_dlg->get_selection(); + p->win = p->import_dlg->get_marchsq_windowsize(); + p->config_substitutions.clear(); } -void SLAImportJob::finalize() +void SLAImportJob::finalize(bool canceled, std::exception_ptr &eptr) { // Ignore the arrange result if aborted. - if (was_canceled()) return; + if (canceled || eptr) + return; if (!p->err.empty()) { show_error(p->plater, p->err); @@ -204,7 +115,7 @@ void SLAImportJob::finalize() std::string name = wxFileName(p->path).GetName().ToUTF8().data(); if (p->profile.empty()) { - m_plater->get_notification_manager()->push_notification( + p->plater->get_notification_manager()->push_notification( NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel, _L("The imported SLA archive did not contain any presets. " @@ -213,7 +124,7 @@ void SLAImportJob::finalize() if (p->sel != Sel::modelOnly) { if (p->profile.empty()) - p->profile = m_plater->sla_print().full_print_config(); + p->profile = p->plater->sla_print().full_print_config(); const ModelObjectPtrs& objects = p->plater->model().objects; for (auto object : objects) diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.hpp b/src/slic3r/GUI/Jobs/SLAImportJob.hpp index c2ca10ef6..bb620c982 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.hpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.hpp @@ -1,22 +1,40 @@ +///|/ Copyright (c) Prusa Research 2020 - 2022 Tomáš Mészáros @tamasmeszaros, David Kocík @kocikdav +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef SLAIMPORTJOB_HPP #define SLAIMPORTJOB_HPP -#include "PlaterJob.hpp" +#include "Job.hpp" namespace Slic3r { namespace GUI { -class SLAImportJob : public PlaterJob { +class SLAImportJobView +{ +public: + enum Sel { modelAndProfile, profileOnly, modelOnly }; + + virtual ~SLAImportJobView() = default; + + virtual Sel get_selection() const = 0; + virtual Vec2i get_marchsq_windowsize() const = 0; + virtual std::string get_path() const = 0; +}; + +class Plater; + +class SLAImportJob : public Job { class priv; std::unique_ptr p; - -protected: - void prepare() override; - void process() override; - void finalize() override; + using Sel = SLAImportJobView::Sel; public: - SLAImportJob(std::shared_ptr pri, Plater *plater); + void prepare(); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &) override; + + SLAImportJob(const SLAImportJobView *); ~SLAImportJob(); void reset(); diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index ced162784..8f8f68399 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -5,33 +5,34 @@ #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/format.hpp" namespace Slic3r { namespace GUI { -static wxString check_gcode_failed_str = _L("Abnormal print file data. Please slice again."); -static wxString printjob_cancel_str = _L("Task canceled."); -static wxString timeout_to_upload_str = _L("Upload task timed out. Please check the network status and try again."); -static wxString failed_in_cloud_service_str = _L("Cloud service connection failed. Please try again."); -static wxString file_is_not_exists_str = _L("Print file not found. please slice again."); -static wxString file_over_size_str = _L("The print file exceeds the maximum allowable size (1GB). Please simplify the model and slice again."); -static wxString print_canceled_str = _L("Task canceled."); -static wxString send_print_failed_str = _L("Failed to send the print job. Please try again."); -static wxString upload_ftp_failed_str = _L("Failed to upload file to ftp. Please try again."); +static auto check_gcode_failed_str = _u8L("Abnormal print file data. Please slice again."); +static auto printjob_cancel_str = _u8L("Task canceled."); +static auto timeout_to_upload_str = _u8L("Upload task timed out. Please check the network status and try again."); +static auto failed_in_cloud_service_str = _u8L("Cloud service connection failed. Please try again."); +static auto file_is_not_exists_str = _u8L("Print file not found. please slice again."); +static auto file_over_size_str = _u8L("The print file exceeds the maximum allowable size (1GB). Please simplify the model and slice again."); +static auto print_canceled_str = _u8L("Task canceled."); +static auto send_print_failed_str = _u8L("Failed to send the print job. Please try again."); +static auto upload_ftp_failed_str = _u8L("Failed to upload file to ftp. Please try again."); -static wxString desc_network_error = _L("Check the current status of the bambu server by clicking on the link above."); -static wxString desc_file_too_large = _L("The size of the print file is too large. Please adjust the file size and try again."); -static wxString desc_fail_not_exist = _L("Print file not found, Please slice it again and send it for printing."); -static wxString desc_upload_ftp_failed = _L("Failed to upload print file to FTP. Please check the network status and try again."); +static auto desc_network_error = _u8L("Check the current status of the bambu server by clicking on the link above."); +static auto desc_file_too_large = _u8L("The size of the print file is too large. Please adjust the file size and try again."); +static auto desc_fail_not_exist = _u8L("Print file not found, Please slice it again and send it for printing."); +static auto desc_upload_ftp_failed = _u8L("Failed to upload print file to FTP. Please check the network status and try again."); -static wxString sending_over_lan_str = _L("Sending print job over LAN"); -static wxString sending_over_cloud_str = _L("Sending print job through cloud service"); +static auto sending_over_lan_str = _u8L("Sending print job over LAN"); +static auto sending_over_cloud_str = _u8L("Sending print job through cloud service"); -SendJob::SendJob(std::shared_ptr pri, Plater* plater, std::string dev_id) -: PlaterJob{ std::move(pri), plater }, +SendJob::SendJob(std::string dev_id) +: m_plater{wxGetApp().plater()}, m_dev_id(dev_id) { - m_print_job_completed_id = plater->get_send_finished_event(); + m_print_job_completed_id = m_plater->get_send_finished_event(); } void SendJob::prepare() @@ -45,16 +46,6 @@ void SendJob::prepare() } } -void SendJob::on_exception(const std::exception_ptr &eptr) -{ - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (std::exception &e) { - PlaterJob::on_exception(eptr); - } -} - wxString SendJob::get_http_error_msg(unsigned int status, std::string body) { int code = 0; @@ -112,10 +103,10 @@ inline std::string get_transform_string(int bytes) return buffer; } -void SendJob::process() +void SendJob::process(Ctl &ctl) { BBL::PrintParams params; - wxString msg; + std::string msg; int curr_percent = 10; NetworkAgent* m_agent = wxGetApp().getAgent(); AppConfig* config = wxGetApp().app_config; @@ -154,13 +145,15 @@ void SendJob::process() /* display info */ - msg = _L("Sending gcode file over LAN"); + msg = _u8L("Sending gcode file over LAN"); /* if (this->connection_type == "lan") { - msg = _L("Sending gcode file over LAN"); + msg = _u8L("Sending gcode file over LAN"); } else { - msg = _L("Sending gcode file through cloud service"); + msg = _u8L("Sending gcode file through cloud service"); }*/ + ctl.call_on_main_thread([this] { prepare(); }).wait(); + ctl.update_status(0, msg); int total_plate_num = m_plater->get_partplate_list().get_plate_count(); @@ -183,19 +176,19 @@ void SendJob::process() } if (plate == nullptr) { BOOST_LOG_TRIVIAL(error) << "can not find plate with valid gcode file when sending to print, plate_index="<< job_data.plate_idx; - update_status(curr_percent, check_gcode_failed_str); + ctl.update_status(curr_percent, check_gcode_failed_str); return; } } /* check gcode is valid */ if (!plate->is_valid_gcode_file()) { - update_status(curr_percent, check_gcode_failed_str); + ctl.update_status(curr_percent, check_gcode_failed_str); return; } - if (was_canceled()) { - update_status(curr_percent, printjob_cancel_str); + if (ctl.was_canceled()) { + ctl.update_status(curr_percent, printjob_cancel_str); return; } @@ -223,7 +216,7 @@ void SendJob::process() params.use_ssl_for_ftp = m_local_use_ssl_for_ftp; params.use_ssl_for_mqtt = m_local_use_ssl_for_mqtt; wxString error_text; - wxString msg_text; + std::string msg_text; const int StagePercentPoint[(int)PrintingStageFinished + 1] = { 20, // PrintingStageCreate @@ -235,36 +228,37 @@ void SendJob::process() 100 // PrintingStageFinished }; - auto update_fn = [this, &msg, &curr_percent, &error_text, StagePercentPoint](int stage, int code, std::string info) { + auto update_fn = [this, &ctl, + &msg, &curr_percent, &error_text, StagePercentPoint](int stage, int code, std::string info) { if (stage == SendingPrintJobStage::PrintingStageCreate) { if (this->connection_type == "lan") { - msg = _L("Sending gcode file over LAN"); + msg = _u8L("Sending gcode file over LAN"); } else { - msg = _L("Sending gcode file to sdcard"); + msg = _u8L("Sending gcode file to sdcard"); } } else if (stage == SendingPrintJobStage::PrintingStageUpload) { if (code >= 0 && code <= 100 && !info.empty()) { if (this->connection_type == "lan") { - msg = _L("Sending gcode file over LAN"); + msg = _u8L("Sending gcode file over LAN"); } else { - msg = _L("Sending gcode file to sdcard"); + msg = _u8L("Sending gcode file to sdcard"); } if (!info.empty()) { - msg += wxString::Format("(%s)", info); + msg += format("(%s)", info); } } } else if (stage == SendingPrintJobStage::PrintingStageFinished) { - msg = wxString::Format(_L("Successfully sent. Close current page in %s s"), info); + msg = format(_u8L("Successfully sent. Close current page in %s s"), info); } else { if (this->connection_type == "lan") { - msg = _L("Sending gcode file over LAN"); + msg = _u8L("Sending gcode file over LAN"); } else { - msg = _L("Sending gcode file over LAN"); + msg = _u8L("Sending gcode file over LAN"); } } @@ -280,25 +274,25 @@ void SendJob::process() //get errors if (code > 100 || code < 0 || stage == BBL::SendingPrintJobStage::PrintingStageERROR) { if (code == BAMBU_NETWORK_ERR_PRINT_WR_FILE_OVER_SIZE || code == BAMBU_NETWORK_ERR_PRINT_SP_FILE_OVER_SIZE) { - m_plater->update_print_error_info(code, desc_file_too_large.ToStdString(), info); + m_plater->update_print_error_info(code, desc_file_too_large, info); } else if (code == BAMBU_NETWORK_ERR_PRINT_WR_FILE_NOT_EXIST || code == BAMBU_NETWORK_ERR_PRINT_SP_FILE_NOT_EXIST) { - m_plater->update_print_error_info(code, desc_fail_not_exist.ToStdString(), info); + m_plater->update_print_error_info(code, desc_fail_not_exist, info); } else if (code == BAMBU_NETWORK_ERR_PRINT_LP_UPLOAD_FTP_FAILED || code == BAMBU_NETWORK_ERR_PRINT_SG_UPLOAD_FTP_FAILED) { - m_plater->update_print_error_info(code, desc_upload_ftp_failed.ToStdString(), info); + m_plater->update_print_error_info(code, desc_upload_ftp_failed, info); } else { - m_plater->update_print_error_info(code, desc_network_error.ToStdString(), info); + m_plater->update_print_error_info(code, desc_network_error, info); } } else { - this->update_status(curr_percent, msg); + ctl.update_status(curr_percent, msg); } }; - auto cancel_fn = [this]() { - return was_canceled(); + auto cancel_fn = [&ctl]() { + return ctl.was_canceled(); }; @@ -317,7 +311,7 @@ void SendJob::process() && this->has_sdcard) { // try to send local with record BOOST_LOG_TRIVIAL(info) << "send_job: try to send gcode to printer"; - this->update_status(curr_percent, _L("Sending gcode file over LAN")); + ctl.update_status(curr_percent, _u8L("Sending gcode file over LAN")); result = m_agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); if (result == BAMBU_NETWORK_ERR_FTP_UPLOAD_FAILED) { params.comments = "upload_failed"; @@ -327,24 +321,24 @@ void SendJob::process() if (result < 0) { // try to send with cloud BOOST_LOG_TRIVIAL(info) << "send_job: try to send gcode file to printer"; - this->update_status(curr_percent, _L("Sending gcode file over LAN")); + ctl.update_status(curr_percent, _u8L("Sending gcode file over LAN")); } } else { BOOST_LOG_TRIVIAL(info) << "send_job: try to send gcode file to printer"; - this->update_status(curr_percent, _L("Sending gcode file over LAN")); + ctl.update_status(curr_percent, _u8L("Sending gcode file over LAN")); } } else { if (this->has_sdcard) { - this->update_status(curr_percent, _L("Sending gcode file over LAN")); + ctl.update_status(curr_percent, _u8L("Sending gcode file over LAN")); result = m_agent->start_send_gcode_to_sdcard(params, update_fn, cancel_fn, nullptr); } else { - this->update_status(curr_percent, _L("An SD card needs to be inserted before sending to printer.")); + ctl.update_status(curr_percent, _u8L("An SD card needs to be inserted before sending to printer.")); return; } } - if (was_canceled()) { - update_status(curr_percent, printjob_cancel_str); + if (ctl.was_canceled()) { + ctl.update_status(curr_percent, printjob_cancel_str); return; } @@ -374,7 +368,7 @@ void SendJob::process() } if (result != BAMBU_NETWORK_ERR_CANCELED) { - this->show_error_info(msg_text, 0, "", ""); + ctl.show_error_info(msg_text, 0, "", ""); } BOOST_LOG_TRIVIAL(error) << "send_job: failed, result = " << result; @@ -404,10 +398,18 @@ void SendJob::on_check_ip_address_success(std::function func) } -void SendJob::finalize() { - if (was_canceled()) return; +void SendJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + try { + if (eptr) + std::rethrow_exception(eptr); + eptr = nullptr; + } catch (...) { + eptr = std::current_exception(); + } - Job::finalize(); + if (canceled || eptr) + return; } void SendJob::set_project_name(std::string name) diff --git a/src/slic3r/GUI/Jobs/SendJob.hpp b/src/slic3r/GUI/Jobs/SendJob.hpp index 179d4f947..15463aa67 100644 --- a/src/slic3r/GUI/Jobs/SendJob.hpp +++ b/src/slic3r/GUI/Jobs/SendJob.hpp @@ -3,7 +3,7 @@ #include #include -#include "PlaterJob.hpp" +#include "Job.hpp" #include "PrintJob.hpp" namespace fs = boost::filesystem; @@ -11,10 +11,12 @@ namespace fs = boost::filesystem; namespace Slic3r { namespace GUI { +class Plater; + typedef std::function OnUpdateStatusFn; typedef std::function WasCancelledFn; -class SendJob : public PlaterJob +class SendJob : public Job { PrintPrepareData job_data; std::string m_dev_id; @@ -25,14 +27,11 @@ class SendJob : public PlaterJob std::function m_success_fun{nullptr}; std::function m_enter_ip_address_fun_fail{nullptr}; std::function m_enter_ip_address_fun_success{nullptr}; + Plater *m_plater; -protected: - - void prepare() override; - - void on_exception(const std::exception_ptr &) override; public: - SendJob(std::shared_ptr pri, Plater *plater, std::string dev_id = ""); + void prepare(); + SendJob(std::string dev_id = ""); std::string m_project_name; std::string m_dev_ip; @@ -49,7 +48,7 @@ public: wxWindow* m_parent{nullptr}; - int status_range() const override + int status_range() const { return 100; } @@ -58,11 +57,11 @@ public: void set_check_mode() {m_is_check_mode = true;}; void check_and_continue() {m_check_and_continue = true;}; bool is_finished() { return m_job_finished; } - void process() override; + void process(Ctl &ctl) override; void on_success(std::function success); void on_check_ip_address_fail(std::function func); void on_check_ip_address_success(std::function func); - void finalize() override; + void finalize(bool canceled, std::exception_ptr &) override; void set_project_name(std::string name); }; diff --git a/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp b/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp new file mode 100644 index 000000000..94c63a0fd --- /dev/null +++ b/src/slic3r/GUI/Jobs/ThreadSafeQueue.hpp @@ -0,0 +1,128 @@ +///|/ Copyright (c) Prusa Research 2021 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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/UpgradeNetworkJob.cpp b/src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp index 000edf854..1a17e762f 100644 --- a/src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp +++ b/src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp @@ -13,39 +13,28 @@ wxDEFINE_EVENT(EVT_DOWNLOAD_NETWORK_FAILED, wxCommandEvent); wxDEFINE_EVENT(EVT_INSTALL_NETWORK_FAILED, wxCommandEvent); -UpgradeNetworkJob::UpgradeNetworkJob(std::shared_ptr pri) - : Job{std::move(pri)} +UpgradeNetworkJob::UpgradeNetworkJob() { name = "plugins"; package_name = "networking_plugins.zip"; } -void UpgradeNetworkJob::on_exception(const std::exception_ptr &eptr) -{ - try { - if (eptr) - std::rethrow_exception(eptr); - } catch (std::exception &e) { - UpgradeNetworkJob::on_exception(eptr); - } -} - void UpgradeNetworkJob::on_success(std::function success) { m_success_fun = success; } -void UpgradeNetworkJob::update_status(int st, const wxString &msg) +void UpgradeNetworkJob::update_status(Ctl &ctl, int st, const std::string &msg) { BOOST_LOG_TRIVIAL(info) << "UpgradeNetworkJob: percent = " << st << "msg = " << msg; - GUI::Job::update_status(st, msg); + ctl.update_status(st, msg); wxCommandEvent event(EVT_UPGRADE_UPDATE_MESSAGE); event.SetString(msg); event.SetEventObject(m_event_handle); wxPostEvent(m_event_handle, event); } -void UpgradeNetworkJob::process() +void UpgradeNetworkJob::process(Ctl &ctl) { // downloading int result = 0; @@ -64,24 +53,24 @@ void UpgradeNetworkJob::process() BOOST_LOG_TRIVIAL(info) << "UpgradeNetworkJob: save netowrk_plugin to " << tmp_path.string(); - auto cancel_fn = [this]() { - return was_canceled(); + auto cancel_fn = [&ctl]() { + return ctl.was_canceled(); }; int curr_percent = 0; result = wxGetApp().download_plugin(name, package_name, - [this, &curr_percent](int state, int percent, bool &cancel) { + [this, &ctl, &curr_percent](int state, int percent, bool &cancel) { if (state == InstallStatusNormal) { - update_status(percent, _L("Downloading")); + update_status(ctl, percent, _u8L("Downloading")); } else if (state == InstallStatusDownloadFailed) { - update_status(percent, _L("Download failed")); + update_status(ctl, percent, _u8L("Download failed")); } else { - update_status(percent, _L("Downloading")); + update_status(ctl, percent, _u8L("Downloading")); } curr_percent = percent; }, cancel_fn); - if (was_canceled()) { - update_status(0, _L("Cancelled")); + if (ctl.was_canceled()) { + update_status(ctl, 0, _u8L("Cancelled")); wxCommandEvent event(wxEVT_CLOSE_WINDOW); event.SetEventObject(m_event_handle); wxPostEvent(m_event_handle, event); @@ -89,7 +78,7 @@ void UpgradeNetworkJob::process() } if (result < 0) { - update_status(0, _L("Download failed")); + update_status(ctl, 0, _u8L("Download failed")); wxCommandEvent event(EVT_DOWNLOAD_NETWORK_FAILED); event.SetEventObject(m_event_handle); wxPostEvent(m_event_handle, event); @@ -98,16 +87,16 @@ void UpgradeNetworkJob::process() result = wxGetApp().install_plugin( name, package_name, - [this](int state, int percent, bool &cancel) { + [this, &ctl](int state, int percent, bool &cancel) { if (state == InstallStatusInstallCompleted) { - update_status(percent, _L("Install successfully.")); + update_status(ctl, percent, _u8L("Install successfully.")); } else { - update_status(percent, _L("Installing")); + update_status(ctl, percent, _u8L("Installing")); } }, cancel_fn); - if (was_canceled()) { - update_status(0, _L("Cancelled")); + if (ctl.was_canceled()) { + update_status(ctl, 0, _u8L("Cancelled")); wxCommandEvent event(wxEVT_CLOSE_WINDOW); event.SetEventObject(m_event_handle); wxPostEvent(m_event_handle, event); @@ -115,7 +104,7 @@ void UpgradeNetworkJob::process() } if (result != 0) { - update_status(0, _L("Install failed")); + update_status(ctl, 0, _u8L("Install failed")); wxCommandEvent event(EVT_INSTALL_NETWORK_FAILED); event.SetEventObject(m_event_handle); wxPostEvent(m_event_handle, event); @@ -129,11 +118,18 @@ void UpgradeNetworkJob::process() return; } -void UpgradeNetworkJob::finalize() +void UpgradeNetworkJob::finalize(bool canceled, std::exception_ptr &eptr) { - if (was_canceled()) return; + try { + if (eptr) + std::rethrow_exception(eptr); + eptr = nullptr; + } catch (...) { + eptr = std::current_exception(); + } - Job::finalize(); + if (canceled || eptr) + return; } void UpgradeNetworkJob::set_event_handle(wxWindow *hanle) diff --git a/src/slic3r/GUI/Jobs/UpgradeNetworkJob.hpp b/src/slic3r/GUI/Jobs/UpgradeNetworkJob.hpp index 20bbf2b4f..0af597d0f 100644 --- a/src/slic3r/GUI/Jobs/UpgradeNetworkJob.hpp +++ b/src/slic3r/GUI/Jobs/UpgradeNetworkJob.hpp @@ -32,11 +32,10 @@ protected: std::string name; std::string package_name; - void on_exception(const std::exception_ptr &) override; public: - UpgradeNetworkJob(std::shared_ptr pri); + UpgradeNetworkJob(); - int status_range() const override + int status_range() const { return 100; } @@ -44,9 +43,9 @@ public: bool is_finished() { return m_job_finished; } void on_success(std::function success); - void update_status(int st, const wxString &msg); - void process() override; - void finalize() override; + void update_status(Ctl &ctl, int st, const std::string &msg); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &e) override; void set_event_handle(wxWindow* hanle); }; diff --git a/src/slic3r/GUI/Jobs/Worker.hpp b/src/slic3r/GUI/Jobs/Worker.hpp new file mode 100644 index 000000000..69ec6f0b8 --- /dev/null +++ b/src/slic3r/GUI/Jobs/Worker.hpp @@ -0,0 +1,123 @@ +///|/ Copyright (c) Prusa Research 2021 Tomáš Mészáros @tamasmeszaros +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef PRUSALSICER_WORKER_HPP +#define PRUSALSICER_WORKER_HPP + +#include + +#include "Job.hpp" + +namespace Slic3r { namespace GUI { + +// 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. +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: Job { + 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 74aa65ae3..c8dad01b4 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1,3 +1,14 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, David Kocík @kocikdav, Vojtěch Bubník @bubnikv, Tomáš Mészáros @tamasmeszaros, Enrico Turri @enricoturri1966, Filip Sykala @Jony01, Lukáš Hejl @hejllukas, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2021 Jason Scurtu @xarbit +///|/ Copyright (c) 2019 John Drake @foxox +///|/ +///|/ ported from lib/Slic3r/GUI/MainFrame.pm: +///|/ Copyright (c) Prusa Research 2016 - 2019 Vojtěch Bubník @bubnikv, Vojtěch Král @vojtechkral, Oleksandra Iushchenko @YuSanka, Tomáš Mészáros @tamasmeszaros, Enrico Turri @enricoturri1966 +///|/ Copyright (c) Slic3r 2014 - 2016 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2014 Mark Hindess +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "MainFrame.hpp" #include @@ -811,7 +822,7 @@ void MainFrame::shutdown() #endif // _WIN32 if (m_plater != nullptr) { - m_plater->stop_jobs(); + m_plater->get_ui_job_worker().cancel_all(); // Unbinding of wxWidgets event handling in canvases needs to be done here because on MAC, // when closing the application using Command+Q, a mouse event is triggered after this lambda is completed, diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index f5f6e35b8..42bf891da 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -357,13 +357,13 @@ void MediaPlayCtrl::ToggleStream() DownloadProgressDialog2(MediaPlayCtrl *ctrl) : DownloadProgressDialog(_L("Downloading Virtual Camera Tools")), ctrl(ctrl) {} struct UpgradeNetworkJob2 : UpgradeNetworkJob { - UpgradeNetworkJob2(std::shared_ptr pri) : UpgradeNetworkJob(pri) { + UpgradeNetworkJob2() { name = "cameratools"; package_name = "camera_tools.zip"; } }; - std::shared_ptr make_job(std::shared_ptr pri) override - { return std::make_shared(pri); } + std::unique_ptr make_job() override + { return std::make_unique(); } void on_finish() override { ctrl->CallAfter([ctrl = this->ctrl] { ctrl->ToggleStream(); }); diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 4e0e8a009..011c77128 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -489,7 +489,7 @@ std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transfo { std::vector out; - const Transform3d& instance_matrix_no_translation_no_scaling = trafo.get_matrix(true,false,true); + const Transform3d instance_matrix_no_translation_no_scaling = trafo.get_rotation_matrix(); Vec3d direction_to_camera = -camera.get_dir_forward(); Vec3d direction_to_camera_mesh = (instance_matrix_no_translation_no_scaling.inverse() * direction_to_camera).normalized().eval(); direction_to_camera_mesh = direction_to_camera_mesh.cwiseProduct(trafo.get_scaling_factor()); diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 81a5511cd..390cae058 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, 0.588, 0.533, 1); @@ -158,17 +156,32 @@ 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, 0.588, 0.533, 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; @@ -731,6 +744,7 @@ void NotificationManager::PopNotification::render_hypertext(ImGuiWrapper& imgui, void NotificationManager::PopNotification::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(); ImVec2 win_size(win_size_x, win_size_y); ImVec2 win_pos(win_pos_x, win_pos_y); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); @@ -856,6 +870,7 @@ void NotificationManager::PopNotification::bbl_render_block_notif_left_sign(ImGu 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); @@ -887,6 +902,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); diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 91211a661..68e26f4a0 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2020 - 2023 David Kocík @kocikdav, Lukáš Matěna @lukasmatena, Pavel Mikuš @Godrak, Filip Sykala @Jony01, Vojtěch Bubník @bubnikv, Tomáš Mészáros @tamasmeszaros, Lukáš Hejl @hejllukas, Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_GUI_NotificationManager_hpp_ #define slic3r_GUI_NotificationManager_hpp_ @@ -119,6 +123,8 @@ enum class NotificationType // Give user advice to simplify object with big amount of triangles // Contains ObjectID for closing when object is deleted SimplifySuggestion, + // Change of text will change font to similar one on. + UnknownFont, // information about netfabb is finished repairing model (blocking proccess) NetfabbFinished, // Short meesage to fill space between start and finish of export @@ -462,7 +468,10 @@ private: // 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. @@ -492,6 +501,7 @@ private: ImVec4 m_CurrentColor; float m_WindowRadius; + bool m_WindowRadius_inited = false; void use_bbl_theme(); void restore_default_theme(); diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 07c4e380d..b0570e456 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -1,3 +1,8 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966, Lukáš Hejl @hejllukas, David Kocík @kocikdav, Tomáš Mészáros @tamasmeszaros, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2020 Gianni Ceccarelli @dakkar +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #include "ObjectDataViewModel.hpp" #include "wxExtensions.hpp" #include "BitmapCache.hpp" @@ -12,12 +17,23 @@ #include #include + namespace Slic3r { + namespace GUI { + wxDEFINE_EVENT(wxCUSTOMEVT_LAST_VOLUME_IS_DELETED, wxCommandEvent); BitmapCache* m_bitmap_cache = nullptr; +wxBitmapBundle* find_bndl(const std::string& bmp_name) +{ + if (!m_bitmap_cache) + m_bitmap_cache = new BitmapCache; + + return m_bitmap_cache->find_bndl(bmp_name); +} + // ***************************************************************************** // ---------------------------------------------------------------------------- // ObjectDataViewModelNode @@ -72,19 +88,19 @@ const std::map INFO_ITEMS{ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, - const wxBitmapBundle& bmp, + const bool is_text_volume, + const bool is_svg_volume, const wxString& extruder, - const int idx/* = -1*/, - const std::string& warning_icon_name /*= std::string*/) : + const int idx/* = -1*/) : m_parent(parent), m_name(sub_obj_name), m_type(itVolume), m_volume_type(type), + m_is_text_volume(is_text_volume), + 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) + m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : "") { - m_bmp = bmp; set_icons(); init_container(); } @@ -135,7 +151,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const t_layer_height_range& layer_range, const int idx /*= -1 */, - const wxString extruder) : + const wxString& extruder) : m_parent(parent), m_type(itLayer), m_idx(idx), @@ -271,7 +287,7 @@ void ObjectDataViewModelNode::update_settings_digest_bitmaps() std::string scaled_bitmap_name = m_name.ToUTF8().data(); scaled_bitmap_name += (wxGetApp().dark_mode() ? "-dm" : ""); - wxBitmapBundle *bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); + wxBitmapBundle *bmp = find_bndl(scaled_bitmap_name); if (bmp == nullptr) { std::vector bmps; for (auto& category : m_opt_categories) @@ -432,6 +448,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 = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = *get_bmp_bundle(LockIcon); @@ -461,11 +479,6 @@ void ObjectDataViewModel::Init() AddOutsidePlate(); } -wxBitmapBundle& ObjectDataViewModel::GetWarningBitmap(const std::string& warning_icon_name) -{ - return warning_icon_name.empty() ? m_empty_bmp : warning_icon_name == WarningIcon ? m_warning_bmp : m_warning_manifold_bmp; -} - wxDataViewItem ObjectDataViewModel::AddPlate(PartPlate* part_plate, wxString name, bool refresh) { int plate_idx = part_plate ? part_plate->get_index() : -1; @@ -524,19 +537,23 @@ 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; } std::string scaled_bitmap_name = std::string(); if (node->has_warning_icon()) scaled_bitmap_name += node->warning_icon_name(); - if (node->has_lock()) + if (node->has_lock()) scaled_bitmap_name += LockIcon; if (is_volume_node) scaled_bitmap_name += std::to_string(vol_type); + scaled_bitmap_name += (wxGetApp().dark_mode() ? "-dm" : "-lm"); - wxBitmapBundle *bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); + wxBitmapBundle* bmp = find_bndl(scaled_bitmap_name); if (!bmp) { std::vector bmps; if (node->has_warning_icon()) @@ -544,15 +561,19 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode *node) if (node->has_lock()) bmps.emplace_back(&m_lock_bmp); if (is_volume_node) - bmps.emplace_back(m_volume_bmps[vol_type]); + 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_bndl(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); } @@ -578,8 +599,8 @@ wxDataViewItem ObjectDataViewModel::AddObject(ModelObject *model_object, std::st //const wxString extruder_str = extruder == 0 ? _(L("default")) : wxString::Format("%d", extruder); 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); + // Add warning icon if detected auto-repaire + UpdateBitmapForNode(obj_node, warning_bitmap, has_lock); if (plate_node != nullptr) { obj_node->m_parent = plate_node; @@ -605,6 +626,8 @@ 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_text_volume, + const bool is_svg_volume, const std::string& warning_icon_name/* = std::string()*/, const int extruder/* = 0*/, const bool create_frst_child/* = true*/) @@ -620,7 +643,8 @@ 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_text_volume, is_svg_volume, root->m_extruder, 0); + UpdateBitmapForNode(node, root->m_warning_icon_name, root->has_lock()); insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); // notify control @@ -643,14 +667,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), - extruder_str, root->m_volumes_cnt, warning_icon_name); + const auto node = new ObjectDataViewModelNode(root, name, volume_type, is_text_volume, is_svg_volume, extruder_str, root->m_volumes_cnt); + 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) ) - root->SetWarningBitmap(GetWarningBitmap(warning_icon_name), warning_icon_name); + (root->m_warning_icon_name.empty() || root->m_warning_icon_name == WarningManifoldIcon) ) { + root->SetWarningIconName(warning_icon_name); + UpdateBitmapForNode(root); + } // notify control const wxDataViewItem child((void*)node); @@ -2297,6 +2323,8 @@ void ObjectDataViewModel::SetSinkState(const bool painted, wxDataViewItem obj_it void ObjectDataViewModel::UpdateBitmaps() { 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 = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = *get_bmp_bundle(LockIcon); @@ -2318,10 +2346,8 @@ void ObjectDataViewModel::UpdateBitmaps() 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 = *get_bmp_bundle(LayerRootIcon); @@ -2339,27 +2365,6 @@ void ObjectDataViewModel::UpdateBitmaps() } } -wxBitmapBundle ObjectDataViewModel::GetVolumeIcon(const Slic3r::ModelVolumeType vol_type, const std::string& warning_icon_name/* = std::string()*/) -{ - if (warning_icon_name.empty()) - return *m_volume_bmps[static_cast(vol_type)]; - - std::string scaled_bitmap_name = warning_icon_name + std::to_string(static_cast(vol_type)); - scaled_bitmap_name += "-em" + std::to_string(wxGetApp().em_unit()) + (wxGetApp().dark_mode() ? "-dm" : "-lm"); - - wxBitmapBundle *bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); - if (bmp == nullptr) { - std::vector bmps; - - bmps.emplace_back(&GetWarningBitmap(warning_icon_name)); - bmps.emplace_back(m_volume_bmps[static_cast(vol_type)]); - - bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); - } - - return *bmp; -} - void ObjectDataViewModel::AddWarningIcon(const wxDataViewItem& item, const std::string& warning_icon_name) { if (!item.IsOk()) @@ -2367,13 +2372,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; } } @@ -2388,12 +2394,9 @@ void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bo if (!node->GetBitmap().IsOk() || !(node->GetType() & (itVolume | itObject))) return; - if (node->GetType() & itVolume) { - node->SetWarningBitmap(*m_volume_bmps[static_cast(node->volume_type())], ""); - return; - } + node->SetWarningIconName(std::string()); + UpdateBitmapForNode(node); - node->SetWarningBitmap(wxNullBitmap, ""); if (unmark_object) { wxDataViewItemArray children; diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 1b3d6f601..a4a2c5c4e 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -1,3 +1,7 @@ +///|/ Copyright (c) Prusa Research 2018 - 2023 Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Lukáš Hejl @hejllukas, Enrico Turri @enricoturri1966, David Kocík @kocikdav, Vojtěch Bubník @bubnikv, Tomáš Mészáros @tamasmeszaros, Vojtěch Král @vojtechkral +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ #ifndef slic3r_GUI_ObjectDataViewModel_hpp_ #define slic3r_GUI_ObjectDataViewModel_hpp_ @@ -97,6 +101,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 @@ -127,15 +133,15 @@ public: ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, - const wxBitmapBundle& bmp, + const bool is_text_volume, + const bool is_svg_volume, const wxString& extruder, - const int idx = -1, - const std::string& warning_icon_name = std::string()); + const int idx = -1 ); ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const t_layer_height_range& layer_range, const int idx = -1, - const wxString extruder = "" ); + const wxString& extruder = wxEmptyString ); ObjectDataViewModelNode(PartPlate* part_plate, wxString name); @@ -225,9 +231,9 @@ public: void SetVolumeType(ModelVolumeType type) { m_volume_type = type; } void SetBitmap(const wxBitmapBundle &icon) { m_bmp = icon; } void SetExtruder(const wxString &extruder) { m_extruder = extruder; } - void SetWarningBitmap(const wxBitmapBundle& 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 wxBitmapBundle& GetBitmap() const { return m_bmp; } + void SetWarningIconName(const std::string& warning_icon_name) { m_warning_icon_name = warning_icon_name; } + void SetLock(bool has_lock) { m_has_lock = has_lock; } + const wxBitmapBundle& GetBitmap() const { return m_bmp; } const wxString& GetName() const { return m_name; } ItemType GetType() const { return m_type; } InfoItemType GetInfoItemType() const { return m_info_item_type; } @@ -293,6 +299,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 sys_color_changed(); #ifndef NDEBUG @@ -320,6 +328,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; wxBitmapBundle m_empty_bmp; wxBitmapBundle m_warning_bmp; @@ -343,6 +353,8 @@ public: wxDataViewItem AddVolumeChild( const wxDataViewItem &parent_item, const wxString &name, const Slic3r::ModelVolumeType volume_type, + const bool is_text_volume, + const bool is_svg_volume, const std::string& warning_icon_name = std::string(), const int extruder = 0, const bool create_frst_child = true); @@ -469,8 +481,6 @@ public: // Rescale bitmaps for existing Items void UpdateBitmaps(); - wxBitmapBundle GetVolumeIcon(const Slic3r::ModelVolumeType vol_type, - const std::string& warning_icon_name = std::string()); void AddWarningIcon(const wxDataViewItem& item, const std::string& warning_name); void DeleteWarningIcon(const wxDataViewItem& item, const bool unmark_object = false); void UpdateWarningIcon(const wxDataViewItem& item, const std::string& warning_name); @@ -500,12 +510,11 @@ private: wxDataViewItem AddInstanceRoot(const wxDataViewItem& parent_item); void AddAllChildren(const wxDataViewItem& parent); - wxBitmapBundle& GetWarningBitmap(const std::string& warning_icon_name); void ReparentObject(ObjectDataViewModelNode* plate, ObjectDataViewModelNode* object); 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 23e06faf0..bb0061392 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -108,8 +108,11 @@ #include "Jobs/FillBedJob.hpp" #include "Jobs/RotoptimizeJob.hpp" #include "Jobs/SLAImportJob.hpp" +#include "Jobs/SLAImportDialog.hpp" #include "Jobs/PrintJob.hpp" #include "Jobs/NotificationProgressIndicator.hpp" +#include "Jobs/PlaterWorker.hpp" +#include "Jobs/BoostThreadWorker.hpp" #include "BackgroundSlicingProcess.hpp" #include "SelectMachine.hpp" #include "SendToPrinter.hpp" @@ -128,6 +131,7 @@ #include "MsgDialog.hpp" #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification +#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GizmoObjectManipulation.hpp" // BBS @@ -1897,39 +1901,38 @@ void Sidebar::can_search() class PlaterDropTarget : public wxFileDropTarget { public: - PlaterDropTarget(Plater* plater) : m_plater(plater) { this->SetDefaultAction(wxDragCopy); } + PlaterDropTarget(MainFrame& mainframe, Plater& plater) : m_mainframe(mainframe), m_plater(plater) { + this->SetDefaultAction(wxDragCopy); + } virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames); - void handleOnIdle(wxIdleEvent & event); - private: - Plater* m_plater; - wxArrayString m_filenames; + MainFrame& m_mainframe; + Plater& m_plater; }; -bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) +namespace { +bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position) { -#ifdef WIN32 - // hides the system icon - this->MSWUpdateDragImageOnLeave(); -#endif // WIN32 + std::string svg_file_str = into_u8(svg_file); + GLCanvas3D* canvas = plater.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; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": drag %1% files into app")%filenames.size(); - m_filenames = filenames; - wxGetApp().Bind(wxEVT_IDLE, &PlaterDropTarget::handleOnIdle, this); - return true; + // Refresh hover state to find surface point under mouse + wxMouseEvent evt(wxEVT_MOTION); + 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); } - -void PlaterDropTarget::handleOnIdle(wxIdleEvent &event) -{ - wxGetApp().mainframe->Raise(); - wxGetApp().Unbind(wxEVT_IDLE, &PlaterDropTarget::handleOnIdle, this); - if (m_plater != nullptr) { - m_plater->load_files(m_filenames); - wxGetApp().mainframe->update_title(); - } - //m_filenames.clear(); } // State to manage showing after export notifications and device ejecting @@ -2030,70 +2033,15 @@ struct Plater::priv BackgroundSlicingProcess background_process; bool suppressed_backround_processing_update { false }; - // 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 - // can't be restarted while a ui job is still running. - class Jobs: public ExclusiveJobGroup - { - priv *m; - size_t m_arrange_id, m_fill_bed_id, m_rotoptimize_id, m_sla_import_id, m_orient_id; - std::shared_ptr m_pri; - //BBS - size_t m_print_id; - - void before_start() override { m->background_process.stop(); } - - public: - Jobs(priv *_m) : - m(_m), - m_pri{std::make_shared(m->notification_manager.get())} - { - m_arrange_id = add_job(std::make_unique(m_pri, m->q)); - m_orient_id = add_job(std::make_unique(m_pri, m->q)); - m_fill_bed_id = add_job(std::make_unique(m_pri, m->q)); - m_rotoptimize_id = add_job(std::make_unique(m_pri, m->q)); - m_sla_import_id = add_job(std::make_unique(m_pri, m->q)); - //BBS add print id - m_print_id = add_job(std::make_unique(m_pri, m->q)); - } - - void arrange() - { - m->take_snapshot("Arrange"); - start(m_arrange_id); - } - - void orient() - { - m->take_snapshot("Orient"); - start(m_orient_id); - } - - void fill_bed() - { - m->take_snapshot("Fill bed"); - start(m_fill_bed_id); - } - - void optimize_rotation() - { - m->take_snapshot("Optimize Rotation"); - start(m_rotoptimize_id); - } - - void import_sla_arch() - { - m->take_snapshot("Import SLA archive"); - start(m_sla_import_id); - } - - //BBS bbl printing job - void print() - { - start(m_print_id); - } - } m_ui_jobs; + // TODO: A mechanism would be useful for blocking the plater interactions: + // objects would be frozen for the user. In case of arrange, an animation + // could be shown, or with the optimize orientations, partial results + // could be displayed. + // + // UIThreadWorker can be used as a replacement for BoostThreadWorker if + // no additional worker threads are desired (useful for debugging or profiling) + PlaterWorker m_worker; + SLAImportDialog * m_sla_import_dlg; int m_job_prepare_state; @@ -2375,6 +2323,7 @@ struct Plater::priv void on_modify_filament(SimpleEvent &); void on_object_select(SimpleEvent&); + void show_right_click_menu(Vec2d mouse_position, wxMenu *menu); void on_right_click(RBtnEvent&); //BBS: add model repair void on_repair_model(wxCommandEvent &event); @@ -2424,7 +2373,6 @@ struct Plater::priv bool can_delete() const; bool can_delete_all() const; - bool can_edit_text() const; bool can_add_plate() const; bool can_delete_plate() const; bool can_increase_instances() const; @@ -2538,6 +2486,37 @@ const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::ica const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase); const std::regex Plater::priv::pattern_prusa(".*bbl", std::regex::icase); +bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) +{ +#ifdef WIN32 + // hides the system icon + this->MSWUpdateDragImageOnLeave(); +#endif // WIN32 + + m_mainframe.Raise(); + m_mainframe.select_tab(size_t(MainFrame::tp3DEditor)); + if (wxGetApp().is_editor()) + m_plater.select_view_3D("3D"); + + // 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 = m_plater.GetPosition() + m_plater.p->current_panel->GetPosition(); + Vec2d mouse_position(x - offset.x, y - offset.y); + // Scale for retina displays + const GLCanvas3D *canvas = m_plater.canvas3D(); + canvas->apply_retina_scale(mouse_position); + return emboss_svg(m_plater, filename, mouse_position); + } + } + bool res = m_plater.load_files(filenames); + m_mainframe.update_title(); + return res; +} + Plater::priv::priv(Plater *q, MainFrame *main_frame) : q(q) , main_frame(main_frame) @@ -2558,7 +2537,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) })) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) - , m_ui_jobs(this) + , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} + , m_sla_import_dlg{new SLAImportDialog{q}} , m_job_prepare_state(Job::JobPrepareState::PREPARE_STATE_DEFAULT) , delayed_scene_refresh(false) , collapse_toolbar(GLToolbar::Normal, "Collapse") @@ -2873,7 +2853,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) } // Drop target: - q->SetDropTarget(new PlaterDropTarget(q)); // if my understanding is right, wxWindow takes the owenership + q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership q->Layout(); apply_color_mode(); @@ -4515,7 +4495,7 @@ void Plater::priv::remove(size_t obj_idx) if (view3D->is_layers_editing_enabled()) view3D->enable_layers_editing(false); - m_ui_jobs.cancel_all(); + m_worker.cancel_all(); model.delete_object(obj_idx); //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); @@ -4546,7 +4526,7 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia if (!obj->name.empty()) snapshot_label += ": " + obj->name; Plater::TakeSnapshot snapshot(q, snapshot_label); - m_ui_jobs.cancel_all(); + m_worker.cancel_all(); if (obj->is_cut()) sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); @@ -4576,7 +4556,7 @@ void Plater::priv::delete_all_objects_from_model() view3D->get_canvas3d()->reset_sequential_print_clearance(); - m_ui_jobs.cancel_all(); + m_worker.cancel_all(); // Stop and reset the Print content. background_process.reset(); @@ -4616,7 +4596,7 @@ void Plater::priv::reset(bool apply_presets_change) view3D->get_canvas3d()->reset_sequential_print_clearance(); - m_ui_jobs.cancel_all(); + m_worker.cancel_all(); //BBS: clear the partplate list's object before object cleared partplate_list.reinit(); @@ -5037,7 +5017,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // Restart background processing thread based on a bitmask of UpdateBackgroundProcessReturnState. bool Plater::priv::restart_background_process(unsigned int state) { - if (m_ui_jobs.is_any_running()) { + if (!m_worker.is_idle()) { // Avoid a race condition BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: ui jobs running, return false")%__LINE__; return false; @@ -5237,7 +5217,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); new_volume->set_transformation(old_volume->get_transformation()); - new_volume->translate(new_volume->get_transformation().get_matrix(true) * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) new_volume->convert_from_imperial_units(); @@ -5605,8 +5585,8 @@ void Plater::priv::reload_from_disk() Transform3d transform = Transform3d::Identity(); transform.translate(new_volume->source.mesh_offset - old_volume->source.mesh_offset); - new_volume->set_transformation(old_volume->get_transformation().get_matrix() * old_volume->source.transform.get_matrix(true) * - transform * new_volume->source.transform.get_matrix(true).inverse()); + new_volume->set_transformation(old_volume->get_transformation().get_matrix() * old_volume->source.transform.get_matrix_no_offset() * + transform * new_volume->source.transform.get_matrix_no_offset().inverse()); new_volume->source.object_idx = old_volume->source.object_idx; new_volume->source.volume_idx = old_volume->source.volume_idx; @@ -5679,7 +5659,7 @@ void Plater::priv::reload_from_disk() new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); new_volume->set_transformation(old_volume->get_transformation()); - new_volume->translate(new_volume->get_transformation().get_matrix(true) * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); new_volume->source.object_idx = old_volume->source.object_idx; new_volume->source.volume_idx = old_volume->source.volume_idx; assert(! old_volume->source.is_converted_from_inches || ! old_volume->source.is_converted_from_meters); @@ -6208,7 +6188,7 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) std::string title_text = _u8L("Slicing"); evt.status.text = title_text + evt.status.text; if (evt.status.percent >= 0) { - if (m_ui_jobs.is_any_running()) { + if (!m_worker.is_idle()) { // Avoid a race condition return; } @@ -6584,7 +6564,7 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) } } } - q->SetDropTarget(new PlaterDropTarget(q)); + q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); } else { @@ -7003,6 +6983,24 @@ static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x y = res_y; } +void Plater::priv::show_right_click_menu(Vec2d mouse_position, wxMenu *menu) +{ + // BBS: GUI refactor: move sidebar to the left + int x, y; + get_position(current_panel, wxGetApp().mainframe, x, y); + wxPoint position(static_cast(mouse_position.x() + x), static_cast(mouse_position.y() + y)); +#ifdef __linux__ + // For some reason on Linux the menu isn't displayed if position is + // specified (even though the position is sane). + position = wxDefaultPosition; +#endif + GLCanvas3D &canvas = *q->canvas3D(); + canvas.apply_retina_scale(mouse_position); + canvas.set_popup_menu_position(mouse_position); + q->PopupMenu(menu, position); + canvas.clear_popup_menu_position(); +} + void Plater::priv::on_right_click(RBtnEvent& evt) { int obj_idx = get_selected_object_idx(); @@ -7048,41 +7046,30 @@ 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_text()) ? menus.text_part_menu() : + (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : + menus.part_menu(); + } else + menu = menus.multi_selection_menu(); } } } if (q != nullptr && menu) { -#ifdef __linux__ - // For some reason on Linux the menu isn't displayed if position is specified - // (even though the position is sane). - q->PopupMenu(menu); -#else - //BBS: GUI refactor: move sidebar to the left - int x, y; - get_position(current_panel, wxGetApp().mainframe, x, y); - q->PopupMenu(menu, (int) evt.data.first.x() + x, (int) evt.data.first.y() + y); - //q->PopupMenu(menu); -#endif + show_right_click_menu(evt.data.first, menu); } } //BBS: add part plate related logic void Plater::priv::on_plate_right_click(RBtnPlateEvent& evt) { - wxMenu* menu = menus.plate_menu(); - -#ifdef __linux__ - q->PopupMenu(menu); -#else - //BBS: GUI refactor: move sidebar to the left - int x, y; - get_position(current_panel, wxGetApp().mainframe, x, y); - q->PopupMenu(menu, (int) evt.data.first.x() + x, (int) evt.data.first.y() + y); - //q->PopupMenu(menu); -#endif + wxMenu *menu = menus.plate_menu(); + show_right_click_menu(evt.data.first, menu); } void Plater::priv::on_update_geometry(Vec3dsEvent<2>&) @@ -7317,7 +7304,11 @@ void Plater::priv::init_notification_manager() void Plater::orient() { - p->m_ui_jobs.orient(); + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Orient")); + replace_job(w, std::make_unique()); + } } //BBS: add job state related functions @@ -7672,24 +7663,6 @@ bool Plater::priv::can_delete_all() const return !model.objects.empty(); } -bool Plater::priv::can_edit_text() const -{ - const Selection &selection = view3D->get_canvas3d()->get_selection(); - if (selection.is_single_full_instance()) - return true; - - if (selection.is_single_volume()) { - const GLVolume *gl_volume = selection.get_first_volume(); - int out_object_idx = gl_volume->object_idx(); - ModelObject * model_object = selection.get_model()->objects[out_object_idx]; - int out_volume_idx = gl_volume->volume_idx(); - ModelVolume * model_volume = model_object->volumes[out_volume_idx]; - if (model_volume) - return !model_volume->get_text_info().m_text.empty(); - } - return false; -} - bool Plater::priv::can_add_plate() const { return q->get_partplate_list().get_plate_count() < PartPlateList::MAX_PLATES_COUNT; @@ -7738,7 +7711,7 @@ bool Plater::priv::can_simplify() const bool Plater::priv::can_increase_instances() const { - if (m_ui_jobs.is_any_running() + if (!m_worker.is_idle() || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; @@ -7750,7 +7723,7 @@ bool Plater::priv::can_increase_instances() const bool Plater::priv::can_decrease_instances() const { - if (m_ui_jobs.is_any_running() + if (!m_worker.is_idle() || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; @@ -7771,7 +7744,7 @@ bool Plater::priv::can_split_to_volumes() const bool Plater::priv::can_arrange() const { - return !model.objects.empty() && !m_ui_jobs.is_any_running(); + return !model.objects.empty() && m_worker.is_idle(); } bool Plater::priv::layers_height_allowed() const @@ -9324,8 +9297,11 @@ void Plater::calib_VFA(const Calib_Params& params) BuildVolume_Type Plater::get_build_volume_type() const { return p->bed.get_build_volume_type(); } void Plater::import_sl1_archive() { - if (!p->m_ui_jobs.is_any_running()) - p->m_ui_jobs.import_sla_arch(); + auto &w = get_ui_job_worker(); + if (w.is_idle() && p->m_sla_import_dlg->ShowModal() == wxID_OK) { + p->take_snapshot(_u8L("Import SLA archive")); + replace_job(w, std::make_unique(p->m_sla_import_dlg)); + } } void Plater::extract_config_from_project() @@ -10150,12 +10126,9 @@ void Plater::update(bool conside_update_flag, bool force_background_processing_u void Plater::object_list_changed() { p->object_list_changed(); } -void Plater::stop_jobs() { p->m_ui_jobs.stop_all(); } +Worker &Plater::get_ui_job_worker() { return p->m_worker; } -bool Plater::is_any_job_running() const -{ - return p->m_ui_jobs.is_any_running(); -} +const Worker &Plater::get_ui_job_worker() const { return p->m_worker; } void Plater::update_ui_from_settings() { p->update_ui_from_settings(); } @@ -10265,7 +10238,7 @@ void Plater::set_selected_visible(bool visible) return; Plater::TakeSnapshot snapshot(this, "Set Selected Objects Visible in AssembleView"); - p->m_ui_jobs.cancel_all(); + get_ui_job_worker().cancel_all(); p->get_current_canvas3D()->set_selected_visible(visible); } @@ -10283,7 +10256,7 @@ void Plater::remove_selected() return; Plater::TakeSnapshot snapshot(this, "Delete Selected Objects"); - p->m_ui_jobs.cancel_all(); + get_ui_job_worker().cancel_all(); //BBS delete current selected // p->view3D->delete_selected(); @@ -10403,8 +10376,11 @@ void Plater::set_number_of_copies(/*size_t num*/) void Plater::fill_bed_with_instances() { - if (!p->m_ui_jobs.is_any_running()) - p->m_ui_jobs.fill_bed(); + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Arrange")); + replace_job(w, std::make_unique()); + } } bool Plater::is_selection_empty() const @@ -10944,6 +10920,110 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls) } }*/ +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; + if (volume->text_configuration.has_value()) + continue; // text dosen't have svg path + + 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); + } +} +} + // BBS: backup int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn) { @@ -10963,6 +11043,10 @@ 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); wxBusyCursor wait; @@ -11217,9 +11301,15 @@ void Plater::reslice() // and notify user that he should leave it first. if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) return; - - // Stop arrange and (or) optimize rotation tasks. - this->stop_jobs(); + + // Stop the running (and queued) UI jobs and only proceed if they actually + // get stopped. + unsigned timeout_ms = 10000; + if (!stop_queue(this->get_ui_job_worker(), timeout_ms)) { + BOOST_LOG_TRIVIAL(error) << "Could not stop UI job within " + << timeout_ms << " milliseconds timeout!"; + return; + } // Orca: regenerate CalibPressureAdvancePattern custom G-code to apply changes if (model().calib_pa_pattern) { @@ -12077,8 +12167,10 @@ GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview) void Plater::arrange() { - if (!p->m_ui_jobs.is_any_running()) { - p->m_ui_jobs.arrange(); + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Arrange")); + replace_job(w, std::make_unique()); } } @@ -12165,22 +12257,35 @@ 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) return; - // recenter and re - align to Z = 0 - p->model.objects[obj_idx]->ensure_on_bed(p->printer_technology != ptSLA); - if (this->p->printer_technology == ptSLA) { - // Update the SLAPrint from the current Model, so that the reload_scene() - // pulls the correct data, update the 3D scene. - this->p->update_restart_background_process(true, false); - } - else - p->view3D->reload_scene(false); - - // update print - this->p->schedule_background_process(); + ModelObject *object = p->model.objects[obj_idx]; + if (object == nullptr) + return; + changed_object(*object); } void Plater::changed_objects(const std::vector& object_idxs) @@ -12234,7 +12339,14 @@ void Plater::center_selection() { p->center_selection(); } void Plater::mirror(Axis axis) { p->mirror(axis); } void Plater::split_object() { p->split_object(); } void Plater::split_volume() { p->split_volume(); } -void Plater::optimize_rotation() { if (!p->m_ui_jobs.is_any_running()) p->m_ui_jobs.optimize_rotation(); } +void Plater::optimize_rotation() +{ + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Optimize Rotation")); + replace_job(w, std::make_unique()); + } +} void Plater::update_menus() { p->menus.update(); } // BBS //void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); } @@ -13140,14 +13252,6 @@ void Plater::show_status_message(std::string s) BOOST_LOG_TRIVIAL(trace) << "show_status_message:" << s; } -void Plater::edit_text() -{ - auto &manager = get_view3D_canvas3D()->get_gizmos_manager(); - manager.open_gizmo(GLGizmosManager::Text); - update(); -} - -bool Plater::can_edit_text() const { return p->can_edit_text(); } bool Plater::can_delete() const { return p->can_delete(); } bool Plater::can_delete_all() const { return p->can_delete_all(); } bool Plater::can_add_model() const { return !is_background_process_slicing(); } @@ -13317,6 +13421,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::text_part_menu() { return p->menus.text_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 e968981e8..681db5670 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -32,6 +32,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" @@ -302,8 +303,41 @@ public: void update(bool conside_update_flag = false, bool force_background_processing_update = false); //BBS void object_list_changed(); - void stop_jobs(); - bool is_any_job_running() const; + + // Get the worker handling the UI jobs (arrange, fill bed, etc...) + // Here is an example of starting up an ad-hoc job: + // queue_job( + // get_ui_job_worker(), + // [](Job::Ctl &ctl) { + // // Executed in the worker thread + // + // CursorSetterRAII cursor_setter{ctl}; + // std::string msg = "Running"; + // + // ctl.update_status(0, msg); + // for (int i = 0; i < 100; i++) { + // usleep(100000); + // if (ctl.was_canceled()) break; + // ctl.update_status(i + 1, msg); + // } + // ctl.update_status(100, msg); + // }, + // [](bool, std::exception_ptr &e) { + // // Executed in UI thread after the work is done + // + // try { + // if (e) std::rethrow_exception(e); + // } catch (std::exception &e) { + // BOOST_LOG_TRIVIAL(error) << e.what(); + // } + // e = nullptr; + // }); + // This would result in quick run of the progress indicator notification + // from 0 to 100. Use replace_job() instead of queue_job() to cancel all + // pending jobs. + Worker& get_ui_job_worker(); + const Worker & get_ui_job_worker() const; + void select_view(const std::string& direction); //BBS: add no_slice logic void select_view_3D(const std::string& name, bool no_slice = true); @@ -392,6 +426,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); @@ -505,10 +540,6 @@ public: //BBS: void fill_color(int extruder_id); - //BBS: - void edit_text(); - bool can_edit_text() const; - bool can_delete() const; bool can_delete_all() const; bool can_add_model() const; @@ -724,6 +755,8 @@ public: wxMenu* plate_menu(); wxMenu* object_menu(); wxMenu* part_menu(); + wxMenu* text_part_menu(); + wxMenu* svg_part_menu(); wxMenu* sla_object_menu(); wxMenu* default_menu(); wxMenu* instance_menu(); @@ -778,6 +811,7 @@ private: void cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes); friend class SuppressBackgroundProcessingUpdate; + friend class PlaterDropTarget; }; class SuppressBackgroundProcessingUpdate diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 05132945c..b94e1bf5c 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -12,6 +12,8 @@ #include "Widgets/RoundedRectangle.hpp" #include "Widgets/StaticBox.hpp" #include "Widgets/WebView.hpp" +#include "Jobs/BoostThreadWorker.hpp" +#include "Jobs/PlaterWorker.hpp" #include #include @@ -1162,6 +1164,7 @@ InputIpAddressDialog::InputIpAddressDialog(wxWindow* parent) m_status_bar = std::make_shared(this); m_status_bar->get_panel()->Hide(); + m_worker = std::make_unique>(this, m_status_bar, "send_worker"); auto m_step_icon_panel1 = new wxWindow(this, wxID_ANY); auto m_step_icon_panel2 = new wxWindow(this, wxID_ANY); @@ -1278,12 +1281,7 @@ InputIpAddressDialog::InputIpAddressDialog(wxWindow* parent) void InputIpAddressDialog::on_cancel() { - if (m_send_job) { - if (m_send_job->is_running()) { - m_send_job->cancel(); - m_send_job->join(); - } - } + m_worker->cancel_all(); if (m_result == 0){ this->EndModal(wxID_YES); }else { @@ -1401,25 +1399,17 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt) m_button_ok->SetBackgroundColor(wxColour(0x90, 0x90, 0x90)); m_button_ok->SetBorderColor(wxColour(0x90, 0x90, 0x90)); - if (m_send_job) { - m_send_job->join(); - } + m_worker->wait_for_idle(); m_status_bar->reset(); m_status_bar->set_prog_block(); m_status_bar->set_cancel_callback_fina([this]() { BOOST_LOG_TRIVIAL(info) << "print_job: enter canceled"; - if (m_send_job) { - if (m_send_job->is_running()) { - BOOST_LOG_TRIVIAL(info) << "send_job: canceled"; - m_send_job->cancel(); - } - m_send_job->join(); - } + m_worker->cancel_all(); }); - m_send_job = std::make_shared(m_status_bar, wxGetApp().plater(), m_obj->dev_id); + auto m_send_job = std::make_unique(m_obj->dev_id); m_send_job->m_dev_ip = ip.ToStdString(); m_send_job->m_access_code = str_access_code.ToStdString(); @@ -1454,7 +1444,7 @@ void InputIpAddressDialog::on_ok(wxMouseEvent& evt) }); - m_send_job->start(); + replace_job(*m_worker, std::move(m_send_job)); } void InputIpAddressDialog::check_ip_address_failed(int result) diff --git a/src/slic3r/GUI/ReleaseNote.hpp b/src/slic3r/GUI/ReleaseNote.hpp index 04863aee6..a6deabdf2 100644 --- a/src/slic3r/GUI/ReleaseNote.hpp +++ b/src/slic3r/GUI/ReleaseNote.hpp @@ -38,6 +38,8 @@ #include #include +#include "Jobs/Worker.hpp" + namespace Slic3r { namespace GUI { wxDECLARE_EVENT(EVT_SECONDARY_CHECK_CONFIRM, wxCommandEvent); @@ -223,8 +225,8 @@ public: wxHyperlinkCtrl* m_trouble_shoot{ nullptr }; bool m_show_access_code{ false }; int m_result; - std::shared_ptr m_send_job{nullptr}; - std::shared_ptr m_status_bar; + std::shared_ptr m_status_bar; + std::unique_ptr m_worker; void on_cancel(); void update_title(wxString title); diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index acb9f980e..0436250a2 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -13,6 +13,8 @@ #include "Widgets/RoundedRectangle.hpp" #include "Widgets/StaticBox.hpp" #include "ConnectPrinter.hpp" +#include "Jobs/BoostThreadWorker.hpp" +#include "Jobs/PlaterWorker.hpp" #include @@ -1247,6 +1249,8 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) m_status_bar = std::make_shared(m_simplebook); m_panel_sending = m_status_bar->get_panel(); m_simplebook->AddPage(m_panel_sending, wxEmptyString, false); + + m_worker = std::make_unique>(this, m_status_bar, "send_worker"); // finish mode m_panel_finish = new wxPanel(m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); @@ -1701,9 +1705,7 @@ void SelectMachineDialog::prepare_mode(bool refresh_button) show_print_failed_info(false); m_is_in_sending_mode = false; - if (m_print_job) { - m_print_job->join(); - } + m_worker->wait_for_idle(); if (wxIsBusy()) wxEndBusyCursor(); @@ -2206,12 +2208,7 @@ void SelectMachineDialog::on_cancel(wxCloseEvent &event) if (m_mapping_popup.IsShown()) m_mapping_popup.Dismiss(); - if (m_print_job) { - if (m_print_job->is_running()) { - m_print_job->cancel(); - m_print_job->join(); - } - } + m_worker->cancel_all(); this->EndModal(wxID_CANCEL); } @@ -2664,13 +2661,7 @@ void SelectMachineDialog::on_send_print() m_status_bar->set_prog_block(); m_status_bar->set_cancel_callback_fina([this]() { BOOST_LOG_TRIVIAL(info) << "print_job: enter canceled"; - if (m_print_job) { - if (m_print_job->is_running()) { - BOOST_LOG_TRIVIAL(info) << "print_job: canceled"; - m_print_job->cancel(); - } - m_print_job->join(); - } + m_worker->cancel_all(); m_is_canceled = true; wxCommandEvent* event = new wxCommandEvent(EVT_PRINT_JOB_CANCEL); wxQueueEvent(this, event); @@ -2738,7 +2729,7 @@ void SelectMachineDialog::on_send_print() } } - m_print_job = std::make_shared(m_status_bar, m_plater, m_printer_last_select); + auto m_print_job = std::make_unique(m_printer_last_select); m_print_job->m_dev_ip = obj_->dev_ip; m_print_job->m_ftp_folder = obj_->get_ftp_folder(); m_print_job->m_access_code = obj_->get_access_code(); @@ -2824,7 +2815,7 @@ void SelectMachineDialog::on_send_print() if (agent) agent->track_update_property("dev_ota_version", obj_->get_ota_version()); - m_print_job->start(); + replace_job(*m_worker, std::move(m_print_job)); BOOST_LOG_TRIVIAL(info) << "print_job: start print job"; } diff --git a/src/slic3r/GUI/SelectMachine.hpp b/src/slic3r/GUI/SelectMachine.hpp index c8668edc6..160c21592 100644 --- a/src/slic3r/GUI/SelectMachine.hpp +++ b/src/slic3r/GUI/SelectMachine.hpp @@ -41,6 +41,8 @@ #include #include +#include "Jobs/Worker.hpp" + namespace Slic3r { namespace GUI { enum PrinterState { @@ -313,6 +315,7 @@ private: std::vector m_filaments; std::vector m_ams_mapping_result; std::shared_ptr m_status_bar; + std::unique_ptr m_worker; Slic3r::DynamicPrintConfig m_required_data_config; Slic3r::Model m_required_data_model; @@ -377,7 +380,6 @@ protected: wxStaticText* m_statictext_finish{nullptr}; TextInput* m_rename_input{nullptr}; wxTimer* m_refresh_timer{ nullptr }; - std::shared_ptr m_print_job; wxScrolledWindow* m_scrollable_view; wxScrolledWindow* m_sw_print_failed_info{nullptr}; wxHyperlinkCtrl* m_hyperlink{nullptr}; diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index aef47ed6d..7b605a58d 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -27,6 +27,10 @@ #include #include +#include +#include +#include + static const Slic3r::ColorRGBA UNIFORM_SCALE_COLOR = Slic3r::ColorRGBA::ORANGE(); static const Slic3r::ColorRGBA SOLID_PLANE_COLOR = {0.0f, 174.0f / 255.0f, 66.0f / 255.0f, 1.0f}; static const Slic3r::ColorRGBA TRANSPARENT_PLANE_COLOR = { 0.8f, 0.8f, 0.8f, 0.5f }; @@ -36,26 +40,19 @@ namespace GUI { Selection::VolumeCache::TransformCache::TransformCache() : position(Vec3d::Zero()) - , rotation(Vec3d::Zero()) - , scaling_factor(Vec3d::Ones()) - , mirror(Vec3d::Ones()) , rotation_matrix(Transform3d::Identity()) , scale_matrix(Transform3d::Identity()) , mirror_matrix(Transform3d::Identity()) - , full_matrix(Transform3d::Identity()) { } Selection::VolumeCache::TransformCache::TransformCache(const Geometry::Transformation& transform) : position(transform.get_offset()) - , rotation(transform.get_rotation()) - , scaling_factor(transform.get_scaling_factor()) - , mirror(transform.get_mirror()) - , full_matrix(transform.get_matrix()) + , transform(transform) { - rotation_matrix = Geometry::assemble_transform(Vec3d::Zero(), rotation); - scale_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scaling_factor); - mirror_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d::Ones(), mirror); + rotation_matrix = transform.get_rotation_matrix(); + scale_matrix = transform.get_scaling_factor_matrix(); + mirror_matrix = transform.get_mirror_matrix(); } Selection::VolumeCache::VolumeCache(const Geometry::Transformation& volume_transform, const Geometry::Transformation& instance_transform) @@ -748,6 +745,17 @@ bool Selection::is_sla_compliant() const return true; } +bool Selection::is_single_text() const +{ + if (!is_single_volume_or_modifier()) + return false; + + const GLVolume* gl_volume = (*m_volumes)[*m_list.begin()]; + const ModelVolume* model_volume = m_model->objects[gl_volume->object_idx()]->volumes[gl_volume->volume_idx()]; + + return model_volume && model_volume->text_configuration.has_value(); +} + bool Selection::contains_all_volumes(const std::vector& volume_idxs) const { for (unsigned int i : volume_idxs) { @@ -828,6 +836,11 @@ 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()) { @@ -844,6 +857,8 @@ const BoundingBoxf3& Selection::get_bounding_box() const const BoundingBoxf3& Selection::get_unscaled_instance_bounding_box() const { + assert(is_single_full_instance()); + if (!m_unscaled_instance_bounding_box.has_value()) { std::optional* bbox = const_cast*>(&m_unscaled_instance_bounding_box); *bbox = BoundingBoxf3(); @@ -852,7 +867,7 @@ const BoundingBoxf3& Selection::get_unscaled_instance_bounding_box() const const GLVolume& volume = *(*m_volumes)[i]; if (volume.is_modifier) continue; - Transform3d trafo = volume.get_instance_transformation().get_matrix(false, false, true, false) * volume.get_volume_transformation().get_matrix(); + Transform3d trafo = volume.get_instance_transformation().get_matrix_no_scaling_factor() * volume.get_volume_transformation().get_matrix(); trafo.translation().z() += volume.get_sla_shift_z(); (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); } @@ -863,6 +878,8 @@ const BoundingBoxf3& Selection::get_unscaled_instance_bounding_box() const const BoundingBoxf3& Selection::get_scaled_instance_bounding_box() const { + assert(is_single_full_instance()); + if (!m_scaled_instance_bounding_box.has_value()) { std::optional* bbox = const_cast*>(&m_scaled_instance_bounding_box); *bbox = BoundingBoxf3(); @@ -871,7 +888,7 @@ const BoundingBoxf3& Selection::get_scaled_instance_bounding_box() const const GLVolume& volume = *(*m_volumes)[i]; if (volume.is_modifier) continue; - Transform3d trafo = volume.get_instance_transformation().get_matrix(false, false, false, false) * volume.get_volume_transformation().get_matrix(); + Transform3d trafo = volume.get_instance_transformation().get_matrix() * volume.get_volume_transformation().get_matrix(); trafo.translation().z() += volume.get_sla_shift_z(); (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); } @@ -880,6 +897,193 @@ const BoundingBoxf3& Selection::get_scaled_instance_bounding_box() const return *m_scaled_instance_bounding_box; } +const BoundingBoxf3& Selection::get_full_unscaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_unscaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_unscaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_instance_transformation().get_matrix_no_scaling_factor() * volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_unscaled_instance_bounding_box; +} + +const BoundingBoxf3& Selection::get_full_scaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_scaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_scaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_instance_transformation().get_matrix() * volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_scaled_instance_bounding_box; +} + +const BoundingBoxf3& Selection::get_full_unscaled_instance_local_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_unscaled_instance_local_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_unscaled_instance_local_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_unscaled_instance_local_bounding_box; +} + +const std::pair& Selection::get_bounding_box_in_current_reference_system() const +{ + static int last_coordinates_type = -1; + + assert(!is_empty()); + + ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (m_mode == Instance && coordinates_type == ECoordinatesType::Local) + coordinates_type = ECoordinatesType::World; + + if (last_coordinates_type != int(coordinates_type)) + const_cast>*>(&m_bounding_box_in_current_reference_system)->reset(); + + if (!m_bounding_box_in_current_reference_system.has_value()) { + last_coordinates_type = int(coordinates_type); + *const_cast>*>(&m_bounding_box_in_current_reference_system) = get_bounding_box_in_reference_system(coordinates_type); + } + + return *m_bounding_box_in_current_reference_system; +} + +std::pair Selection::get_bounding_box_in_reference_system(ECoordinatesType type) const +{ + // + // trafo to current reference system + // + Transform3d trafo; + switch (type) + { + case ECoordinatesType::World: { trafo = Transform3d::Identity(); break; } + case ECoordinatesType::Instance: { trafo = get_first_volume()->get_instance_transformation().get_matrix(); break; } + case ECoordinatesType::Local: { trafo = get_first_volume()->world_matrix(); break; } + } + + // + // trafo basis in world coordinates + // + Geometry::Transformation t(trafo); + t.reset_scaling_factor(); + const Transform3d basis_trafo = t.get_matrix_no_offset(); + std::vector axes = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() }; + for (size_t i = 0; i < axes.size(); ++i) { + axes[i] = basis_trafo * axes[i]; + } + + // + // calculate bounding box aligned to trafo basis + // + Vec3d min = { DBL_MAX, DBL_MAX, DBL_MAX }; + Vec3d max = { -DBL_MAX, -DBL_MAX, -DBL_MAX }; + for (unsigned int id : m_list) { + const GLVolume& vol = *get_volume(id); + const Transform3d vol_world_rafo = vol.world_matrix(); + const TriangleMesh* mesh = vol.convex_hull(); + if (mesh == nullptr) + mesh = &m_model->objects[vol.object_idx()]->volumes[vol.volume_idx()]->mesh(); + assert(mesh != nullptr); + for (const stl_vertex& v : mesh->its.vertices) { + const Vec3d world_v = vol_world_rafo * v.cast(); + for (int i = 0; i < 3; ++i) { + const double i_comp = world_v.dot(axes[i]); + min(i) = std::min(min(i), i_comp); + max(i) = std::max(max(i), i_comp); + } + } + } + + const Vec3d box_size = max - min; + Vec3d half_box_size = 0.5 * box_size; + Geometry::Transformation out_trafo(trafo); + Vec3d center = 0.5 * (min + max); + + // Fix for non centered volume + // by move with calculated center(to volume center) and extend half box size + // e.g. for right aligned embossed text + if (m_list.size() == 1 && + type == ECoordinatesType::Local) { + const GLVolume& vol = *get_volume(*m_list.begin()); + const Transform3d vol_world_trafo = vol.world_matrix(); + Vec3d world_zero = vol_world_trafo * Vec3d::Zero(); + for (size_t i = 0; i < 3; i++){ + // move center to local volume zero + center[i] = world_zero.dot(axes[i]); + // extend half size to bigger distance from center + half_box_size[i] = std::max( + abs(center[i] - min[i]), + abs(center[i] - max[i])); + } + } + + const BoundingBoxf3 out_box(-half_box_size, half_box_size); + out_trafo.set_offset(basis_trafo * center); + return { out_box, out_trafo.get_matrix_no_scaling_factor() }; +} + +const std::pair Selection::get_bounding_sphere() const +{ + if (!m_bounding_sphere.has_value()) { + std::optional>* sphere = const_cast>*>(&m_bounding_sphere); + *sphere = { Vec3d::Zero(), 0.0 }; + + using K = CGAL::Simple_cartesian; + using Traits = CGAL::Min_sphere_of_points_d_traits_3; + using Min_sphere = CGAL::Min_sphere_of_spheres_d; + using Point = K::Point_3; + + std::vector points; + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + const TriangleMesh* hull = volume.convex_hull(); + const indexed_triangle_set& its = (hull != nullptr) ? + hull->its : m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh().its; + const Transform3d& matrix = volume.world_matrix(); + for (const Vec3f& v : its.vertices) { + const Vec3d vv = matrix * v.cast(); + points.push_back(Point(vv.x(), vv.y(), vv.z())); + } + } + + Min_sphere ms(points.begin(), points.end()); + const float* center_x = ms.center_cartesian_begin(); + (*sphere)->first = { *center_x, *(center_x + 1), *(center_x + 2) }; + (*sphere)->second = ms.radius(); + } + } + + return *m_bounding_sphere; +} + void Selection::setup_cache() { if (!m_valid) @@ -921,66 +1125,47 @@ void Selection::move_to_center(const Vec3d& displacement, bool local) this->set_bounding_boxes_dirty(); } -void Selection::translate(const Vec3d& displacement, bool local) +void Selection::translate(const Vec3d& displacement, TransformationType transformation_type) { if (!m_valid) return; - EMode translation_type = m_mode; - //BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": %1%, displacement {%2%, %3%, %4%}") % __LINE__ % displacement(X) % displacement(Y) % displacement(Z); + // Emboss use translate in local coordinate + assert(transformation_type.relative() || + transformation_type.local()); for (unsigned int i : m_list) { GLVolume& v = *(*m_volumes)[i]; - if (v.is_wipe_tower) { - int plate_idx = v.object_idx() - 1000; - BoundingBoxf3 plate_bbox = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_bounding_box(); - Vec3d tower_size = v.bounding_box().size(); - Vec3d tower_origin = m_cache.volumes_data[i].get_volume_position(); - Vec3d actual_displacement = displacement; - const double margin = WIPE_TOWER_MARGIN; - - if (!local) - actual_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; - - if (tower_origin(0) + actual_displacement(0) - margin < plate_bbox.min(0)) { - actual_displacement(0) = plate_bbox.min(0) - tower_origin(0) + margin; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + if (m_mode == Instance && !is_wipe_tower()) { + assert(is_from_fully_selected_instance(i)); + if (transformation_type.instance()) { + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + v.set_instance_offset(inst_trafo.get_offset() + inst_trafo.get_rotation_matrix() * displacement); } - else if (tower_origin(0) + actual_displacement(0) + tower_size(0) + margin > plate_bbox.max(0)) { - actual_displacement(0) = plate_bbox.max(0) - tower_origin(0) - tower_size(0) - margin; - } - - if (tower_origin(1) + actual_displacement(1) - margin < plate_bbox.min(1)) { - actual_displacement(1) = plate_bbox.min(1) - tower_origin(1) + margin; - } - else if (tower_origin(1) + actual_displacement(1) + tower_size(1) + margin > plate_bbox.max(1)) { - actual_displacement(1) = plate_bbox.max(1) - tower_origin(1) - tower_size(1) - margin; - } - - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + actual_displacement); + else + transform_instance_relative(v, volume_data, transformation_type, Geometry::translation_transform(displacement), m_cache.dragging_center); } - else if (m_mode == Volume || v.is_wipe_tower) { - if (local) - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + displacement); - else { - const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); + else { + if (transformation_type.local() && transformation_type.absolute()) { + const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + v.set_volume_offset(vol_trafo.get_offset() + inst_trafo.get_scaling_factor_matrix().inverse() * vol_trafo.get_rotation_matrix() * displacement); } - } - else if (m_mode == Instance) { - if (is_from_fully_selected_instance(i)) - v.set_instance_offset(m_cache.volumes_data[i].get_instance_position() + displacement); else { - const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); - translation_type = Volume; + Vec3d relative_disp = displacement; + if (transformation_type.world() && transformation_type.instance()) + relative_disp = volume_data.get_instance_transform().get_scaling_factor_matrix().inverse() * relative_disp; + + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(relative_disp), m_cache.dragging_center); } } } #if !DISABLE_INSTANCES_SYNCH - if (translation_type == Instance) - synchronize_unselected_instances(SYNC_ROTATION_NONE); - else if (translation_type == Volume) + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) synchronize_unselected_volumes(); #endif // !DISABLE_INSTANCES_SYNCH @@ -995,109 +1180,98 @@ void Selection::rotate(const Vec3d& rotation, TransformationType transformation_ if (!m_valid) return; - // Only relative rotation values are allowed in the world coordinate system. - assert(!transformation_type.world() || transformation_type.relative()); + assert(transformation_type.relative() || (transformation_type.absolute() && transformation_type.local())); - if (!is_wipe_tower()) { - int rot_axis_max = 0; - if (rotation.isApprox(Vec3d::Zero())) { - for (unsigned int i : m_list) { - GLVolume &v = *(*m_volumes)[i]; - if (m_mode == Instance) { - v.set_instance_rotation(m_cache.volumes_data[i].get_instance_rotation()); - v.set_instance_offset(m_cache.volumes_data[i].get_instance_position()); + bool requires_general_synchronization = false; + + for (unsigned int i : m_list) { + Transform3d rotation_matrix = Geometry::rotation_transform(rotation); + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + if (m_mode == Instance && !is_wipe_tower()) { + assert(is_from_fully_selected_instance(i)); + if (transformation_type.instance()) { + // ensure that the instance rotates as a rigid body + Transform3d inst_rotation_matrix = inst_trafo.get_rotation_matrix(); + if (inst_trafo.is_left_handed()) { + Geometry::TransformationSVD inst_svd(inst_trafo); + inst_rotation_matrix = inst_svd.u * inst_svd.v.transpose(); + // ensure the rotation has the proper direction + if (!rotation.normalized().cwiseAbs().isApprox(Vec3d::UnitX())) + rotation_matrix = rotation_matrix.inverse(); } - else if (m_mode == Volume) { - v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation()); - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position()); + + const Transform3d inst_matrix_no_offset = inst_trafo.get_matrix_no_offset(); + rotation_matrix = inst_matrix_no_offset.inverse() * inst_rotation_matrix * rotation_matrix * inst_rotation_matrix.inverse() * inst_matrix_no_offset; + + // rotate around selection center + const Vec3d inst_pivot = inst_trafo.get_matrix_no_offset().inverse() * (m_cache.rotation_pivot - inst_trafo.get_offset()); + rotation_matrix = Geometry::translation_transform(inst_pivot) * rotation_matrix * Geometry::translation_transform(-inst_pivot); + + // Detects if the rotation is equivalent to a world rotation around the Z axis + // If not, force for a full synchronization of unselected instances + if (!requires_general_synchronization) { + const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); + const Transform3d old_world_rotation_matrix = (inst_trafo * vol_trafo).get_rotation_matrix(); + const Transform3d new_world_rotation_matrix = (inst_trafo * Geometry::Transformation(rotation_matrix) * vol_trafo).get_rotation_matrix(); + if (std::abs((old_world_rotation_matrix * Vec3d::UnitX()).z() - (new_world_rotation_matrix * Vec3d::UnitX()).z()) > EPSILON) + requires_general_synchronization = true; + else if (std::abs((old_world_rotation_matrix * Vec3d::UnitY()).z() - (new_world_rotation_matrix * Vec3d::UnitY()).z()) > EPSILON) + requires_general_synchronization = true; + else if (std::abs((old_world_rotation_matrix * Vec3d::UnitZ()).z() - (new_world_rotation_matrix * Vec3d::UnitZ()).z()) > EPSILON) + requires_general_synchronization = true; } } + transform_instance_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.rotation_pivot); } - else { // this is not the wipe tower - //FIXME this does not work for absolute rotations (transformation_type.absolute() is true) - rotation.cwiseAbs().maxCoeff(&rot_axis_max); - -// if ( single instance or single volume ) - // Rotate around center , if only a single object or volume -// transformation_type.set_independent(); - - // For generic rotation, we want to rotate the first volume in selection, and then to synchronize the other volumes with it. - std::vector object_instance_first(m_model->objects.size(), -1); - auto rotate_instance = [this, &rotation, &object_instance_first, rot_axis_max, transformation_type](GLVolume &volume, int i) { - const int first_volume_idx = object_instance_first[volume.object_idx()]; - if (rot_axis_max != 2 && first_volume_idx != -1) { - // Generic rotation, but no rotation around the Z axis. - // Always do a local rotation (do not consider the selection to be a rigid body). - assert(is_approx(rotation.z(), 0.0)); - const GLVolume &first_volume = *(*m_volumes)[first_volume_idx]; - const Vec3d &rotation = first_volume.get_instance_rotation(); - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[first_volume_idx].get_instance_rotation(), m_cache.volumes_data[i].get_instance_rotation()); - volume.set_instance_rotation(Vec3d(rotation(0), rotation(1), rotation(2) + z_diff)); + else { + if (!is_single_volume_or_modifier()) { + assert(transformation_type.world()); + transform_volume_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.rotation_pivot); + } + else { + if (transformation_type.instance()) { + // ensure that the volume rotates as a rigid body + const Transform3d inst_scale_matrix = inst_trafo.get_scaling_factor_matrix(); + rotation_matrix = inst_scale_matrix.inverse() * rotation_matrix * inst_scale_matrix; } else { - // extracts rotations from the composed transformation - Vec3d new_rotation = transformation_type.world() ? - Geometry::extract_euler_angles(Geometry::assemble_transform(Vec3d::Zero(), rotation) * m_cache.volumes_data[i].get_instance_rotation_matrix()) : - transformation_type.absolute() ? rotation : rotation + m_cache.volumes_data[i].get_instance_rotation(); - if (rot_axis_max == 2 && transformation_type.joint()) { - // Only allow rotation of multiple instances as a single rigid body when rotating around the Z axis. - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), new_rotation); - volume.set_instance_offset(m_cache.dragging_center + Eigen::AngleAxisd(z_diff, Vec3d::UnitZ()) * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); - } - volume.set_instance_rotation(new_rotation); - object_instance_first[volume.object_idx()] = i; - } - }; - - for (unsigned int i : m_list) { - GLVolume &v = *(*m_volumes)[i]; - if (is_single_full_instance()) - rotate_instance(v, i); - else if (is_single_volume() || is_single_modifier()) { - if (transformation_type.independent()) - v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation() + rotation); - else { - const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); - const Vec3d new_rotation = Geometry::extract_euler_angles(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); - v.set_volume_rotation(new_rotation); - } - } - else - { - if (m_mode == Instance) - rotate_instance(v, i); - else if (m_mode == Volume) { - // extracts rotations from the composed transformation - Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); - Vec3d new_rotation = Geometry::extract_euler_angles(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); - if (transformation_type.joint()) { - const Vec3d local_pivot = m_cache.volumes_data[i].get_instance_full_matrix().inverse() * m_cache.dragging_center; - const Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() - local_pivot); - v.set_volume_offset(local_pivot + offset); + if (transformation_type.local()) { + // ensure that the volume rotates as a rigid body + const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); + const Transform3d vol_matrix_no_offset = vol_trafo.get_matrix_no_offset(); + const Transform3d inst_scale_matrix = inst_trafo.get_scaling_factor_matrix(); + Transform3d vol_rotation_matrix = vol_trafo.get_rotation_matrix(); + if (vol_trafo.is_left_handed()) { + Geometry::TransformationSVD vol_svd(vol_trafo); + vol_rotation_matrix = vol_svd.u * vol_svd.v.transpose(); + // ensure the rotation has the proper direction + if (!rotation.normalized().cwiseAbs().isApprox(Vec3d::UnitX())) + rotation_matrix = rotation_matrix.inverse(); } - v.set_volume_rotation(new_rotation); + rotation_matrix = vol_matrix_no_offset.inverse() * inst_scale_matrix.inverse() * vol_rotation_matrix * rotation_matrix * + vol_rotation_matrix.inverse() * inst_scale_matrix * vol_matrix_no_offset; } } + transform_volume_relative(v, volume_data, transformation_type, rotation_matrix, m_cache.rotation_pivot); } } - - #if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances((rot_axis_max == 2) ? SYNC_ROTATION_NONE : SYNC_ROTATION_GENERAL); - else if (m_mode == Volume) - synchronize_unselected_volumes(); - #endif // !DISABLE_INSTANCES_SYNCH } - else { // it's the wipe tower that's selected and being rotated - GLVolume& volume = *((*m_volumes)[*m_list.begin()]); // the wipe tower is always alone in the selection - // make sure the wipe tower rotates around its center, not origin - // we can assume that only Z rotation changes - const Vec3d center_local = volume.transformed_bounding_box().center() - volume.get_volume_offset(); - const Vec3d center_local_new = Eigen::AngleAxisd(rotation(2)-volume.get_volume_rotation()(2), Vec3d(0.0, 0.0, 1.0)) * center_local; - volume.set_volume_rotation(rotation); - volume.set_volume_offset(volume.get_volume_offset() + center_local - center_local_new); +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) { + int rot_axis_max = 0; + rotation.cwiseAbs().maxCoeff(&rot_axis_max); + const SyncRotationType type = (transformation_type.instance() && requires_general_synchronization) || + (!transformation_type.instance() && rot_axis_max != 2) || + rotation.isApprox(Vec3d::Zero()) ? + SyncRotationType::GENERAL : SyncRotationType::NONE; + synchronize_unselected_instances(type); } + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH set_bounding_boxes_dirty(); wxGetApp().plater()->canvas3D()->requires_check_outside_state(); @@ -1113,39 +1287,27 @@ void Selection::flattening_rotate(const Vec3d& normal) if (!m_valid) return; - // BBS: show the normal for debug - std::stringstream ss; - ss << std::fixed << std::setprecision(4) << ": " << (-normal).transpose(); - wxGetApp().plater()->show_status_message("place on face -normal: "+ss.str()); - BOOST_LOG_TRIVIAL(debug) <<"flattening_rotate at "<<__FILE__<<":"<<__LINE__ << std::fixed << std::setprecision(4) << ": " << normal.transpose(); - flush_logs(); - for (unsigned int i : m_list) { GLVolume& v = *(*m_volumes)[i]; // Normal transformed from the object coordinate space to the world coordinate space. - const auto &voldata = m_cache.volumes_data[i]; - Vec3d tnormal = (Geometry::assemble_transform( - Vec3d::Zero(), voldata.get_instance_rotation(), - voldata.get_instance_scaling_factor().cwiseInverse(), voldata.get_instance_mirror()) * normal).normalized(); + const Geometry::Transformation& old_inst_trafo = v.get_instance_transformation(); + const Vec3d tnormal = old_inst_trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal; // Additional rotation to align tnormal with the down vector in the world coordinate space. - auto extra_rotation = Eigen::Quaterniond().setFromTwoVectors(tnormal, - Vec3d::UnitZ()); - v.set_instance_rotation(Geometry::extract_euler_angles(extra_rotation.toRotationMatrix() * m_cache.volumes_data[i].get_instance_rotation_matrix())); - - BOOST_LOG_TRIVIAL(debug) << "flattening_rotate " << (*m_volumes)[i]->name << std::fixed << std::setprecision(4) << ": tnormal=" << tnormal.transpose() << "; extra_rotation=" << Geometry::extract_euler_angles(extra_rotation.toRotationMatrix()).transpose(); - flush_logs(); + const Transform3d rotation_matrix = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ())); + v.set_instance_transformation(old_inst_trafo.get_offset_matrix() * rotation_matrix * old_inst_trafo.get_matrix_no_offset()); } #if !DISABLE_INSTANCES_SYNCH // Apply the same transformation also to other instances, // but respect their possibly diffrent z-rotation. if (m_mode == Instance) - synchronize_unselected_instances(SYNC_ROTATION_GENERAL); + synchronize_unselected_instances(SyncRotationType::GENERAL); #endif // !DISABLE_INSTANCES_SYNCH this->set_bounding_boxes_dirty(); } -void Selection::scale(const Vec3d& scale, TransformationType transformation_type) +void Selection::scale_legacy(const Vec3d& scale, TransformationType transformation_type) { if (!m_valid) return; @@ -1168,7 +1330,7 @@ void Selection::scale(const Vec3d& scale, TransformationType transformation_type // Non-uniform scaling. Transform the scaling factors into the local coordinate system. // This is only possible, if the instance rotation is mulitples of ninety degrees. assert(Geometry::is_rotation_ninety_degrees(v.get_instance_rotation())); - v.set_instance_scaling_factor((v.get_instance_transformation().get_matrix(true, false, true, true).matrix().block<3, 3>(0, 0).transpose() * scale).cwiseAbs()); + v.set_instance_scaling_factor((v.get_instance_transformation().get_rotation_matrix().matrix().block<3, 3>(0, 0).transpose() * scale).cwiseAbs()); } else v.set_instance_scaling_factor(scale); @@ -1208,7 +1370,9 @@ void Selection::scale(const Vec3d& scale, TransformationType transformation_type #if !DISABLE_INSTANCES_SYNCH if (m_mode == Instance) - synchronize_unselected_instances(SYNC_ROTATION_NONE); + // even if there is no rotation, we pass SyncRotationType::GENERAL to force + // synchronize_unselected_instances() to apply the scale to the other instances + synchronize_unselected_instances(SyncRotationType::GENERAL); else if (m_mode == Volume) synchronize_unselected_volumes(); #endif // !DISABLE_INSTANCES_SYNCH @@ -1218,6 +1382,11 @@ void Selection::scale(const Vec3d& scale, TransformationType transformation_type wxGetApp().plater()->canvas3D()->requires_check_outside_state(); } +void Selection::scale(const Vec3d& scale, TransformationType transformation_type) +{ + scale_and_translate(scale, Vec3d::Zero(), transformation_type); +} + #if ENABLE_ENHANCED_PRINT_VOLUME_FIT void Selection::scale_to_fit_print_volume(const BuildVolume& volume) { @@ -1240,7 +1409,9 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume) // center selection on print bed setup_cache(); offset.z() = -get_bounding_box().min.z(); - translate(offset); + TransformationType trafo_type; + trafo_type.set_relative(); + translate(offset, trafo_type); wxGetApp().plater()->canvas3D()->do_move(""); // avoid storing another snapshot // BBS @@ -1353,27 +1524,84 @@ void Selection::scale_to_fit_print_volume(const DynamicPrintConfig& config) } #endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT -void Selection::mirror(Axis axis) +void Selection::mirror(Axis axis, TransformationType transformation_type) +{ + const Vec3d mirror((axis == X) ? -1.0 : 1.0, (axis == Y) ? -1.0 : 1.0, (axis == Z) ? -1.0 : 1.0); + scale_and_translate(mirror, Vec3d::Zero(), transformation_type); +} + +void Selection::scale_and_translate(const Vec3d& scale, const Vec3d& world_translation, TransformationType transformation_type) { if (!m_valid) - return; + return; + + Vec3d relative_scale = scale; + if (transformation_type.absolute()) { + // converts to relative scale + if (m_mode == Instance) { + if (is_single_full_instance()) { + BoundingBoxf3 current_box = get_bounding_box_in_current_reference_system().first; + BoundingBoxf3 original_box; + if (transformation_type.world()) + original_box = get_full_unscaled_instance_bounding_box(); + else + original_box = get_full_unscaled_instance_local_bounding_box(); + + relative_scale = original_box.size().cwiseProduct(scale).cwiseQuotient(current_box.size()); + } + } + transformation_type.set_relative(); + } for (unsigned int i : m_list) { GLVolume& v = *(*m_volumes)[i]; - if (is_single_full_instance()) - v.set_instance_mirror(axis, -v.get_instance_mirror(axis)); - else if (m_mode == Volume) - v.set_volume_mirror(axis, -v.get_volume_mirror(axis)); + const VolumeCache& volume_data = m_cache.volumes_data[i]; + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + + if (m_mode == Instance) { + if (transformation_type.instance()) { + const Vec3d world_inst_pivot = m_cache.dragging_center - inst_trafo.get_offset(); + const Vec3d local_inst_pivot = inst_trafo.get_matrix_no_offset().inverse() * world_inst_pivot; + Matrix3d inst_rotation, inst_scale; + inst_trafo.get_matrix().computeRotationScaling(&inst_rotation, &inst_scale); + const Transform3d offset_trafo = Geometry::translation_transform(inst_trafo.get_offset() + world_translation); + const Transform3d scale_trafo = Transform3d(inst_scale) * Geometry::scale_transform(relative_scale); + v.set_instance_transformation(Geometry::translation_transform(world_inst_pivot) * offset_trafo * Transform3d(inst_rotation) * scale_trafo * Geometry::translation_transform(-local_inst_pivot)); + } + else + transform_instance_relative(v, volume_data, transformation_type, Geometry::translation_transform(world_translation) * Geometry::scale_transform(relative_scale), m_cache.dragging_center); + } + else { + if (!is_single_volume_or_modifier()) { + assert(transformation_type.world()); + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(world_translation) * Geometry::scale_transform(scale), m_cache.dragging_center); + } + else { + transformation_type.set_independent(); + Vec3d translation; + if (transformation_type.local()) + translation = volume_data.get_volume_transform().get_matrix_no_offset().inverse() * inst_trafo.get_matrix_no_offset().inverse() * world_translation; + else if (transformation_type.instance()) + translation = inst_trafo.get_matrix_no_offset().inverse() * world_translation; + else + translation = world_translation; + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(scale), m_cache.dragging_center); + } + } } #if !DISABLE_INSTANCES_SYNCH if (m_mode == Instance) - synchronize_unselected_instances(SYNC_ROTATION_NONE); + // even if there is no rotation, we pass SyncRotationType::GENERAL to force + // synchronize_unselected_instances() to apply the scale to the other instances + synchronize_unselected_instances(SyncRotationType::GENERAL); else if (m_mode == Volume) synchronize_unselected_volumes(); #endif // !DISABLE_INSTANCES_SYNCH + ensure_on_bed(); set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); } void Selection::translate(unsigned int object_idx, const Vec3d& displacement) @@ -1627,7 +1855,8 @@ void Selection::render(float scale_factor) m_scale_factor = scale_factor; // render cumulative bounding box of selected volumes - render_bounding_box(get_bounding_box(), ColorRGB::WHITE()); + const auto& [box, trafo] = get_bounding_box_in_current_reference_system(); + render_bounding_box(box, trafo, ColorRGB::WHITE()); render_synchronized_volumes(); } @@ -1684,14 +1913,14 @@ void Selection::render_sidebar_hints(const std::string& sidebar_field, bool unif if (is_single_full_instance()/* && !wxGetApp().obj_manipul()->get_world_coordinates()*/) { if (!boost::starts_with(sidebar_field, "position")) { if (boost::starts_with(sidebar_field, "scale")) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); else if (boost::starts_with(sidebar_field, "rotation")) { if (boost::ends_with(sidebar_field, "x")) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); else if (boost::ends_with(sidebar_field, "y")) { const Vec3d& rotation = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation(); if (rotation.x() == 0.0) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); else orient_matrix.rotate(Eigen::AngleAxisd(rotation.z(), Vec3d::UnitZ())); } @@ -1699,14 +1928,14 @@ void Selection::render_sidebar_hints(const std::string& sidebar_field, bool unif } } else if (is_single_volume() || is_single_modifier()) { - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); if (!boost::starts_with(sidebar_field, "position")) - orient_matrix = orient_matrix * (*m_volumes)[*m_list.begin()]->get_volume_transformation().get_matrix(true, false, true, true); + orient_matrix = orient_matrix * (*m_volumes)[*m_list.begin()]->get_volume_transformation().get_rotation_matrix(); } else { if (requires_local_axes()) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); } } @@ -2143,6 +2372,7 @@ void Selection::set_caches() m_cache.sinking_volumes.push_back(i); } m_cache.dragging_center = get_bounding_box().center(); + m_cache.rotation_pivot = get_bounding_sphere().first; } void Selection::do_add_volume(unsigned int volume_idx) @@ -2197,6 +2427,10 @@ void Selection::render_synchronized_volumes() if (m_mode == Instance) return; + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + BoundingBoxf3 box; + Transform3d trafo; + for (unsigned int i : m_list) { const GLVolume& volume = *(*m_volumes)[i]; int object_idx = volume.object_idx(); @@ -2209,14 +2443,27 @@ void Selection::render_synchronized_volumes() if (v.object_idx() != object_idx || v.volume_idx() != volume_idx) continue; - render_bounding_box(v.transformed_convex_hull_bounding_box(), ColorRGB::YELLOW()); + if (coordinates_type == ECoordinatesType::World) { + box = v.transformed_convex_hull_bounding_box(); + trafo = Transform3d::Identity(); + } + else if (coordinates_type == ECoordinatesType::Local) { + box = v.bounding_box(); + trafo = v.world_matrix(); + } + else { + box = v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix()); + trafo = v.get_instance_transformation().get_matrix(); + } + render_bounding_box(box, trafo, ColorRGB::YELLOW()); } } } -void Selection::render_bounding_box(const BoundingBoxf3& box, const ColorRGB& color) +void Selection::render_bounding_box(const BoundingBoxf3& box, const Transform3d& trafo, const ColorRGB& color) { const BoundingBoxf3& curr_box = m_box.get_bounding_box(); + if (!m_box.is_initialized() || !is_approx(box.min, curr_box.min) || !is_approx(box.max, curr_box.max)) { m_box.reset(); @@ -2287,7 +2534,7 @@ void Selection::render_bounding_box(const BoundingBoxf3& box, const ColorRGB& co init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z() - size.z())); // indices - for (unsigned short i = 0; i < 48; ++i) { + for (unsigned int i = 0; i < 48; ++i) { init_data.add_index(i); } @@ -2304,7 +2551,7 @@ void Selection::render_bounding_box(const BoundingBoxf3& box, const ColorRGB& co shader->start_using(); const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); m_box.set_color(to_rgba(color)); m_box.render(); @@ -2518,6 +2765,16 @@ void Selection::render_sidebar_layers_hints(const std::string& sidebar_field, GL glsafe(::glDisable(GL_BLEND)); } +static bool is_left_handed(const Transform3d::ConstLinearPart& m) +{ + return m.determinant() < 0; +} + +static bool is_left_handed(const Transform3d& m) +{ + return is_left_handed(m.linear()); +} + #ifndef NDEBUG static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) { @@ -2564,48 +2821,40 @@ void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_ if (done.size() == m_volumes->size()) break; - const GLVolume* volume = (*m_volumes)[i]; - const int object_idx = volume->object_idx(); + const GLVolume* volume_i = (*m_volumes)[i]; + const int object_idx = volume_i->object_idx(); if (object_idx >= 1000) continue; - const int instance_idx = volume->instance_idx(); - const Vec3d& rotation = volume->get_instance_rotation(); - const Vec3d& scaling_factor = volume->get_instance_scaling_factor(); - const Vec3d& mirror = volume->get_instance_mirror(); + const int instance_idx = volume_i->instance_idx(); + const Transform3d& curr_inst_trafo_i = volume_i->get_instance_transformation().get_matrix(); + const Transform3d& old_inst_trafo_i = m_cache.volumes_data[i].get_instance_transform().get_matrix(); + bool mirrored = is_left_handed(curr_inst_trafo_i) != is_left_handed(old_inst_trafo_i); +// bool mirrored = curr_inst_trafo_i.linear().determinant() * old_inst_trafo_i.linear().determinant() < 0; // Process unselected instances. for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { if (done.size() == m_volumes->size()) break; - if (done.find(j) != done.end()) continue; - - GLVolume* v = (*m_volumes)[j]; - if (v->object_idx() != object_idx || v->instance_idx() == instance_idx) + GLVolume* volume_j = (*m_volumes)[j]; + if (volume_j->object_idx() != object_idx || volume_j->instance_idx() == instance_idx) continue; - - assert(is_rotation_xy_synchronized(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation())); - switch (sync_rotation_type) { - case SYNC_ROTATION_NONE: { - // z only rotation -> synch instance z - // The X,Y rotations should be synchronized from start to end of the rotation. - assert(is_rotation_xy_synchronized(rotation, v->get_instance_rotation())); - if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) - v->set_instance_offset(Z, volume->get_instance_offset().z()); - break; + const Transform3d& old_inst_trafo_j = m_cache.volumes_data[j].get_instance_transform().get_matrix(); + assert(is_rotation_xy_synchronized(old_inst_trafo_i, old_inst_trafo_j)); + Transform3d new_inst_trafo_j = volume_j->get_instance_transformation().get_matrix(); + if (sync_rotation_type == SyncRotationType::RESET) { + Geometry::Transformation new_inst_trafo_j_no_rotation(new_inst_trafo_j); + new_inst_trafo_j_no_rotation.reset_rotation(); + new_inst_trafo_j = new_inst_trafo_j_no_rotation.get_matrix(); } - case SYNC_ROTATION_GENERAL: - // generic rotation -> update instance z with the delta of the rotation. - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation()); - v->set_instance_rotation({ rotation.x(), rotation.y(), rotation.z() + z_diff }); - break; - } - - v->set_instance_scaling_factor(scaling_factor); - v->set_instance_mirror(mirror); - + else if (sync_rotation_type != SyncRotationType::NONE || mirrored) + new_inst_trafo_j.linear() = (old_inst_trafo_j.linear() * old_inst_trafo_i.linear().inverse()) * curr_inst_trafo_i.linear(); + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + new_inst_trafo_j.translation().z() = curr_inst_trafo_i.translation().z(); + assert(is_rotation_xy_synchronized(curr_inst_trafo_i, new_inst_trafo_j)); + volume_j->set_instance_transformation(new_inst_trafo_j); done.insert(j); } } @@ -2624,10 +2873,7 @@ void Selection::synchronize_unselected_volumes() continue; const int volume_idx = volume->volume_idx(); - const Vec3d& offset = volume->get_volume_offset(); - const Vec3d& rotation = volume->get_volume_rotation(); - const Vec3d& scaling_factor = volume->get_volume_scaling_factor(); - const Vec3d& mirror = volume->get_volume_mirror(); + const Geometry::Transformation& trafo = volume->get_volume_transformation(); // Process unselected volumes. for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { @@ -2638,10 +2884,7 @@ void Selection::synchronize_unselected_volumes() if (v->object_idx() != object_idx || v->volume_idx() != volume_idx) continue; - v->set_volume_offset(offset); - v->set_volume_rotation(rotation); - v->set_volume_scaling_factor(scaling_factor); - v->set_volume_mirror(mirror); + v->set_volume_transformation(trafo); } } } @@ -2756,8 +2999,8 @@ void Selection::paste_volumes_from_clipboard() { ModelInstance* dst_instance = dst_object->instances[dst_inst_idx]; BoundingBoxf3 dst_instance_bb = dst_object->instance_bounding_box(dst_inst_idx); - Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix(true); - Transform3d dst_matrix = dst_instance->get_transformation().get_matrix(true); + Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix_no_offset(); + Transform3d dst_matrix = dst_instance->get_transformation().get_matrix_no_offset(); bool from_same_object = (src_object->input_file == dst_object->input_file) && src_matrix.isApprox(dst_matrix); // used to keep relative position of multivolume selections when pasting from another object @@ -2875,5 +3118,97 @@ void Selection::paste_objects_from_clipboard() #endif /* _DEBUG */ } +void Selection::transform_instance_relative(GLVolume& volume, const VolumeCache& volume_data, TransformationType transformation_type, + const Transform3d& transform, const Vec3d& world_pivot) +{ + assert(transformation_type.relative()); + + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + if (transformation_type.world()) { + const Vec3d inst_pivot = transformation_type.independent() && !is_from_single_instance() ? inst_trafo.get_offset() : world_pivot; + const Transform3d trafo = Geometry::translation_transform(inst_pivot) * transform * Geometry::translation_transform(-inst_pivot); + volume.set_instance_transformation(trafo * inst_trafo.get_matrix()); + } + else if (transformation_type.instance()) + volume.set_instance_transformation(inst_trafo.get_matrix() * transform); + else + assert(false); +} + +void Selection::transform_volume_relative(GLVolume& volume, const VolumeCache& volume_data, TransformationType transformation_type, + const Transform3d& transform, const Vec3d& world_pivot) +{ + assert(transformation_type.relative()); + + const Geometry::Transformation& vol_trafo = volume_data.get_volume_transform(); + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + + if (transformation_type.world()) { + const Vec3d inst_pivot = transformation_type.independent() ? vol_trafo.get_offset() : (Vec3d)(inst_trafo.get_matrix().inverse() * world_pivot); + const Transform3d inst_matrix_no_offset = inst_trafo.get_matrix_no_offset(); + const Transform3d trafo = Geometry::translation_transform(inst_pivot) * inst_matrix_no_offset.inverse() * transform * inst_matrix_no_offset * Geometry::translation_transform(-inst_pivot); + volume.set_volume_transformation(trafo * vol_trafo.get_matrix()); + } + else if (transformation_type.instance()) { + const Vec3d inst_pivot = transformation_type.independent() ? vol_trafo.get_offset() : (Vec3d)(inst_trafo.get_matrix().inverse() * world_pivot); + const Transform3d trafo = Geometry::translation_transform(inst_pivot) * transform * Geometry::translation_transform(-inst_pivot); + volume.set_volume_transformation(trafo * vol_trafo.get_matrix()); + } + else if (transformation_type.local()) + volume.set_volume_transformation(vol_trafo.get_matrix() * transform); + else + 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 233fb4aa5..c506a1a6f 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -17,6 +17,8 @@ namespace Slic3r { class Shader; class Model; class ModelObject; +class ModelVolume; +class ObjectID; class GLVolume; class GLArrow; class GLCurvedArrow; @@ -66,13 +68,11 @@ private: struct TransformCache { Vec3d position; - Vec3d rotation; - Vec3d scaling_factor; - Vec3d mirror; Transform3d rotation_matrix; Transform3d scale_matrix; Transform3d mirror_matrix; - Transform3d full_matrix; + + Geometry::Transformation transform; TransformCache(); explicit TransformCache(const Geometry::Transformation& transform); @@ -86,22 +86,14 @@ private: VolumeCache(const Geometry::Transformation& volume_transform, const Geometry::Transformation& instance_transform); const Vec3d& get_volume_position() const { return m_volume.position; } - const Vec3d& get_volume_rotation() const { return m_volume.rotation; } - const Vec3d& get_volume_scaling_factor() const { return m_volume.scaling_factor; } - const Vec3d& get_volume_mirror() const { return m_volume.mirror; } - const Transform3d& get_volume_rotation_matrix() const { return m_volume.rotation_matrix; } const Transform3d& get_volume_scale_matrix() const { return m_volume.scale_matrix; } - const Transform3d& get_volume_mirror_matrix() const { return m_volume.mirror_matrix; } - const Transform3d& get_volume_full_matrix() const { return m_volume.full_matrix; } + const Geometry::Transformation& get_volume_transform() const { return m_volume.transform; } const Vec3d& get_instance_position() const { return m_instance.position; } - const Vec3d& get_instance_rotation() const { return m_instance.rotation; } - const Vec3d& get_instance_scaling_factor() const { return m_instance.scaling_factor; } - const Vec3d& get_instance_mirror() const { return m_instance.mirror; } const Transform3d& get_instance_rotation_matrix() const { return m_instance.rotation_matrix; } const Transform3d& get_instance_scale_matrix() const { return m_instance.scale_matrix; } const Transform3d& get_instance_mirror_matrix() const { return m_instance.mirror_matrix; } - const Transform3d& get_instance_full_matrix() const { return m_instance.full_matrix; } + const Geometry::Transformation &get_instance_transform() const { return m_instance.transform; } }; public: @@ -146,6 +138,7 @@ private: ObjectIdxsToInstanceIdxsMap content; // List of ids of the volumes which are sinking when starting dragging std::vector sinking_volumes; + Vec3d rotation_pivot; }; // Volumes owned by GLCanvas3D. @@ -162,10 +155,27 @@ private: Cache m_cache; Clipboard m_clipboard; std::optional m_bounding_box; - // Bounding box of a selection, with no instance scaling applied. This bounding box - // is useful for absolute scaling of tilted objects in world coordinate space. + // Bounding box of a single full instance selection, in world coordinates, with no instance scaling applied. + // This bounding box is useful for absolute scaling of tilted objects in world coordinate space. + // Modifiers are NOT taken in account std::optional m_unscaled_instance_bounding_box; + // Bounding box of a single full instance selection, in world coordinates. + // Modifiers are NOT taken in account std::optional m_scaled_instance_bounding_box; + // Bounding box of a single full instance selection, in world coordinates, with no instance scaling applied. + // Modifiers are taken in account + std::optional m_full_unscaled_instance_bounding_box; + // Bounding box of a single full instance selection, in world coordinates. + // Modifiers are taken in account + std::optional m_full_scaled_instance_bounding_box; + // Bounding box of a single full instance selection, in local coordinates, with no instance scaling applied. + // Modifiers are taken in account + std::optional m_full_unscaled_instance_local_bounding_box; + // Bounding box aligned to the axis of the currently selected reference system (World/Object/Part) + // and transform to place and orient it in world coordinates + std::optional> m_bounding_box_in_current_reference_system; + + std::optional> m_bounding_sphere; #if ENABLE_RENDER_SELECTION_CENTER GLModel m_vbo_sphere; @@ -258,6 +268,9 @@ public: bool is_from_single_object() const; bool is_sla_compliant() const; bool is_instance_mode() const { return m_mode == Instance; } + bool is_single_volume_or_modifier() const { return is_single_volume() || is_single_modifier(); } + bool is_single_volume_instance() const { return is_single_full_instance() && m_list.size() == 1; } + bool is_single_text() const; bool contains_volume(unsigned int volume_idx) const { return m_list.find(volume_idx) != m_list.end(); } // returns true if the selection contains all the given indices @@ -282,28 +295,54 @@ public: const IndicesList& get_volume_idxs() const { return m_list; } const GLVolume* get_volume(unsigned int volume_idx) const; const GLVolume* get_first_volume() const { return get_volume(*m_list.begin()); } + GLVolume* get_volume(unsigned int volume_idx); + const ObjectIdxsToInstanceIdxsMap& get_content() const { return m_cache.content; } unsigned int volumes_count() const { return (unsigned int)m_list.size(); } const BoundingBoxf3& get_bounding_box() const; - // Bounding box of a selection, with no instance scaling applied. This bounding box - // is useful for absolute scaling of tilted objects in world coordinate space. + // Bounding box of a single full instance selection, in world coordinates, with no instance scaling applied. + // This bounding box is useful for absolute scaling of tilted objects in world coordinate space. + // Modifiers are NOT taken in account const BoundingBoxf3& get_unscaled_instance_bounding_box() const; + // Bounding box of a single full instance selection, in world coordinates. + // Modifiers are NOT taken in account const BoundingBoxf3& get_scaled_instance_bounding_box() const; + // Bounding box of a single full instance selection, in world coordinates, with no instance scaling applied. + // Modifiers are taken in account + const BoundingBoxf3& get_full_unscaled_instance_bounding_box() const; + // Bounding box of a single full instance selection, in world coordinates. + // Modifiers are taken in account + const BoundingBoxf3& get_full_scaled_instance_bounding_box() const; + // Bounding box of a single full instance selection, in local coordinates, with no instance scaling applied. + // Modifiers are taken in account + const BoundingBoxf3& get_full_unscaled_instance_local_bounding_box() const; + // Returns the bounding box aligned to the axes of the currently selected reference system (World/Object/Part) + // and the transform to place and orient it in world coordinates + const std::pair& get_bounding_box_in_current_reference_system() const; + // Returns the bounding box aligned to the axes of the given reference system + // and the transform to place and orient it in world coordinates + std::pair get_bounding_box_in_reference_system(ECoordinatesType type) const; + + // Returns the bounding sphere: first = center, second = radius + const std::pair get_bounding_sphere() const; void setup_cache(); - void translate(const Vec3d& displacement, bool local = false); + void translate(const Vec3d& displacement, TransformationType transformation_type); void move_to_center(const Vec3d& displacement, bool local = false); void rotate(const Vec3d& rotation, TransformationType transformation_type); void flattening_rotate(const Vec3d& normal); + [[deprecated("Only used by GizmoObjectManipulation")]] + void scale_legacy(const Vec3d& scale, TransformationType transformation_type); void scale(const Vec3d& scale, TransformationType transformation_type); #if ENABLE_ENHANCED_PRINT_VOLUME_FIT void scale_to_fit_print_volume(const BuildVolume& volume); #else void scale_to_fit_print_volume(const DynamicPrintConfig& config); #endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT - void mirror(Axis axis); + void scale_and_translate(const Vec3d& scale, const Vec3d& world_translation, TransformationType transformation_type); + void mirror(Axis axis, TransformationType transformation_type); void translate(unsigned int object_idx, const Vec3d& displacement); void translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement); @@ -327,7 +366,7 @@ public: void render_bounding_box(const BoundingBoxf3& box, const ColorRGB& color, float scale) { m_scale_factor = scale; - render_bounding_box(box, color); + render_bounding_box(box, Transform3d::Identity(), color); } //BBS @@ -361,9 +400,16 @@ private: void do_remove_volume(unsigned int volume_idx); void do_remove_instance(unsigned int object_idx, unsigned int instance_idx); void do_remove_object(unsigned int object_idx); - void set_bounding_boxes_dirty() { m_bounding_box.reset(); m_unscaled_instance_bounding_box.reset(); m_scaled_instance_bounding_box.reset(); } + void set_bounding_boxes_dirty() { + m_bounding_box.reset(); + m_unscaled_instance_bounding_box.reset(); m_scaled_instance_bounding_box.reset(); + m_full_unscaled_instance_bounding_box.reset(); m_full_scaled_instance_bounding_box.reset(); + m_full_unscaled_instance_local_bounding_box.reset(); + m_bounding_box_in_current_reference_system.reset(); + m_bounding_sphere.reset(); + } void render_synchronized_volumes(); - void render_bounding_box(const BoundingBoxf3& box, const ColorRGB& color); + void render_bounding_box(const BoundingBoxf3& box, const Transform3d& trafo, const ColorRGB& color); void render_sidebar_position_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix); void render_sidebar_rotation_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix); //BBS: GUI refactor: add uniform_scale from gizmo @@ -371,11 +417,13 @@ private: void render_sidebar_layers_hints(const std::string& sidebar_field, GLShaderProgram& shader); public: - enum SyncRotationType { + enum class SyncRotationType { // Do not synchronize rotation. Either not rotating at all, or rotating by world Z axis. - SYNC_ROTATION_NONE = 0, + NONE = 0, // Synchronize after rotation by an axis not parallel with Z. - SYNC_ROTATION_GENERAL = 1, + GENERAL = 1, + // Synchronize after rotation reset. + RESET = 2 }; void synchronize_unselected_instances(SyncRotationType sync_rotation_type); void synchronize_unselected_volumes(); @@ -387,8 +435,19 @@ private: void paste_volumes_from_clipboard(); void paste_objects_from_clipboard(); + + void transform_instance_relative(GLVolume& volume, const VolumeCache& volume_data, TransformationType transformation_type, + const Transform3d& transform, const Vec3d& world_pivot); + 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/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index 3132ca664..1900e281b 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -12,6 +12,8 @@ #include "Widgets/RoundedRectangle.hpp" #include "Widgets/StaticBox.hpp" #include "ConnectPrinter.hpp" +#include "Jobs/BoostThreadWorker.hpp" +#include "Jobs/PlaterWorker.hpp" #include #include @@ -300,6 +302,8 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater) m_status_bar = std::make_shared(m_simplebook); m_panel_sending = m_status_bar->get_panel(); m_simplebook->AddPage(m_panel_sending, wxEmptyString, false); + + m_worker = std::make_unique>(this, m_status_bar, "send_worker"); // finish mode m_panel_finish = new wxPanel(m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); @@ -580,9 +584,7 @@ void SendToPrinterDialog::prepare_mode() { m_is_in_sending_mode = false; m_comboBox_printer->Enable(); - if (m_send_job) { - m_send_job->join(); - } + m_worker->wait_for_idle(); if (wxIsBusy()) wxEndBusyCursor(); @@ -670,12 +672,7 @@ void SendToPrinterDialog::init_timer() void SendToPrinterDialog::on_cancel(wxCloseEvent &event) { - if (m_send_job) { - if (m_send_job->is_running()) { - m_send_job->cancel(); - m_send_job->join(); - } - } + m_worker->cancel_all(); this->EndModal(wxID_CANCEL); } @@ -712,13 +709,7 @@ void SendToPrinterDialog::on_ok(wxCommandEvent &event) m_status_bar->set_prog_block(); m_status_bar->set_cancel_callback_fina([this]() { BOOST_LOG_TRIVIAL(info) << "print_job: enter canceled"; - if (m_send_job) { - if (m_send_job->is_running()) { - BOOST_LOG_TRIVIAL(info) << "send_job: canceled"; - m_send_job->cancel(); - } - m_send_job->join(); - } + m_worker->cancel_all(); m_is_canceled = true; wxCommandEvent* event = new wxCommandEvent(EVT_PRINT_JOB_CANCEL); wxQueueEvent(this, event); @@ -776,7 +767,7 @@ void SendToPrinterDialog::on_ok(wxCommandEvent &event) - m_send_job = std::make_shared(m_status_bar, m_plater, m_printer_last_select); + auto m_send_job = std::make_unique(m_printer_last_select); m_send_job->m_dev_ip = obj_->dev_ip; m_send_job->m_access_code = obj_->get_access_code(); @@ -805,12 +796,10 @@ void SendToPrinterDialog::on_ok(wxCommandEvent &event) if (obj_->is_lan_mode_printer()) { m_send_job->set_check_mode(); m_send_job->check_and_continue(); - m_send_job->start(); - } - else { - m_send_job->start(); } + replace_job(*m_worker, std::move(m_send_job)); + BOOST_LOG_TRIVIAL(info) << "send_job: send print job"; } diff --git a/src/slic3r/GUI/SendToPrinter.hpp b/src/slic3r/GUI/SendToPrinter.hpp index d048caa29..b084a382f 100644 --- a/src/slic3r/GUI/SendToPrinter.hpp +++ b/src/slic3r/GUI/SendToPrinter.hpp @@ -105,7 +105,6 @@ private: wxStaticText* m_file_name; PrintDialogStatus m_print_status{ PrintStatusInit }; - std::shared_ptr m_send_job{nullptr}; std::vector m_bedtype_list; std::map m_checkbox_list; std::vector m_list; @@ -113,6 +112,7 @@ private: wxColour m_colour_bold_color{ wxColour(38, 46, 48) }; wxTimer* m_refresh_timer{ nullptr }; std::shared_ptr m_status_bar; + std::unique_ptr m_worker; wxScrolledWindow* m_sw_print_failed_info{nullptr}; std::shared_ptr m_token = std::make_shared(0); diff --git a/src/slic3r/GUI/SurfaceDrag.cpp b/src/slic3r/GUI/SurfaceDrag.cpp new file mode 100644 index 000000000..8b6680780 --- /dev/null +++ b/src/slic3r/GUI/SurfaceDrag.cpp @@ -0,0 +1,732 @@ +///|/ Copyright (c) Prusa Research 2023 Oleksandra Iushchenko @YuSanka +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#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 {}; + + 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); +} + +std::optional calc_distance(const GLVolume &gl_volume, const RaycastManager &raycaster, const RaycastManager::ISkip *condition) +{ + Transform3d w = gl_volume.world_matrix(); + 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; + + ModelInstance *instance_ptr = get_model_instance(gl_volume, object); + assert(instance_ptr != nullptr); + if (instance_ptr == nullptr) + return false; + 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) : + (-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(GLCanvas3D &canvas, double relative_angle) +{ + Selection &selection = canvas.get_selection(); + + 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); + + std::string snapshot_name; // empty meand no store undo / redo + // NOTE: it use L instead of _L macro because prefix _ is appended + // inside function do_move + // snapshot_name = L("Set text rotation"); + canvas.do_rotate(snapshot_name); +} + +void do_local_z_move(GLCanvas3D &canvas, double relative_move) { + + Selection &selection = canvas.get_selection(); + 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); + + std::string snapshot_name; // empty mean no store undo / redo + // NOTE: it use L instead of _L macro because prefix _ is appended inside + // function do_move + // snapshot_name = L("Set surface distance"); + canvas.do_move(snapshot_name); +} + +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 + Vec2i 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); + if (gl_volume_ptr == nullptr) + return false; + const GLVolume &gl_volume = *gl_volume_ptr; + + // is selected volume closest hovered? + 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 *object = get_model_object(gl_volume, objects); + assert(object != nullptr); + if (object == nullptr) + return false; + + 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 false; + + // allowed drag&drop by canvas for object + if (volume->is_the_only_one_part()) + return false; + + RaycastManager::AllowVolumes condition = create_condition(object->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 + Vec2i 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); + 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); + 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..9f1c0e3c0 --- /dev/null +++ b/src/slic3r/GUI/SurfaceDrag.hpp @@ -0,0 +1,169 @@ +#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); + +/// +/// 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(GLCanvas3D &canvas, double relative_angle); + +/// +/// Translation along local z Axis (emboss direction) +/// +/// Selected volume for translate +/// Relative move along emboss direction +void do_local_z_move(GLCanvas3D &canvas, 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/GUI/TextLines.cpp b/src/slic3r/GUI/TextLines.cpp new file mode 100644 index 000000000..258d4916e --- /dev/null +++ b/src/slic3r/GUI/TextLines.cpp @@ -0,0 +1,354 @@ +#include "TextLines.hpp" + +#include + +#include "libslic3r/Model.hpp" + +#include "libslic3r/Emboss.hpp" +#include "libslic3r/TriangleMeshSlicer.hpp" +#include "libslic3r/Tesselate.hpp" + +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/ExPolygonsIndex.hpp" +#include "libslic3r/ClipperUtils.hpp" + +#include "slic3r/GUI/Selection.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GLModel.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Camera.hpp" +#include "slic3r/GUI/3DScene.hpp" + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; + +namespace { +// Be careful it is not water tide and contain self intersections +// It is only for visualization purposes +indexed_triangle_set its_create_torus(const Slic3r::Polygon &polygon, float radius, size_t steps = 20) +{ + assert(!polygon.empty()); + if (polygon.empty()) + return {}; + + size_t count = polygon.size(); + if (count < 3) + return {}; + + // convert and scale to float + std::vector points_d; + points_d.reserve(count); + for (const Point &point : polygon.points) + points_d.push_back(unscale(point).cast()); + + // pre calculate normalized line directions + auto calc_line_norm = [](const Vec2f &f, const Vec2f &s) -> Vec2f { return (s - f).normalized(); }; + std::vector line_norm(points_d.size()); + for (size_t i = 0; i < count - 1; ++i) + line_norm[i] = calc_line_norm(points_d[i], points_d[i + 1]); + line_norm.back() = calc_line_norm(points_d.back(), points_d.front()); + + // precalculate sinus and cosinus + double angle_step = 2 * M_PI / steps; + std::vector> sin_cos; + sin_cos.reserve(steps); + for (size_t s = 0; s < steps; ++s) { + double angle = s * angle_step; + sin_cos.emplace_back( + radius * std::sin(angle), + static_cast(radius * std::cos(angle)) + ); + } + + indexed_triangle_set sphere = its_make_sphere(radius, 2 * PI / steps); + + // create torus model along polygon path + indexed_triangle_set model; + model.vertices.reserve(2 * steps * count + sphere.vertices.size()*count); + model.indices.reserve(2 * steps * count + sphere.indices.size()*count); + + const Vec2f *prev_prev_point_d = &points_d[count-2]; // one before back + const Vec2f *prev_point_d = &points_d.back(); + + auto calc_angle = [](const Vec2f &d0, const Vec2f &d1) { + double dot = d0.dot(d1); + double det = d0.x() * d1.y() - d0.y() * d1.x(); // Determinant + return std::atan2(det, dot); // atan2(y, x) or atan2(sin, cos) + }; + + // opposit previos direction of line - for calculate angle + Vec2f opposit_prev_dir = (*prev_prev_point_d) - (*prev_point_d); + for (size_t i = 0; i < count; ++i) { + + const Vec2f & point_d = points_d[i]; + // line segment direction + Vec2f dir = point_d - (*prev_point_d); + + double angle = calc_angle(opposit_prev_dir, dir); + double allowed_preccission = 1e-6; + if (angle >= (PI - allowed_preccission) || + angle <= (-PI + allowed_preccission)) + continue; // it is almost line + + // perpendicular direction to line + Vec2d p_dir(dir.y(), -dir.x()); + p_dir.normalize(); // Should done with double preccission + // p_dir is tube unit side vector + // tube unit top vector is z direction + + // Tube + int prev_index = model.vertices.size() + 2 * sin_cos.size() - 2; + for (const auto &[s, c] : sin_cos) { + Vec2f side = (s * p_dir).cast(); + Vec2f xy0 = side + (*prev_point_d); + Vec2f xy1 = side + point_d; + model.vertices.emplace_back(xy0.x(), xy0.y(), c); // pointing of prev index + model.vertices.emplace_back(xy1.x(), xy1.y(), c); + + // create triangle indices + int f0 = prev_index; + int s0 = f0 + 1; + int f1 = model.vertices.size() - 2; + int s1 = f1 + 1; + prev_index = f1; + model.indices.emplace_back(s0, f0, s1); + model.indices.emplace_back(f1, s1, f0); + } + + prev_prev_point_d = prev_point_d; + prev_point_d = &point_d; + opposit_prev_dir = -dir; + } + + // sphere on each point + for (Vec2f& p: points_d){ + indexed_triangle_set sphere_copy = sphere; + its_translate(sphere_copy, Vec3f(p.x(), p.y(), 0.f)); + its_merge(model, sphere_copy); + } + + return model; +} + +// select closest contour for each line +TextLines select_closest_contour(const std::vector &line_contours) { + TextLines result; + result.reserve(line_contours.size()); + Vec2d zero(0., 0.); + for (const Polygons &polygons : line_contours){ + if (polygons.empty()) { + result.emplace_back(); + continue; + } + // Improve: use int values and polygons only + // Slic3r::Polygons polygons = union_(polygons); + // std::vector lines = to_lines(polygons); + // AABBTreeIndirect::Tree<2, Point> tree; + // size_t line_idx; + // Point hit_point; + // Point::Scalar distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, line_idx, hit_point); + + ExPolygons expolygons = union_ex(polygons); + std::vector linesf = to_linesf(expolygons); + AABBTreeIndirect::Tree2d tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf); + + size_t line_idx = 0; + Vec2d hit_point; + // double distance = + AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, zero, line_idx, hit_point); + + // conversion between index of point and expolygon + ExPolygonsIndices cvt(expolygons); + ExPolygonsIndex index = cvt.cvt(static_cast(line_idx)); + + const Slic3r::Polygon& polygon = index.is_contour() ? + expolygons[index.expolygons_index].contour : + expolygons[index.expolygons_index].holes[index.hole_index()]; + + Point hit_point_int = hit_point.cast(); + TextLine tl{polygon, PolygonPoint{index.point_index, hit_point_int}}; + result.emplace_back(tl); + } + return result; +} + +inline Eigen::AngleAxis get_rotation() { return Eigen::AngleAxis(-M_PI_2, Vec3d::UnitX()); } + +indexed_triangle_set create_its(const TextLines &lines, float radius) +{ + indexed_triangle_set its; + // create model from polygons + for (const TextLine &line : lines) { + const Slic3r::Polygon &polygon = line.polygon; + if (polygon.empty()) continue; + indexed_triangle_set line_its = its_create_torus(polygon, radius); + auto transl = Eigen::Translation3d(0., line.y, 0.); + Transform3d tr = transl * get_rotation(); + its_transform(line_its, tr); + its_merge(its, line_its); + } + return its; +} + +GLModel::Geometry create_geometry(const TextLines &lines, float radius, bool is_mirrored) +{ + indexed_triangle_set its = create_its(lines, radius); + + GLModel::Geometry geometry; + geometry.format = {GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3}; + ColorRGBA color(.7f, .7f, .7f, .7f); // Transparent Gray + geometry.color = color; + + geometry.reserve_vertices(its.vertices.size()); + for (Vec3f vertex : its.vertices) + geometry.add_vertex(vertex); + + geometry.reserve_indices(its.indices.size() * 3); + + if (is_mirrored) { + // change order of indices + for (Vec3i t : its.indices) + geometry.add_triangle(t[0], t[2], t[1]); + } else { + for (Vec3i t : its.indices) + geometry.add_triangle(t[0], t[1], t[2]); + } + return geometry; +} +} // namespace + +void TextLinesModel::init(const Transform3d &text_tr, + const ModelVolumePtrs &volumes_to_slice, + /*const*/ Emboss::StyleManager &style_manager, + unsigned count_lines) +{ + assert(style_manager.is_active_font()); + if (!style_manager.is_active_font()) + return; + const auto &ffc = style_manager.get_font_file_with_cache(); + assert(ffc.has_value()); + if (!ffc.has_value()) + return; + const auto &ff_ptr = ffc.font_file; + assert(ff_ptr != nullptr); + if (ff_ptr == nullptr) + return; + const FontFile &ff = *ff_ptr; + const FontProp &fp = style_manager.get_font_prop(); + + FontProp::VerticalAlign align = fp.align.second; + + double line_height_mm = calc_line_height_in_mm(ff, fp); + assert(line_height_mm > 0); + if (line_height_mm <= 0) + return; + + m_model.reset(); + m_lines.clear(); + + // size_in_mm .. contain volume scale and should be ascent value in mm + double line_offset = fp.size_in_mm * ascent_ratio_offset; + double first_line_center = line_offset + get_align_y_offset_in_mm(align, count_lines, ff, fp); + std::vector line_centers(count_lines); + for (size_t i = 0; i < count_lines; ++i) + line_centers[i] = static_cast(first_line_center - i * line_height_mm); + + // contour transformation + Transform3d c_trafo = text_tr * get_rotation(); + Transform3d c_trafo_inv = c_trafo.inverse(); + + std::vector line_contours(count_lines); + for (const ModelVolume *volume : volumes_to_slice) { + MeshSlicingParams slicing_params; + slicing_params.trafo = c_trafo_inv * volume->get_matrix(); + for (size_t i = 0; i < count_lines; ++i) { + const Polygons polys = Slic3r::slice_mesh(volume->mesh().its, line_centers[i], slicing_params); + if (polys.empty()) + continue; + Polygons &contours = line_contours[i]; + contours.insert(contours.end(), polys.begin(), polys.end()); + } + } + + // fix for text line out of object + // When move text close to edge - line center could be out of object + for (Polygons &contours: line_contours) { + if (!contours.empty()) + continue; + + // use line center at zero, there should be some contour. + float line_center = 0.f; + for (const ModelVolume *volume : volumes_to_slice) { + MeshSlicingParams slicing_params; + slicing_params.trafo = c_trafo_inv * volume->get_matrix(); + const Polygons polys = Slic3r::slice_mesh(volume->mesh().its, line_center, slicing_params); + if (polys.empty()) + continue; + contours.insert(contours.end(), polys.begin(), polys.end()); + } + } + + m_lines = select_closest_contour(line_contours); + assert(m_lines.size() == count_lines); + assert(line_centers.size() == count_lines); + for (size_t i = 0; i < count_lines; ++i) + m_lines[i].y = line_centers[i]; + + bool is_mirrored = has_reflection(text_tr); + float radius = static_cast(line_height_mm / 20.); + //* + GLModel::Geometry geometry = create_geometry(m_lines, radius, is_mirrored); + if (geometry.vertices_count() == 0 || geometry.indices_count() == 0) + return; + m_model.init_from(std::move(geometry)); + /*/ + // slower solution + ColorRGBA color(.7f, .7f, .7f, .7f); // Transparent Gray + m_model.set_color(color); + m_model.init_from(create_its(m_lines)); + //*/ +} + +void TextLinesModel::render(const Transform3d &text_world) +{ + if (!m_model.is_initialized()) + return; + + GUI_App &app = wxGetApp(); + const GLShaderProgram *shader = app.get_shader("flat"); + if (shader == nullptr) + return; + + const Camera &camera = app.plater()->get_camera(); + + shader->start_using(); + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * text_world); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + + bool is_depth_test = glIsEnabled(GL_DEPTH_TEST); + if (!is_depth_test) + glsafe(::glEnable(GL_DEPTH_TEST)); + + bool is_blend = glIsEnabled(GL_BLEND); + if (!is_blend) + glsafe(::glEnable(GL_BLEND)); + // glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + m_model.render(); + + if (!is_depth_test) + glsafe(::glDisable(GL_DEPTH_TEST)); + if (!is_blend) + glsafe(::glDisable(GL_BLEND)); + + shader->stop_using(); +} + +double TextLinesModel::calc_line_height_in_mm(const Slic3r::Emboss::FontFile &ff, const FontProp &fp) +{ + int line_height = Slic3r::Emboss::get_line_height(ff, fp); // In shape size + double scale = Slic3r::Emboss::get_text_shape_scale(fp, ff); + return line_height * scale; +} diff --git a/src/slic3r/GUI/TextLines.hpp b/src/slic3r/GUI/TextLines.hpp new file mode 100644 index 000000000..f6e297cc2 --- /dev/null +++ b/src/slic3r/GUI/TextLines.hpp @@ -0,0 +1,49 @@ +#ifndef slic3r_TextLines_hpp_ +#define slic3r_TextLines_hpp_ + +#include +#include +#include +#include +#include "slic3r/GUI/GLModel.hpp" +#include "slic3r/Utils/EmbossStyleManager.hpp" + +namespace Slic3r { +class ModelVolume; +typedef std::vector ModelVolumePtrs; +} + +namespace Slic3r::GUI { +class TextLinesModel +{ +public: + /// + /// Initialize model and lines + /// + /// Transformation of text volume inside object (aka inside of instance) + /// Vector of volumes to be sliced + /// Contain Font file, size and align + /// Count lines of embossed text(for veritcal alignment) + void init(const Transform3d &text_tr, const ModelVolumePtrs &volumes_to_slice, /*const*/ Emboss::StyleManager &style_manager, unsigned count_lines); + + void render(const Transform3d &text_world); + + bool is_init() const { return m_model.is_initialized(); } + void reset() { m_model.reset(); m_lines.clear(); } + const Slic3r::Emboss::TextLines &get_lines() const { return m_lines; } + + static double calc_line_height_in_mm(const Slic3r::Emboss::FontFile& ff, const FontProp& fp); // return lineheight in mm +private: + Slic3r::Emboss::TextLines m_lines; + + // Keep model for visualization text lines + GLModel m_model; + + // Used to move slice (text line) on place where is approx vertical center of text + // When copy value const double ASCENT_CENTER from Emboss.cpp and Vertical align is center than + // text line will cross object center + const double ascent_ratio_offset = 1/3.; +}; + +} // namespace Slic3r::GUI +#endif // slic3r_TextLines_hpp_ \ No newline at end of file diff --git a/src/slic3r/Utils/CalibUtils.cpp b/src/slic3r/Utils/CalibUtils.cpp index 1bc9e215c..95dae8aef 100644 --- a/src/slic3r/Utils/CalibUtils.cpp +++ b/src/slic3r/Utils/CalibUtils.cpp @@ -7,11 +7,13 @@ #include "libslic3r/CutUtils.hpp" #include "libslic3r/Model.hpp" +#include "slic3r/GUI/Jobs/BoostThreadWorker.hpp" +#include "slic3r/GUI/Jobs/PlaterWorker.hpp" namespace Slic3r { namespace GUI { -std::shared_ptr CalibUtils::print_job; +std::unique_ptr CalibUtils::print_worker; wxString wxstr_temp_dir = fs::path(fs::temp_directory_path() / "calib").wstring(); static const std::string temp_dir = wxstr_temp_dir.utf8_string(); static const std::string temp_gcode_path = temp_dir + "/temp.gcode"; @@ -1134,7 +1136,9 @@ void CalibUtils::send_to_print(const CalibInfo &calib_info, wxString &error_mess } } - print_job = std::make_shared(std::move(process_bar), wxGetApp().plater(), dev_id); + print_worker = std::make_unique>(wxGetApp().plater(), std::move(process_bar), "calib_worker"); + + auto print_job = std::make_unique(dev_id); print_job->m_dev_ip = obj_->dev_ip; print_job->m_ftp_folder = obj_->get_ftp_folder(); print_job->m_access_code = obj_->get_access_code(); @@ -1187,7 +1191,7 @@ void CalibUtils::send_to_print(const CalibInfo &calib_info, wxString &error_mess BOOST_LOG_TRIVIAL(info) << "send_cali_job - after send: " << j.dump(); } - print_job->start(); + replace_job(*print_worker, std::move(print_job)); } } diff --git a/src/slic3r/Utils/CalibUtils.hpp b/src/slic3r/Utils/CalibUtils.hpp index 55dbb5e32..320697f78 100644 --- a/src/slic3r/Utils/CalibUtils.hpp +++ b/src/slic3r/Utils/CalibUtils.hpp @@ -2,6 +2,7 @@ #include "libslic3r/calib.hpp" #include "../GUI/DeviceManager.hpp" #include "../GUI/Jobs/PrintJob.hpp" +#include "slic3r/GUI/Jobs/Worker.hpp" namespace Slic3r { @@ -26,7 +27,7 @@ class CalibUtils { public: CalibUtils(){}; - static std::shared_ptr print_job; + static std::unique_ptr print_worker; static CalibMode get_calib_mode_by_name(const std::string name, int &cali_stage); @@ -71,4 +72,4 @@ private: }; } -} \ No newline at end of file +} diff --git a/src/slic3r/Utils/EmbossStyleManager.cpp b/src/slic3r/Utils/EmbossStyleManager.cpp new file mode 100644 index 000000000..b367d4483 --- /dev/null +++ b/src/slic3r/Utils/EmbossStyleManager.cpp @@ -0,0 +1,788 @@ +///|/ Copyright (c) Prusa Research 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "EmbossStyleManager.hpp" +#include +#include // Imgui texture +#include // ImTextCharFromUtf8 +#include +#include // ScopeGuard + +#include "WxFontUtils.hpp" +#include "slic3r/GUI/3DScene.hpp" // ::glsafe +#include "slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" // check of font ranges + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI::Emboss; + +StyleManager::StyleManager(const ImWchar *language_glyph_range, const std::function& create_default_styles) + : m_create_default_styles(create_default_styles) + , m_imgui_init_glyph_range(language_glyph_range) +{} + +StyleManager::~StyleManager() { + clear_imgui_font(); + free_style_images(); +} + +/// +/// For store/load emboss style to/from AppConfig +/// +namespace { +void store_style_index(AppConfig &cfg, size_t index); +::std::optional load_style_index(const AppConfig &cfg); + +StyleManager::Styles load_styles(const AppConfig &cfg); +void store_styles(AppConfig &cfg, const StyleManager::Styles &styles); +void make_unique_name(const StyleManager::Styles &styles, std::string &name); +} // namespace + +void StyleManager::init(AppConfig *app_config) +{ + assert(app_config != nullptr); + m_app_config = app_config; + m_styles = ::load_styles(*app_config); + + if (m_styles.empty()) { + // No styles loaded from ini file so use default + EmbossStyles styles = m_create_default_styles(); + for (EmbossStyle &style : styles) { + ::make_unique_name(m_styles, style.name); + m_styles.push_back({style}); + } + } + + std::optional active_index_opt = (app_config != nullptr) ? + ::load_style_index(*app_config) : + std::optional{}; + + size_t active_index = 0; + if (active_index_opt.has_value()) active_index = *active_index_opt; + if (active_index >= m_styles.size()) active_index = 0; + + // find valid font item + if (load_style(active_index)) + return; // style is loaded + + // Try to fix that style can't be loaded + m_styles.erase(m_styles.begin() + active_index); + + load_valid_style(); +} + +bool StyleManager::store_styles_to_app_config(bool use_modification, bool store_active_index) +{ + assert(m_app_config != nullptr); + if (m_app_config == nullptr) return false; + if (use_modification) { + if (exist_stored_style()) { + // update stored item + m_styles[m_style_cache.style_index] = m_style_cache.style; + } else { + // add new into stored list + EmbossStyle &style = m_style_cache.style; + ::make_unique_name(m_styles, style.name); + m_style_cache.truncated_name.clear(); + m_style_cache.style_index = m_styles.size(); + m_styles.push_back({style}); + } + m_style_cache.stored_wx_font = m_style_cache.wx_font; + } + + if (store_active_index) + { + size_t style_index = exist_stored_style() ? + m_style_cache.style_index : + m_last_style_index; + store_style_index(*m_app_config, style_index); + } + + store_styles(*m_app_config, m_styles); + return true; +} + +void StyleManager::add_style(const std::string &name) { + EmbossStyle& style = m_style_cache.style; + style.name = name; + ::make_unique_name(m_styles, style.name); + m_style_cache.style_index = m_styles.size(); + m_style_cache.stored_wx_font = m_style_cache.wx_font; + m_style_cache.truncated_name.clear(); + m_styles.push_back({style}); +} + +void StyleManager::swap(size_t i1, size_t i2) { + if (i1 >= m_styles.size() || + i2 >= m_styles.size()) return; + std::swap(m_styles[i1], m_styles[i2]); + // fix selected index + if (!exist_stored_style()) return; + if (m_style_cache.style_index == i1) { + m_style_cache.style_index = i2; + } else if (m_style_cache.style_index == i2) { + m_style_cache.style_index = i1; + } +} + +void StyleManager::discard_style_changes() { + if (exist_stored_style()) { + if (load_style(m_style_cache.style_index)) + return; // correct reload style + } else { + if(load_style(m_last_style_index)) + return; // correct load last used style + } + + // try to save situation by load some font + load_valid_style(); +} + +void StyleManager::erase(size_t index) { + if (index >= m_styles.size()) return; + + // fix selected index + if (exist_stored_style()) { + size_t &i = m_style_cache.style_index; + if (index < i) --i; + else if (index == i) i = std::numeric_limits::max(); + } + + m_styles.erase(m_styles.begin() + index); +} + +void StyleManager::rename(const std::string& name) { + m_style_cache.style.name = name; + m_style_cache.truncated_name.clear(); + if (exist_stored_style()) { + Style &it = m_styles[m_style_cache.style_index]; + it.name = name; + it.truncated_name.clear(); + } +} + +void StyleManager::load_valid_style() +{ + // iterate over all known styles + while (!m_styles.empty()) { + if (load_style(0)) + return; + // can't load so erase it from list + m_styles.erase(m_styles.begin()); + } + + // no one style is loadable + // set up default font list + EmbossStyles def_style = m_create_default_styles(); + for (EmbossStyle &style : def_style) { + ::make_unique_name(m_styles, style.name); + m_styles.push_back({std::move(style)}); + } + + // iterate over default styles + // There have to be option to use build in font + while (!m_styles.empty()) { + if (load_style(0)) + return; + // can't load so erase it from list + m_styles.erase(m_styles.begin()); + } + + // This OS doesn't have TTF as default font, + // find some loadable font out of default list + assert(false); +} + +bool StyleManager::load_style(size_t style_index) +{ + if (style_index >= m_styles.size()) return false; + if (!load_style(m_styles[style_index])) return false; + m_style_cache.style_index = style_index; + m_style_cache.stored_wx_font = m_style_cache.wx_font; // copy + m_last_style_index = style_index; + return true; +} + +bool StyleManager::load_style(const Style &style) { + if (style.type == EmbossStyle::Type::file_path) { + std::unique_ptr font_ptr = + create_font_file(style.path.c_str()); + if (font_ptr == nullptr) return false; + m_style_cache.wx_font = {}; + m_style_cache.font_file = + FontFileWithCache(std::move(font_ptr)); + m_style_cache.style = style; // copy + m_style_cache.style_index = std::numeric_limits::max(); + m_style_cache.stored_wx_font = {}; + return true; + } + if (style.type != WxFontUtils::get_current_type()) return false; + std::optional wx_font_opt = WxFontUtils::load_wxFont(style.path); + if (!wx_font_opt.has_value()) return false; + return load_style(style, *wx_font_opt); +} + +bool StyleManager::load_style(const Style &style, const wxFont &font) +{ + m_style_cache.style = style; // copy + + // wx font property has bigger priority to set + // it must be after copy of the style + if (!set_wx_font(font)) return false; + + m_style_cache.style_index = std::numeric_limits::max(); + m_style_cache.stored_wx_font = {}; + m_style_cache.truncated_name.clear(); + return true; +} + +bool StyleManager::is_font_changed() const +{ + const wxFont &wx_font = get_wx_font(); + if (!wx_font.IsOk()) + return false; + if (!exist_stored_style()) + return false; + const EmbossStyle *stored_style = get_stored_style(); + if (stored_style == nullptr) + return false; + + const wxFont &wx_font_stored = get_stored_wx_font(); + if (!wx_font_stored.IsOk()) + return false; + + const FontProp &prop = get_style().prop; + const FontProp &prop_stored = stored_style->prop; + + // Exist change in face name? + if(wx_font_stored.GetFaceName() != wx_font.GetFaceName()) return true; + + const std::optional &skew = prop.skew; + bool is_italic = skew.has_value() || WxFontUtils::is_italic(wx_font); + const std::optional &skew_stored = prop_stored.skew; + bool is_stored_italic = skew_stored.has_value() || WxFontUtils::is_italic(wx_font_stored); + // is italic changed + if (is_italic != is_stored_italic) + return true; + + const std::optional &boldness = prop.boldness; + bool is_bold = boldness.has_value() || WxFontUtils::is_bold(wx_font); + const std::optional &boldness_stored = prop_stored.boldness; + bool is_stored_bold = boldness_stored.has_value() || WxFontUtils::is_bold(wx_font_stored); + // is bold changed + return is_bold != is_stored_bold; +} + +bool StyleManager::is_unique_style_name(const std::string &name) const { + for (const StyleManager::Style &style : m_styles) + if (style.name == name) + return false; + return true; +} + +bool StyleManager::is_active_font() { return m_style_cache.font_file.has_value(); } + +const StyleManager::Style *StyleManager::get_stored_style() const +{ + if (m_style_cache.style_index >= m_styles.size()) return nullptr; + return &m_styles[m_style_cache.style_index]; +} + +void StyleManager::clear_glyphs_cache() +{ + FontFileWithCache &ff = m_style_cache.font_file; + if (!ff.has_value()) return; + ff.cache = std::make_shared(); +} + +void StyleManager::clear_imgui_font() { m_style_cache.atlas.Clear(); } + +ImFont *StyleManager::get_imgui_font() +{ + if (!is_active_font()) return nullptr; + + ImVector &fonts = m_style_cache.atlas.Fonts; + if (fonts.empty()) return nullptr; + + // check correct index + int f_size = fonts.size(); + assert(f_size == 1); + if (f_size != 1) return nullptr; + ImFont *font = fonts.front(); + if (font == nullptr) return nullptr; + return font; +} + +const StyleManager::Styles &StyleManager::get_styles() const{ return m_styles; } +void StyleManager::init_trunc_names(float max_width) { + for (auto &s : m_styles) + if (s.truncated_name.empty()) { + std::string name = s.name; + ImGuiWrapper::escape_double_hash(name); + s.truncated_name = ImGuiWrapper::trunc(name, max_width); + } +} + +// for access to worker +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" + +// for get DPI +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp" + +void StyleManager::init_style_images(const Vec2i &max_size, + const std::string &text) +{ + // check already initialized + if (m_exist_style_images) return; + + // check is initializing + if (m_temp_style_images != nullptr) { + // is initialization finished + if (!m_temp_style_images->styles.empty()) { + assert(m_temp_style_images->images.size() == + m_temp_style_images->styles.size()); + // copy images into styles + for (StyleManager::StyleImage &image : m_temp_style_images->images){ + size_t index = &image - &m_temp_style_images->images.front(); + StyleImagesData::Item &style = m_temp_style_images->styles[index]; + + // find style in font list and copy to it + for (auto &it : m_styles) { + if (it.name != style.text || + !(it.prop == style.prop)) + continue; + it.image = image; + break; + } + } + m_temp_style_images = nullptr; + m_exist_style_images = true; + return; + } + // in process of initialization inside of job + return; + } + + // create job for init images + m_temp_style_images = std::make_shared(); + StyleImagesData::Items styles; + styles.reserve(m_styles.size()); + for (const Style &style : m_styles) { + std::optional wx_font_opt = WxFontUtils::load_wxFont(style.path); + if (!wx_font_opt.has_value()) continue; + std::unique_ptr font_file = + WxFontUtils::create_font_file(*wx_font_opt); + if (font_file == nullptr) continue; + styles.push_back({ + FontFileWithCache(std::move(font_file)), + style.name, + style.prop + }); + } + + auto mf = wxGetApp().mainframe; + // dot per inch for monitor + int dpi = get_dpi_for_window(mf); + // pixel per milimeter + double ppm = dpi / GizmoObjectManipulation::in_to_mm; + + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + StyleImagesData data{std::move(styles), max_size, text, m_temp_style_images, ppm}; + queue_job(worker, std::make_unique(std::move(data))); +} + +void StyleManager::free_style_images() { + if (!m_exist_style_images) return; + GLuint tex_id = 0; + for (Style &it : m_styles) { + if (tex_id == 0 && it.image.has_value()) + tex_id = (GLuint)(intptr_t) it.image->texture_id; + it.image.reset(); + } + if (tex_id != 0) + glsafe(::glDeleteTextures(1, &tex_id)); + m_exist_style_images = false; +} + +float StyleManager::min_imgui_font_size = 18.f; +float StyleManager::max_imgui_font_size = 60.f; +float StyleManager::get_imgui_font_size(const FontProp &prop, const FontFile &file, double scale) +{ + const FontFile::Info& info = get_font_info(file, prop); + // coeficient for convert line height to font size + float c1 = (info.ascent - info.descent + info.linegap) / + (float) info.unit_per_em; + + // The point size is defined as 1/72 of the Anglo-Saxon inch (25.4 mm): + // It is approximately 0.0139 inch or 352.8 um. + return c1 * std::abs(prop.size_in_mm) / 0.3528f * scale; +} + +ImFont *StyleManager::create_imgui_font(const std::string &text, double scale) +{ + // inspiration inside of ImGuiWrapper::init_font + auto& ff = m_style_cache.font_file; + if (!ff.has_value()) return nullptr; + const FontFile &font_file = *ff.font_file; + + ImFontGlyphRangesBuilder builder; + builder.AddRanges(m_imgui_init_glyph_range); + if (!text.empty()) + builder.AddText(text.c_str()); + + ImVector &ranges = m_style_cache.ranges; + ranges.clear(); + builder.BuildRanges(&ranges); + + m_style_cache.atlas.Flags |= ImFontAtlasFlags_NoMouseCursors | + ImFontAtlasFlags_NoPowerOfTwoHeight; + + const FontProp &font_prop = m_style_cache.style.prop; + float font_size = get_imgui_font_size(font_prop, font_file, scale); + if (font_size < min_imgui_font_size) + font_size = min_imgui_font_size; + if (font_size > max_imgui_font_size) + font_size = max_imgui_font_size; + + ImFontConfig font_config; + // TODO: start using merge mode + //font_config.MergeMode = true; + int unit_per_em = get_font_info(font_file, font_prop).unit_per_em; + float coef = font_size / (double) unit_per_em; + if (font_prop.char_gap.has_value()) + font_config.GlyphExtraSpacing.x = coef * (*font_prop.char_gap); + if (font_prop.line_gap.has_value()) + font_config.GlyphExtraSpacing.y = coef * (*font_prop.line_gap); + + font_config.FontDataOwnedByAtlas = false; + + const std::vector &buffer = *font_file.data; + ImFont * font = m_style_cache.atlas.AddFontFromMemoryTTF( + (void *) buffer.data(), buffer.size(), font_size, &font_config, m_style_cache.ranges.Data); + + unsigned char *pixels; + int width, height; + m_style_cache.atlas.GetTexDataAsRGBA32(&pixels, &width, &height); + + // Upload texture to graphics system + GLint last_texture; + glsafe(::glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture)); + ScopeGuard sg([last_texture]() { + glsafe(::glBindTexture(GL_TEXTURE_2D, last_texture)); + }); + + GLuint font_texture; + glsafe(::glGenTextures(1, &font_texture)); + glsafe(::glBindTexture(GL_TEXTURE_2D, font_texture)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)); + if (OpenGLManager::are_compressed_textures_supported()) + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels)); + else + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels)); + + // Store our identifier + m_style_cache.atlas.TexID = (ImTextureID) (intptr_t) font_texture; + assert(!m_style_cache.atlas.Fonts.empty()); + if (m_style_cache.atlas.Fonts.empty()) return nullptr; + assert(font == m_style_cache.atlas.Fonts.back()); + if (!font->IsLoaded()) return nullptr; + assert(font->IsLoaded()); + return font; +} + +bool StyleManager::set_wx_font(const wxFont &wx_font) { + std::unique_ptr font_file = + WxFontUtils::create_font_file(wx_font); + return set_wx_font(wx_font, std::move(font_file)); +} + +bool StyleManager::set_wx_font(const wxFont &wx_font, std::unique_ptr font_file) +{ + if (font_file == nullptr) return false; + m_style_cache.wx_font = wx_font; // copy + m_style_cache.font_file = + FontFileWithCache(std::move(font_file)); + + EmbossStyle &style = m_style_cache.style; + style.type = WxFontUtils::get_current_type(); + // update string path + style.path = WxFontUtils::store_wxFont(wx_font); + WxFontUtils::update_property(style.prop, wx_font); + clear_imgui_font(); + return true; +} + +#include +#include "WxFontUtils.hpp" +#include "fast_float/fast_float.h" + +// StylesSerializable +namespace { + +using namespace Slic3r; +using namespace Slic3r::GUI; +using Section = std::map; + +const std::string APP_CONFIG_FONT_NAME = "name"; +const std::string APP_CONFIG_FONT_DESCRIPTOR = "descriptor"; +const std::string APP_CONFIG_FONT_LINE_HEIGHT = "line_height"; +const std::string APP_CONFIG_FONT_DEPTH = "depth"; +const std::string APP_CONFIG_FONT_USE_SURFACE = "use_surface"; +const std::string APP_CONFIG_FONT_BOLDNESS = "boldness"; +const std::string APP_CONFIG_FONT_SKEW = "skew"; +const std::string APP_CONFIG_FONT_DISTANCE = "distance"; +const std::string APP_CONFIG_FONT_ANGLE = "angle"; +const std::string APP_CONFIG_FONT_COLLECTION = "collection"; +const std::string APP_CONFIG_FONT_CHAR_GAP = "char_gap"; +const std::string APP_CONFIG_FONT_LINE_GAP = "line_gap"; + +const std::string APP_CONFIG_ACTIVE_FONT = "active_font"; + +std::string create_section_name(unsigned index) +{ + return AppConfig::SECTION_EMBOSS_STYLE + ':' + std::to_string(index); +} + +// check only existence of flag +bool read(const Section §ion, const std::string &key, bool &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + + value = true; + return true; +} + +bool read(const Section §ion, const std::string &key, float &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + float value_; + fast_float::from_chars(data.c_str(), data.c_str() + data.length(), value_); + // read only non zero value + if (fabs(value_) <= std::numeric_limits::epsilon()) + return false; + + value = value_; + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + int value_ = std::atoi(data.c_str()); + if (value_ == 0) + return false; + + value = value_; + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + int value_ = std::atoi(data.c_str()); + if (value_ <= 0) + return false; + + value = static_cast(value_); + return true; +} + +bool read(const Section §ion, const std::string &key, std::optional &value) +{ + auto item = section.find(key); + if (item == section.end()) + return false; + const std::string &data = item->second; + if (data.empty()) + return false; + float value_; + fast_float::from_chars(data.c_str(), data.c_str() + data.length(), value_); + // read only non zero value + if (fabs(value_) <= std::numeric_limits::epsilon()) + return false; + + value = value_; + return true; +} + +std::optional load_style(const Section &app_cfg_section) +{ + auto path_it = app_cfg_section.find(APP_CONFIG_FONT_DESCRIPTOR); + if (path_it == app_cfg_section.end()) + return {}; + + StyleManager::Style s; + EmbossProjection& ep = s.projection; + FontProp& fp = s.prop; + + s.path = path_it->second; + s.type = WxFontUtils::get_current_type(); + auto name_it = app_cfg_section.find(APP_CONFIG_FONT_NAME); + const std::string default_name = "font_name"; + s.name = (name_it == app_cfg_section.end()) ? default_name : name_it->second; + + read(app_cfg_section, APP_CONFIG_FONT_LINE_HEIGHT, fp.size_in_mm); + float depth = 1.; + read(app_cfg_section, APP_CONFIG_FONT_DEPTH, depth); + ep.depth = depth; + read(app_cfg_section, APP_CONFIG_FONT_USE_SURFACE, ep.use_surface); + read(app_cfg_section, APP_CONFIG_FONT_BOLDNESS, fp.boldness); + read(app_cfg_section, APP_CONFIG_FONT_SKEW, fp.skew); + read(app_cfg_section, APP_CONFIG_FONT_DISTANCE, s.distance); + read(app_cfg_section, APP_CONFIG_FONT_ANGLE, s.angle); + read(app_cfg_section, APP_CONFIG_FONT_COLLECTION, fp.collection_number); + read(app_cfg_section, APP_CONFIG_FONT_CHAR_GAP, fp.char_gap); + read(app_cfg_section, APP_CONFIG_FONT_LINE_GAP, fp.line_gap); + return s; +} + +void store_style(AppConfig &cfg, const StyleManager::Style &s, unsigned index) +{ + const EmbossProjection &ep = s.projection; + Section data; + data[APP_CONFIG_FONT_NAME] = s.name; + data[APP_CONFIG_FONT_DESCRIPTOR] = s.path; + const FontProp &fp = s.prop; + data[APP_CONFIG_FONT_LINE_HEIGHT] = std::to_string(fp.size_in_mm); + data[APP_CONFIG_FONT_DEPTH] = std::to_string(ep.depth); + if (ep.use_surface) + data[APP_CONFIG_FONT_USE_SURFACE] = "true"; + if (fp.boldness.has_value()) + data[APP_CONFIG_FONT_BOLDNESS] = std::to_string(*fp.boldness); + if (fp.skew.has_value()) + data[APP_CONFIG_FONT_SKEW] = std::to_string(*fp.skew); + if (s.distance.has_value()) + data[APP_CONFIG_FONT_DISTANCE] = std::to_string(*s.distance); + if (s.angle.has_value()) + data[APP_CONFIG_FONT_ANGLE] = std::to_string(*s.angle); + if (fp.collection_number.has_value()) + data[APP_CONFIG_FONT_COLLECTION] = std::to_string(*fp.collection_number); + if (fp.char_gap.has_value()) + data[APP_CONFIG_FONT_CHAR_GAP] = std::to_string(*fp.char_gap); + if (fp.line_gap.has_value()) + data[APP_CONFIG_FONT_LINE_GAP] = std::to_string(*fp.line_gap); + cfg.set_section(create_section_name(index), std::move(data)); +} + +void store_style_index(AppConfig &cfg, size_t index) +{ + // store actual font index + // active font first index is +1 to correspond with section name + Section data; + data[APP_CONFIG_ACTIVE_FONT] = std::to_string(index); + cfg.set_section(AppConfig::SECTION_EMBOSS_STYLE, std::move(data)); +} + +std::optional load_style_index(const AppConfig &cfg) +{ + if (!cfg.has_section(AppConfig::SECTION_EMBOSS_STYLE)) + return {}; + + auto section = cfg.get_section(AppConfig::SECTION_EMBOSS_STYLE); + auto it = section.find(APP_CONFIG_ACTIVE_FONT); + if (it == section.end()) + return {}; + + size_t active_font = static_cast(std::atoi(it->second.c_str())); + // order in config starts with number 1 + return active_font - 1; +} + +::StyleManager::Styles load_styles(const AppConfig &cfg) +{ + StyleManager::Styles result; + // human readable index inside of config starts from 1 !! + unsigned index = 1; + std::string section_name = create_section_name(index); + while (cfg.has_section(section_name)) { + std::optional style_opt = load_style(cfg.get_section(section_name)); + if (style_opt.has_value()) { + make_unique_name(result, style_opt->name); + result.emplace_back(*style_opt); + } + + section_name = create_section_name(++index); + } + return result; +} + +void store_styles(AppConfig &cfg, const StyleManager::Styles &styles) +{ + EmbossStyle::Type current_type = WxFontUtils::get_current_type(); + // store styles + unsigned index = 1; + for (const StyleManager::Style &style : styles) { + // skip file paths + fonts from other OS(loaded from .3mf) + assert(style.type == current_type); + if (style.type != current_type) + continue; + store_style(cfg, style, index); + ++index; + } + + // remove rest of font sections (after deletation) + std::string section_name = create_section_name(index); + while (cfg.has_section(section_name)) { + cfg.clear_section(section_name); + section_name = create_section_name(index); + ++index; + } +} + +void make_unique_name(const StyleManager::Styles& styles, std::string &name) +{ + auto is_unique = [&styles](const std::string &name){ + for (const StyleManager::Style &it : styles) + if (it.name == name) return false; + return true; + }; + + // Style name can't be empty so default name is set + if (name.empty()) name = "Text style"; + + // When name is already unique, nothing need to be changed + if (is_unique(name)) return; + + // when there is previous version of style name only find number + const char *prefix = " ("; + const char suffix = ')'; + auto pos = name.find_last_of(prefix); + if (name.c_str()[name.size() - 1] == suffix && + pos != std::string::npos) { + // short name by ord number + name = name.substr(0, pos); + } + + int order = 1; // start with value 2 to represents same font name + std::string new_name; + do { + new_name = name + prefix + std::to_string(++order) + suffix; + } while (!is_unique(new_name)); + name = new_name; +} + +} // namespace diff --git a/src/slic3r/Utils/EmbossStyleManager.hpp b/src/slic3r/Utils/EmbossStyleManager.hpp new file mode 100644 index 000000000..d36f062f4 --- /dev/null +++ b/src/slic3r/Utils/EmbossStyleManager.hpp @@ -0,0 +1,325 @@ +///|/ Copyright (c) Prusa Research 2022 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_EmbossStyleManager_hpp_ +#define slic3r_EmbossStyleManager_hpp_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r::GUI::Emboss { +/// +/// Manage Emboss text styles +/// Cache actual state of style +/// + imgui font +/// + wx font +/// +class StyleManager +{ + friend class CreateFontStyleImagesJob; // access to StyleImagesData +public: + /// Character to load for imgui when initialize imgui font + /// Function to create default styles + StyleManager(const ImWchar *language_glyph_range, const std::function& create_default_styles); + + /// + /// Release imgui font and style images from GPU + /// + ~StyleManager(); + + /// + /// Load font style list from config + /// Also select actual activ font + /// + /// Application configuration loaded from file "PrusaSlicer.ini" + /// + cfg is stored to privat variable + void init(AppConfig *app_config); + + /// + /// Write font list into AppConfig + /// + /// Configuration + /// When true cache state will be used for store + /// When true store activ index into configuration + /// True on succes otherwise False. + bool store_styles_to_app_config(bool use_modification = true, bool store_active_index = true); + + /// + /// Append actual style to style list + /// + /// New name for style + void add_style(const std::string& name); + + /// + /// Change order of style item in m_styles. + /// Fix selected font index when (i1 || i2) == m_font_selected + /// + /// First index to m_styles + /// Second index to m_styles + void swap(size_t i1, size_t i2); + + /// + /// Discard changes in activ style + /// When no activ style use last used OR first loadable + /// + void discard_style_changes(); + + /// + /// Remove style from m_styles. + /// Fix selected font index when index is under m_font_selected + /// + /// Index of style to be removed + void erase(size_t index); + + /// + /// Rename actual selected font item + /// + /// New name + void rename(const std::string &name); + + /// + /// load some valid style + /// + void load_valid_style(); + + /// + /// Change active font + /// When font not loaded roll back activ font + /// + /// New font index(from m_styles range) + /// True on succes. False on fail load font + bool load_style(size_t font_index); + // load font style not stored in list + struct Style; + bool load_style(const Style &style); + // fastering load font on index by wxFont, ignore type and descriptor + bool load_style(const Style &style, const wxFont &font); + + // clear actual selected glyphs cache + void clear_glyphs_cache(); + + // remove cached imgui font for actual selected font + void clear_imgui_font(); + + // getters for private data + const Style *get_stored_style() const; + + const Style &get_style() const { return m_style_cache.style; } + Style &get_style() { return m_style_cache.style; } + size_t get_style_index() const { return m_style_cache.style_index; } + std::string &get_truncated_name() { return m_style_cache.truncated_name; } + const ImFontAtlas &get_atlas() const { return m_style_cache.atlas; } + const FontProp &get_font_prop() const { return get_style().prop; } + FontProp &get_font_prop() { return get_style().prop; } + const wxFont &get_wx_font() const { return m_style_cache.wx_font; } + const wxFont &get_stored_wx_font() const { return m_style_cache.stored_wx_font; } + Slic3r::Emboss::FontFileWithCache &get_font_file_with_cache() { return m_style_cache.font_file; } + bool has_collections() const { return m_style_cache.font_file.font_file != nullptr && + m_style_cache.font_file.font_file->infos.size() > 1; } + + // True when activ style has same name as some of stored style + bool exist_stored_style() const { return m_style_cache.style_index != std::numeric_limits::max(); } + + /// + /// check whether current style differ to selected + /// + /// + bool is_font_changed() const; + + bool is_unique_style_name(const std::string &name) const; + + /// + /// Setter on wx_font when changed + /// + /// new wx font + /// True on success set otherwise FALSE + bool set_wx_font(const wxFont &wx_font); + + /// + /// Faster way of set wx_font when font file is known(do not load font file twice) + /// When you not sure that wx_font is made by font_file use only set_wx_font(wx_font) + /// + /// Must be source of font file + /// font file created by WxFontUtils::create_font_file(wx_font) + /// True on success otherwise false + bool set_wx_font(const wxFont &wx_font, std::unique_ptr font_file); + + // Getter on acitve font pointer for imgui + // Initialize imgui font(generate texture) when doesn't exist yet. + // Extend font atlas when not in glyph range + ImFont *get_imgui_font(); + // initialize font range by unique symbols in text + ImFont *create_imgui_font(const std::string& text, double scale); + + // init truncated names of styles + void init_trunc_names(float max_width); + + /// + /// Initialization texture with rendered font style + /// + /// Maximal width and height of one style texture + /// Text to render by style + void init_style_images(const Vec2i& max_size, const std::string &text); + void free_style_images(); + + // access to all managed font styles + const std::vector