diff --git a/CMakeLists.txt b/CMakeLists.txt index 48ad5033e0..1d4576c37d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,11 @@ option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0) set(SLIC3R_GTK "2" CACHE STRING "GTK version to use with wxWidgets on Linux") +if (APPLE) + set(CMAKE_FIND_FRAMEWORK LAST) + set(CMAKE_FIND_APPBUNDLE LAST) +endif () + # Proposal for C++ unit tests and sandboxes option(SLIC3R_BUILD_SANDBOXES "Build development sandboxes" OFF) option(SLIC3R_BUILD_TESTS "Build unit tests" ON) @@ -386,7 +391,7 @@ if (NOT EXPAT_FOUND) set(EXPAT_LIBRARIES expat) endif () -find_package(PNG) +find_package(PNG REQUIRED) find_package(OpenGL REQUIRED) diff --git a/deps/deps-linux.cmake b/deps/deps-linux.cmake index 3ad3cca64f..ae972327f8 100644 --- a/deps/deps-linux.cmake +++ b/deps/deps-linux.cmake @@ -3,10 +3,11 @@ set(DEP_CMAKE_OPTS "-DCMAKE_POSITION_INDEPENDENT_CODE=ON") include("deps-unix-common.cmake") -find_package(PNG QUIET) -if (NOT PNG_FOUND) - message(WARNING "No PNG dev package found in system, building static library. You should install the system package.") -endif () +# Some Linuxes may have very old libpng, so it's best to bundle it instead of relying on the system version. +# find_package(PNG QUIET) +# if (NOT PNG_FOUND) +# message(WARNING "No PNG dev package found in system, building static library. You should install the system package.") +# endif () #TODO UDEV diff --git a/doc/How to build - Mac OS.md b/doc/How to build - Mac OS.md index 082c560b7a..bab40ea265 100644 --- a/doc/How to build - Mac OS.md +++ b/doc/How to build - Mac OS.md @@ -79,3 +79,29 @@ This is set in the property list file /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Info.plist To remove the limitation, simply delete the key `MinimumSDKVersion` from that file. + + +# TL; DR + +Works on a fresh installation of MacOS Catalina 10.15.6 + +- Install [brew](https://brew.sh/): +- Open Terminal + +- Enter: + +```brew install cmake git gettext +brew update +brew upgrade +git clone https://github.com/prusa3d/PrusaSlicer/ +cd PrusaSlicer/deps +mkdir build +cd build +cmake .. +make +cd ../.. +mkdir build +cd build +cmake .. -DCMAKE_PREFIX_PATH="$PWD/../deps/build/destdir/usr/local" +make +src/prusa-slicer diff --git a/resources/icons/PrusaSlicer-gcodeviewer.ico b/resources/icons/PrusaSlicer-gcodeviewer.ico new file mode 100644 index 0000000000..2c9272369f Binary files /dev/null and b/resources/icons/PrusaSlicer-gcodeviewer.ico differ diff --git a/resources/icons/PrusaSlicer-gcodeviewer_128px.png b/resources/icons/PrusaSlicer-gcodeviewer_128px.png new file mode 100644 index 0000000000..d8e3e438be Binary files /dev/null and b/resources/icons/PrusaSlicer-gcodeviewer_128px.png differ diff --git a/resources/icons/PrusaSlicer-gcodeviewer_192px.png b/resources/icons/PrusaSlicer-gcodeviewer_192px.png new file mode 100644 index 0000000000..0e02430127 Binary files /dev/null and b/resources/icons/PrusaSlicer-gcodeviewer_192px.png differ diff --git a/resources/icons/add.svg b/resources/icons/add.svg index 8a9b253de7..37050d7481 100644 --- a/resources/icons/add.svg +++ b/resources/icons/add.svg @@ -1,17 +1,22 @@ - + - - + + + + + + + diff --git a/resources/icons/arrange.svg b/resources/icons/arrange.svg index 4f30e979e3..62cf939e9f 100644 --- a/resources/icons/arrange.svg +++ b/resources/icons/arrange.svg @@ -1,24 +1,23 @@ - + - - + - + - + - + - + diff --git a/resources/icons/cancel.svg b/resources/icons/cancel.svg new file mode 100644 index 0000000000..da44606a08 --- /dev/null +++ b/resources/icons/cancel.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/resources/icons/copy.svg b/resources/icons/copy.svg index 9b8430dd79..345c2590be 100644 --- a/resources/icons/copy.svg +++ b/resources/icons/copy.svg @@ -1,37 +1,29 @@ - + - - - - - + + - - - - - + + diff --git a/resources/icons/cross_focus_large.svg b/resources/icons/cross_focus_large.svg new file mode 100644 index 0000000000..c246f2bd9e --- /dev/null +++ b/resources/icons/cross_focus_large.svg @@ -0,0 +1,81 @@ + +image/svg+xml + + + + + + + + + + + diff --git a/resources/icons/delete_all.svg b/resources/icons/delete_all.svg index 80e2e503cb..dfa9438129 100644 --- a/resources/icons/delete_all.svg +++ b/resources/icons/delete_all.svg @@ -1,31 +1,17 @@ - + - - + - + - - - - - - - - - - + diff --git a/resources/icons/instance_add.svg b/resources/icons/instance_add.svg index 5ef492cfae..a466c51dbf 100644 --- a/resources/icons/instance_add.svg +++ b/resources/icons/instance_add.svg @@ -1,50 +1,46 @@ - + - + - + - + + + + diff --git a/resources/icons/instance_remove.svg b/resources/icons/instance_remove.svg index 466752ea89..7f9b4f7e1c 100644 --- a/resources/icons/instance_remove.svg +++ b/resources/icons/instance_remove.svg @@ -1,49 +1,42 @@ - + - + - + - + diff --git a/resources/icons/ironing.svg b/resources/icons/ironing.svg new file mode 100644 index 0000000000..94917d6bfe --- /dev/null +++ b/resources/icons/ironing.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/notification_close.svg b/resources/icons/notification_close.svg new file mode 100644 index 0000000000..708d8bfef1 --- /dev/null +++ b/resources/icons/notification_close.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/resources/icons/notification_close_hover.svg b/resources/icons/notification_close_hover.svg new file mode 100644 index 0000000000..a04dce21ad --- /dev/null +++ b/resources/icons/notification_close_hover.svg @@ -0,0 +1,66 @@ + +image/svg+xml + + + + + + + + + + diff --git a/resources/icons/notification_error.svg b/resources/icons/notification_error.svg new file mode 100644 index 0000000000..5356e7af6e --- /dev/null +++ b/resources/icons/notification_error.svg @@ -0,0 +1,71 @@ + +image/svg+xml + + + + + + + + + + diff --git a/resources/icons/notification_minimalize.svg b/resources/icons/notification_minimalize.svg new file mode 100644 index 0000000000..bb3ae9b7a1 --- /dev/null +++ b/resources/icons/notification_minimalize.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/resources/icons/notification_minimalize_hover.svg b/resources/icons/notification_minimalize_hover.svg new file mode 100644 index 0000000000..bc5bc6cca1 --- /dev/null +++ b/resources/icons/notification_minimalize_hover.svg @@ -0,0 +1,58 @@ + +image/svg+xml + + + + + + + diff --git a/resources/icons/notification_warning.svg b/resources/icons/notification_warning.svg new file mode 100644 index 0000000000..6ba7a046d8 --- /dev/null +++ b/resources/icons/notification_warning.svg @@ -0,0 +1,70 @@ + +image/svg+xml + + + + + + + + + + diff --git a/resources/icons/paste.svg b/resources/icons/paste.svg index 028ffb8ea0..bcfe567de3 100644 --- a/resources/icons/paste.svg +++ b/resources/icons/paste.svg @@ -1,27 +1,22 @@ - + - + - - - - - + + diff --git a/resources/icons/prusa_slicer_logo.svg b/resources/icons/prusa_slicer_logo.svg new file mode 100644 index 0000000000..927c3e70ba --- /dev/null +++ b/resources/icons/prusa_slicer_logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/icons/redo_toolbar.svg b/resources/icons/redo_toolbar.svg index d005f83736..d2aca2cc7d 100644 --- a/resources/icons/redo_toolbar.svg +++ b/resources/icons/redo_toolbar.svg @@ -1,13 +1,17 @@ - + - + viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve"> + + + + + + + + diff --git a/resources/icons/remove.svg b/resources/icons/remove.svg index acd21256cd..1bb830d91c 100644 --- a/resources/icons/remove.svg +++ b/resources/icons/remove.svg @@ -1,44 +1,60 @@ - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/seam.svg b/resources/icons/seam.svg new file mode 100644 index 0000000000..a7e7980cc0 --- /dev/null +++ b/resources/icons/seam.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/search_.svg b/resources/icons/search_.svg index 679bb30f71..2985ceb561 100644 --- a/resources/icons/search_.svg +++ b/resources/icons/search_.svg @@ -1,4 +1,26 @@ - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/search_blink.svg b/resources/icons/search_blink.svg new file mode 100644 index 0000000000..d005f83736 --- /dev/null +++ b/resources/icons/search_blink.svg @@ -0,0 +1,13 @@ + + + + + diff --git a/resources/icons/settings.svg b/resources/icons/settings.svg new file mode 100644 index 0000000000..db5bf458d7 --- /dev/null +++ b/resources/icons/settings.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/splashscreen-gcodeviewer.jpg b/resources/icons/splashscreen-gcodeviewer.jpg new file mode 100644 index 0000000000..f170f390c2 Binary files /dev/null and b/resources/icons/splashscreen-gcodeviewer.jpg differ diff --git a/resources/icons/splashscreen.jpg b/resources/icons/splashscreen.jpg new file mode 100644 index 0000000000..86c55f30f6 Binary files /dev/null and b/resources/icons/splashscreen.jpg differ diff --git a/resources/icons/split_objects.svg b/resources/icons/split_objects.svg index a7ccc5df86..e822fd35aa 100644 --- a/resources/icons/split_objects.svg +++ b/resources/icons/split_objects.svg @@ -1,19 +1,20 @@ - + - + - + - - + + + + diff --git a/resources/icons/split_parts.svg b/resources/icons/split_parts.svg index 82a2927706..5cfef0f330 100644 --- a/resources/icons/split_parts.svg +++ b/resources/icons/split_parts.svg @@ -1,18 +1,20 @@ - + - + - + - - + + + + diff --git a/resources/icons/thumb_left.svg b/resources/icons/thumb_left.svg new file mode 100644 index 0000000000..ef78bd1410 --- /dev/null +++ b/resources/icons/thumb_left.svg @@ -0,0 +1,54 @@ + +image/svg+xml + + + + + + diff --git a/resources/icons/thumb_right.svg b/resources/icons/thumb_right.svg new file mode 100644 index 0000000000..f3748525d2 --- /dev/null +++ b/resources/icons/thumb_right.svg @@ -0,0 +1,54 @@ + +image/svg+xml + + + + + + diff --git a/resources/icons/tick_mark.svg b/resources/icons/tick_mark.svg new file mode 100644 index 0000000000..4ccab2192d --- /dev/null +++ b/resources/icons/tick_mark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/icons/timer_dot.svg b/resources/icons/timer_dot.svg new file mode 100644 index 0000000000..3a77962b60 --- /dev/null +++ b/resources/icons/timer_dot.svg @@ -0,0 +1,72 @@ + +image/svg+xml + + + + diff --git a/resources/icons/timer_dot_empty.svg b/resources/icons/timer_dot_empty.svg new file mode 100644 index 0000000000..a8e776b49e --- /dev/null +++ b/resources/icons/timer_dot_empty.svg @@ -0,0 +1,73 @@ + + + +image/svg+xml + + + + \ No newline at end of file diff --git a/resources/icons/undo_toolbar.svg b/resources/icons/undo_toolbar.svg index 15778a7baf..2fc25bf737 100644 --- a/resources/icons/undo_toolbar.svg +++ b/resources/icons/undo_toolbar.svg @@ -1,13 +1,17 @@ - + - + viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve"> + + + + + + + + diff --git a/resources/shaders/gouraud_light.fs b/resources/shaders/gouraud_light.fs new file mode 100644 index 0000000000..1a58abc852 --- /dev/null +++ b/resources/shaders/gouraud_light.fs @@ -0,0 +1,11 @@ +#version 110 + +uniform vec4 uniform_color; + +// x = tainted, y = specular; +varying vec2 intensity; + +void main() +{ + gl_FragColor = vec4(vec3(intensity.y, intensity.y, intensity.y) + uniform_color.rgb * intensity.x, uniform_color.a); +} diff --git a/resources/shaders/gouraud_light.vs b/resources/shaders/gouraud_light.vs new file mode 100644 index 0000000000..d4f71938a9 --- /dev/null +++ b/resources/shaders/gouraud_light.vs @@ -0,0 +1,38 @@ +#version 110 + +#define INTENSITY_CORRECTION 0.6 + +// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION) +#define LIGHT_TOP_SHININESS 20.0 + +// normalized values for (1./1.43, 0.2/1.43, 1./1.43) +const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074); +#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION) + +#define INTENSITY_AMBIENT 0.3 + +// x = tainted, y = specular; +varying vec2 intensity; + +void main() +{ + // First transform the normal into camera space and normalize the result. + vec3 normal = normalize(gl_NormalMatrix * gl_Normal); + + // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. + // Since these two are normalized the cosine is the dot product. We also need to clamp the result to the [0,1] range. + float NdotL = max(dot(normal, LIGHT_TOP_DIR), 0.0); + + intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE; + vec3 position = (gl_ModelViewMatrix * gl_Vertex).xyz; + intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, normal)), 0.0), LIGHT_TOP_SHININESS); + + // Perform the same lighting calculation for the 2nd light source (no specular applied). + NdotL = max(dot(normal, LIGHT_FRONT_DIR), 0.0); + intensity.x += NdotL * LIGHT_FRONT_DIFFUSE; + + gl_Position = ftransform(); +} diff --git a/resources/shaders/options_110.fs b/resources/shaders/options_110.fs new file mode 100644 index 0000000000..ab656998df --- /dev/null +++ b/resources/shaders/options_110.fs @@ -0,0 +1,8 @@ +#version 110 + +uniform vec4 uniform_color; + +void main() +{ + gl_FragColor = uniform_color; +} diff --git a/resources/shaders/options_110.vs b/resources/shaders/options_110.vs new file mode 100644 index 0000000000..7592f86a42 --- /dev/null +++ b/resources/shaders/options_110.vs @@ -0,0 +1,11 @@ +#version 110 + +uniform float zoom; +uniform float point_size; +uniform float near_plane_height; + +void main() +{ + gl_Position = ftransform(); + gl_PointSize = (gl_Position.w == 1.0) ? zoom * near_plane_height * point_size : near_plane_height * point_size / gl_Position.w; +} diff --git a/resources/shaders/options_120.fs b/resources/shaders/options_120.fs new file mode 100644 index 0000000000..e9b61304f2 --- /dev/null +++ b/resources/shaders/options_120.fs @@ -0,0 +1,22 @@ +// version 120 is needed for gl_PointCoord +#version 120 + +uniform vec4 uniform_color; +uniform float percent_outline_radius; +uniform float percent_center_radius; + +vec4 calc_color(float radius, vec4 color) +{ + return ((radius < percent_center_radius) || (radius > 1.0 - percent_outline_radius)) ? + vec4(0.5 * color.rgb, color.a) : color; +} + +void main() +{ + vec2 pos = (gl_PointCoord - 0.5) * 2.0; + float radius = length(pos); + if (radius > 1.0) + discard; + + gl_FragColor = calc_color(radius, uniform_color); +} diff --git a/resources/shaders/options_120.vs b/resources/shaders/options_120.vs new file mode 100644 index 0000000000..baf3cd3a7f --- /dev/null +++ b/resources/shaders/options_120.vs @@ -0,0 +1,11 @@ +#version 120 + +uniform float zoom; +uniform float point_size; +uniform float near_plane_height; + +void main() +{ + gl_Position = ftransform(); + gl_PointSize = (gl_Position.w == 1.0) ? zoom * near_plane_height * point_size : near_plane_height * point_size / gl_Position.w; +} diff --git a/resources/shaders/toolpaths_lines.fs b/resources/shaders/toolpaths_lines.fs new file mode 100644 index 0000000000..31151cdc17 --- /dev/null +++ b/resources/shaders/toolpaths_lines.fs @@ -0,0 +1,28 @@ +#version 110 + +// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31) +const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929); +const vec3 LIGHT_FRONT_DIR = vec3(0.0, 0.0, 1.0); + +// x = ambient, y = top diffuse, z = front diffuse, w = global +uniform vec4 light_intensity; +uniform vec4 uniform_color; + +varying vec3 eye_normal; + +void main() +{ + vec3 normal = normalize(eye_normal); + + // Compute the cos of the angle between the normal and lights direction. The light is directional so the direction is constant for every vertex. + // Since these two are normalized the cosine is the dot product. Take the abs value to light the lines no matter in which direction the normal points. + float NdotL = abs(dot(normal, LIGHT_TOP_DIR)); + + float intensity = light_intensity.x + NdotL * light_intensity.y; + + // Perform the same lighting calculation for the 2nd light source. + NdotL = abs(dot(normal, LIGHT_FRONT_DIR)); + intensity += NdotL * light_intensity.z; + + gl_FragColor = vec4(uniform_color.rgb * light_intensity.w * intensity, uniform_color.a); +} diff --git a/resources/shaders/toolpaths_lines.vs b/resources/shaders/toolpaths_lines.vs new file mode 100644 index 0000000000..85d5c641f3 --- /dev/null +++ b/resources/shaders/toolpaths_lines.vs @@ -0,0 +1,19 @@ +#version 110 + +varying vec3 eye_normal; + +vec3 world_normal() +{ + // the world normal is always parallel to the world XY plane + // the x component is stored into gl_Vertex.w + float x = gl_Vertex.w; + float y = sqrt(1.0 - x * x); + return vec3(x, y, 0.0); +} + +void main() +{ + vec4 world_position = vec4(gl_Vertex.xyz, 1.0); + gl_Position = gl_ModelViewProjectionMatrix * world_position; + eye_normal = gl_NormalMatrix * world_normal(); +} diff --git a/sandboxes/opencsg/CMakeLists.txt b/sandboxes/opencsg/CMakeLists.txt index ec1f4cae91..ace8f4d539 100644 --- a/sandboxes/opencsg/CMakeLists.txt +++ b/sandboxes/opencsg/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable(opencsg_example WIN32 main.cpp Engine.hpp Engine.cpp ShaderCSGDisplay.hpp ShaderCSGDisplay.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/Jobs/Job.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/ProgressStatusBar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/I18N.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/I18N.cpp) diff --git a/sandboxes/opencsg/main.cpp b/sandboxes/opencsg/main.cpp index adf9cc457f..f5fb124935 100644 --- a/sandboxes/opencsg/main.cpp +++ b/sandboxes/opencsg/main.cpp @@ -26,7 +26,7 @@ #include "libslic3r/Format/3mf.hpp" #include "libslic3r/SLAPrint.hpp" -#include "slic3r/GUI/Job.hpp" +#include "slic3r/GUI/Jobs/Job.hpp" #include "slic3r/GUI/ProgressStatusBar.hpp" using namespace Slic3r::GL; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e170ea8d32..ca57ca5531 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -92,6 +92,7 @@ endif() # Create a slic3r executable # Process mainfests for various platforms. configure_file(${CMAKE_CURRENT_SOURCE_DIR}/platform/msw/PrusaSlicer.rc.in ${CMAKE_CURRENT_BINARY_DIR}/PrusaSlicer.rc @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/platform/msw/PrusaSlicer-gcodeviewer.rc.in ${CMAKE_CURRENT_BINARY_DIR}/PrusaSlicer-gcodeviewer.rc @ONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/platform/msw/PrusaSlicer.manifest.in ${CMAKE_CURRENT_BINARY_DIR}/PrusaSlicer.manifest @ONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/platform/osx/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/Info.plist @ONLY) if (WIN32) @@ -105,9 +106,9 @@ if (MINGW) set_target_properties(PrusaSlicer PROPERTIES PREFIX "") endif (MINGW) -if (NOT WIN32) - # Binary name on unix like systems (OSX, Linux) - set_target_properties(PrusaSlicer PROPERTIES OUTPUT_NAME "prusa-slicer") +if (NOT WIN32 AND NOT APPLE) + # Binary name on unix like systems (Linux, Unix) + set_target_properties(PrusaSlicer PROPERTIES OUTPUT_NAME "prusa-slicer") endif () target_link_libraries(PrusaSlicer libslic3r cereal) @@ -161,12 +162,22 @@ if (WIN32) add_executable(PrusaSlicer_app_console PrusaSlicer_app_msvc.cpp ${CMAKE_CURRENT_BINARY_DIR}/PrusaSlicer.rc) # Generate debug symbols even in release mode. if (MSVC) - target_link_options(PrusaSlicer_app_console PUBLIC "$<$:/DEBUG>") + target_link_options(PrusaSlicer_app_console PUBLIC "$<$:/DEBUG>") endif () target_compile_definitions(PrusaSlicer_app_console PRIVATE -DSLIC3R_WRAPPER_CONSOLE) add_dependencies(PrusaSlicer_app_console PrusaSlicer) set_target_properties(PrusaSlicer_app_console PROPERTIES OUTPUT_NAME "prusa-slicer-console") target_link_libraries(PrusaSlicer_app_console PRIVATE boost_headeronly) + + add_executable(PrusaSlicer_app_gcodeviewer WIN32 PrusaSlicer_app_msvc.cpp ${CMAKE_CURRENT_BINARY_DIR}/PrusaSlicer-gcodeviewer.rc) + # Generate debug symbols even in release mode. + if (MSVC) + target_link_options(PrusaSlicer_app_gcodeviewer PUBLIC "$<$:/DEBUG>") + endif () + target_compile_definitions(PrusaSlicer_app_gcodeviewer PRIVATE -DSLIC3R_WRAPPER_NOCONSOLE -DSLIC3R_WRAPPER_GCODEVIEWER) + add_dependencies(PrusaSlicer_app_gcodeviewer PrusaSlicer) + set_target_properties(PrusaSlicer_app_gcodeviewer PROPERTIES OUTPUT_NAME "prusa-gcodeviewer") + target_link_libraries(PrusaSlicer_app_gcodeviewer PRIVATE boost_headeronly) endif () # Link the resources dir to where Slic3r GUI expects it @@ -198,20 +209,34 @@ if (WIN32) add_custom_target(PrusaSlicerDllsCopy ALL DEPENDS PrusaSlicer) prusaslicer_copy_dlls(PrusaSlicerDllsCopy) -elseif (XCODE) - # Because of Debug/Release/etc. configurations (similar to MSVC) the slic3r binary is located in an extra level - add_custom_command(TARGET PrusaSlicer POST_BUILD - COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/resources" - COMMENT "Symlinking the resources directory into the build tree" - VERBATIM - ) else () + if (APPLE) + # On OSX, the name of the binary matches the name of the Application. + add_custom_command(TARGET PrusaSlicer POST_BUILD + COMMAND ln -sf PrusaSlicer prusa-slicer + COMMAND ln -sf PrusaSlicer prusa-gcodeviewer + COMMAND ln -sf PrusaSlicer PrusaGCodeViewer + WORKING_DIRECTORY "$" + COMMENT "Symlinking the G-code viewer to PrusaSlicer, symlinking to prusa-slicer and prusa-gcodeviewer" + VERBATIM) + else () + add_custom_command(TARGET PrusaSlicer POST_BUILD + COMMAND ln -sf prusa-slicer prusa-gcodeviewer + WORKING_DIRECTORY "$" + COMMENT "Symlinking the G-code viewer to PrusaSlicer" + VERBATIM) + endif () + if (XCODE) + # Because of Debug/Release/etc. configurations (similar to MSVC) the slic3r binary is located in an extra level + set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/resources") + else () + set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/../resources") + endif () add_custom_command(TARGET PrusaSlicer POST_BUILD - COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/../resources" + COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${BIN_RESOURCES_DIR}" COMMENT "Symlinking the resources directory into the build tree" - VERBATIM - ) -endif() + VERBATIM) +endif () # Slic3r binary install target if (WIN32) diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index a0422f5fa0..a12ad8bb73 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #include "libslic3r/Format/OBJ.hpp" #include "libslic3r/Format/SL1.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/AppConfig.hpp" #include "PrusaSlicer.hpp" @@ -52,7 +54,6 @@ #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/3DScene.hpp" #include "slic3r/GUI/InstanceCheck.hpp" - #include "slic3r/GUI/AppConfig.hpp" #include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/Plater.hpp" #endif /* SLIC3R_GUI */ @@ -101,7 +102,14 @@ int CLI::run(int argc, char **argv) std::find(m_transforms.begin(), m_transforms.end(), "cut") == m_transforms.end() && std::find(m_transforms.begin(), m_transforms.end(), "cut_x") == m_transforms.end() && std::find(m_transforms.begin(), m_transforms.end(), "cut_y") == m_transforms.end(); - + bool start_as_gcodeviewer = +#ifdef _WIN32 + false; +#else + // On Unix systems, the prusa-slicer binary may be symlinked to give the application a different meaning. + boost::algorithm::iends_with(boost::filesystem::path(argv[0]).filename().string(), "gcodeviewer"); +#endif // _WIN32 + const std::vector &load_configs = m_config.option("load", true)->values; // load config files supplied via --load @@ -132,37 +140,57 @@ int CLI::run(int argc, char **argv) m_print_config.apply(config); } - // Read input file(s) if any. - for (const std::string &file : m_input_files) { - if (! boost::filesystem::exists(file)) { - boost::nowide::cerr << "No such file: " << file << std::endl; - exit(1); +#if ENABLE_GCODE_VIEWER + // are we starting as gcodeviewer ? + for (auto it = m_actions.begin(); it != m_actions.end(); ++it) { + if (*it == "gcodeviewer") { + start_gui = true; + start_as_gcodeviewer = true; + m_actions.erase(it); + break; } - Model model; - try { - // When loading an AMF or 3MF, config is imported as well, including the printer technology. - DynamicPrintConfig config; - model = Model::read_from_file(file, &config, true); - PrinterTechnology other_printer_technology = Slic3r::printer_technology(config); - if (printer_technology == ptUnknown) { - printer_technology = other_printer_technology; - } else if (printer_technology != other_printer_technology && other_printer_technology != ptUnknown) { - boost::nowide::cerr << "Mixing configurations for FFF and SLA technologies" << std::endl; + } +#endif // ENABLE_GCODE_VIEWER + + // Read input file(s) if any. +#if ENABLE_GCODE_VIEWER + if (!start_as_gcodeviewer) { +#endif // ENABLE_GCODE_VIEWER + for (const std::string& file : m_input_files) { + if (!boost::filesystem::exists(file)) { + boost::nowide::cerr << "No such file: " << file << std::endl; + exit(1); + } + Model model; + try { + // When loading an AMF or 3MF, config is imported as well, including the printer technology. + DynamicPrintConfig config; + model = Model::read_from_file(file, &config, true); + PrinterTechnology other_printer_technology = Slic3r::printer_technology(config); + if (printer_technology == ptUnknown) { + printer_technology = other_printer_technology; + } + else if (printer_technology != other_printer_technology && other_printer_technology != ptUnknown) { + boost::nowide::cerr << "Mixing configurations for FFF and SLA technologies" << std::endl; + return 1; + } + // config is applied to m_print_config before the current m_config values. + config += std::move(m_print_config); + m_print_config = std::move(config); + } + catch (std::exception& e) { + boost::nowide::cerr << file << ": " << e.what() << std::endl; return 1; } - // config is applied to m_print_config before the current m_config values. - config += std::move(m_print_config); - m_print_config = std::move(config); - } catch (std::exception &e) { - boost::nowide::cerr << file << ": " << e.what() << std::endl; - return 1; + if (model.objects.empty()) { + boost::nowide::cerr << "Error: file is empty: " << file << std::endl; + continue; + } + m_models.push_back(model); } - if (model.objects.empty()) { - boost::nowide::cerr << "Error: file is empty: " << file << std::endl; - continue; - } - m_models.push_back(model); +#if ENABLE_GCODE_VIEWER } +#endif // ENABLE_GCODE_VIEWER // Apply command line options to a more specific DynamicPrintConfig which provides normalize() // (command line options override --load files) @@ -469,7 +497,11 @@ int CLI::run(int argc, char **argv) print->process(); if (printer_technology == ptFFF) { // The outfile is processed by a PlaceholderParser. +#if ENABLE_GCODE_VIEWER + outfile = fff_print.export_gcode(outfile, nullptr, nullptr); +#else outfile = fff_print.export_gcode(outfile, nullptr); +#endif // ENABLE_GCODE_VIEWER outfile_final = fff_print.print_statistics().finalize_output_path(outfile); } else { outfile = sla_print.output_filepath(outfile); @@ -517,6 +549,11 @@ int CLI::run(int argc, char **argv) << " (" << print.total_extruded_volume()/1000 << "cm3)" << std::endl; */ } +#if !ENABLE_GCODE_VIEWER + } else if (opt_key == "gcodeviewer") { + start_gui = true; + start_as_gcodeviewer = true; +#endif // !ENABLE_GCODE_VIEWER } else { boost::nowide::cerr << "error: option not supported yet: " << opt_key << std::endl; return 1; @@ -526,7 +563,11 @@ int CLI::run(int argc, char **argv) if (start_gui) { #ifdef SLIC3R_GUI // #ifdef USE_WX +#if ENABLE_GCODE_VIEWER + GUI::GUI_App* gui = new GUI::GUI_App(start_as_gcodeviewer ? GUI::GUI_App::EAppMode::GCodeViewer : GUI::GUI_App::EAppMode::Editor); +#else GUI::GUI_App *gui = new GUI::GUI_App(); +#endif // ENABLE_GCODE_VIEWER bool gui_single_instance_setting = gui->app_config->get("single_instance") == "1"; if (Slic3r::instance_check(argc, argv, gui_single_instance_setting)) { @@ -536,28 +577,42 @@ int CLI::run(int argc, char **argv) // gui->autosave = m_config.opt_string("autosave"); GUI::GUI_App::SetInstance(gui); +#if ENABLE_GCODE_VIEWER + gui->CallAfter([gui, this, &load_configs, start_as_gcodeviewer] { +#else gui->CallAfter([gui, this, &load_configs] { +#endif // ENABLE_GCODE_VIEWER if (!gui->initialized()) { return; } + +#if ENABLE_GCODE_VIEWER + if (start_as_gcodeviewer) { + if (!m_input_files.empty()) + gui->plater()->load_gcode(wxString::FromUTF8(m_input_files[0].c_str())); + } else { +#endif // ENABLE_GCODE_VIEWER_AS #if 0 - // Load the cummulative config over the currently active profiles. - //FIXME if multiple configs are loaded, only the last one will have an effect. - // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). - // As of now only the full configs are supported here. - if (!m_print_config.empty()) - gui->mainframe->load_config(m_print_config); + // Load the cummulative config over the currently active profiles. + //FIXME if multiple configs are loaded, only the last one will have an effect. + // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). + // As of now only the full configs are supported here. + if (!m_print_config.empty()) + gui->mainframe->load_config(m_print_config); #endif - if (! load_configs.empty()) - // Load the last config to give it a name at the UI. The name of the preset may be later - // changed by loading an AMF or 3MF. - //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. - gui->mainframe->load_config_file(load_configs.back()); - // If loading a 3MF file, the config is loaded from the last one. - if (! m_input_files.empty()) - gui->plater()->load_files(m_input_files, true, true); - if (! m_extra_config.empty()) - gui->mainframe->load_config(m_extra_config); + if (!load_configs.empty()) + // Load the last config to give it a name at the UI. The name of the preset may be later + // changed by loading an AMF or 3MF. + //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. + gui->mainframe->load_config_file(load_configs.back()); + // If loading a 3MF file, the config is loaded from the last one. + if (!m_input_files.empty()) + gui->plater()->load_files(m_input_files, true, true); + if (!m_extra_config.empty()) + gui->mainframe->load_config(m_extra_config); +#if ENABLE_GCODE_VIEWER + } +#endif // ENABLE_GCODE_VIEWER }); int result = wxEntry(argc, argv); return result; diff --git a/src/PrusaSlicer_app_msvc.cpp b/src/PrusaSlicer_app_msvc.cpp index 712cff687d..5f12c91479 100644 --- a/src/PrusaSlicer_app_msvc.cpp +++ b/src/PrusaSlicer_app_msvc.cpp @@ -221,6 +221,11 @@ int wmain(int argc, wchar_t **argv) std::vector argv_extended; argv_extended.emplace_back(argv[0]); +#ifdef SLIC3R_WRAPPER_GCODEVIEWER + wchar_t gcodeviewer_param[] = L"--gcodeviewer"; + argv_extended.emplace_back(gcodeviewer_param); +#endif /* SLIC3R_WRAPPER_GCODEVIEWER */ + #ifdef SLIC3R_GUI // Here one may push some additional parameters based on the wrapper type. bool force_mesa = false; diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index b85cf9025e..be4cb4a6a8 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -48,9 +48,19 @@ #include #include #include -#include #include +// Profiling support using the Shiny intrusive profiler +//#define CLIPPERLIB_PROFILE +#if defined(SLIC3R_PROFILE) && defined(CLIPPERLIB_PROFILE) + #include + #define CLIPPERLIB_PROFILE_FUNC() PROFILE_FUNC() + #define CLIPPERLIB_PROFILE_BLOCK(name) PROFILE_BLOCK(name) +#else + #define CLIPPERLIB_PROFILE_FUNC() + #define CLIPPERLIB_PROFILE_BLOCK(name) +#endif + #ifdef use_xyz namespace ClipperLib_Z { #else /* use_xyz */ @@ -263,7 +273,7 @@ int PointInPolygon (const IntPoint &pt, OutPt *op) // This is potentially very expensive! O(n^2)! bool Poly2ContainsPoly1(OutPt *OutPt1, OutPt *OutPt2) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); OutPt* op = OutPt1; do { @@ -714,7 +724,7 @@ TEdge* ClipperBase::ProcessBound(TEdge* E, bool NextIsForward) bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); // Remove duplicate end point from a closed input path. // Remove duplicate points from the end of the input path. int highI = (int)pg.size() -1; @@ -738,7 +748,7 @@ bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) bool ClipperBase::AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); std::vector num_edges(ppg.size(), 0); int num_edges_total = 0; for (size_t i = 0; i < ppg.size(); ++ i) { @@ -780,7 +790,7 @@ bool ClipperBase::AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed) bool ClipperBase::AddPathInternal(const Path &pg, int highI, PolyType PolyTyp, bool Closed, TEdge* edges) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); #ifdef use_lines if (!Closed && PolyTyp == ptClip) throw clipperException("AddPath: Open paths must be subject."); @@ -954,7 +964,7 @@ bool ClipperBase::AddPathInternal(const Path &pg, int highI, PolyType PolyTyp, b void ClipperBase::Clear() { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); m_MinimaList.clear(); m_edges.clear(); m_UseFullRange = false; @@ -966,7 +976,7 @@ void ClipperBase::Clear() // Sort the LML entries, initialize the left / right bound edges of each Local Minima. void ClipperBase::Reset() { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); if (m_MinimaList.empty()) return; //ie nothing to process std::sort(m_MinimaList.begin(), m_MinimaList.end(), [](const LocalMinimum& lm1, const LocalMinimum& lm2){ return lm1.Y < lm2.Y; }); @@ -995,7 +1005,7 @@ void ClipperBase::Reset() // Returns (0,0,0,0) for an empty rectangle. IntRect ClipperBase::GetBounds() { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); IntRect result; auto lm = m_MinimaList.begin(); if (lm == m_MinimaList.end()) @@ -1056,7 +1066,7 @@ Clipper::Clipper(int initOptions) : void Clipper::Reset() { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); ClipperBase::Reset(); m_Scanbeam = std::priority_queue(); m_Maxima.clear(); @@ -1071,7 +1081,7 @@ void Clipper::Reset() bool Clipper::Execute(ClipType clipType, Paths &solution, PolyFillType subjFillType, PolyFillType clipFillType) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); if (m_HasOpenPaths) throw clipperException("Error: PolyTree struct is needed for open path clipping."); solution.resize(0); @@ -1089,7 +1099,7 @@ bool Clipper::Execute(ClipType clipType, Paths &solution, bool Clipper::Execute(ClipType clipType, PolyTree& polytree, PolyFillType subjFillType, PolyFillType clipFillType) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); m_SubjFillType = subjFillType; m_ClipFillType = clipFillType; m_ClipType = clipType; @@ -1103,10 +1113,10 @@ bool Clipper::Execute(ClipType clipType, PolyTree& polytree, bool Clipper::ExecuteInternal() { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); bool succeeded = true; try { - PROFILE_BLOCK(Clipper_ExecuteInternal_Process); + CLIPPERLIB_PROFILE_BLOCK(Clipper_ExecuteInternal_Process); Reset(); if (m_MinimaList.empty()) return true; cInt botY = m_Scanbeam.top(); @@ -1131,13 +1141,13 @@ bool Clipper::ExecuteInternal() if (succeeded) { - PROFILE_BLOCK(Clipper_ExecuteInternal_Fix); + CLIPPERLIB_PROFILE_BLOCK(Clipper_ExecuteInternal_Fix); //fix orientations ... //FIXME Vojtech: Does it not invalidate the loop hierarchy maintained as OutRec::FirstLeft pointers? //FIXME Vojtech: The area is calculated with floats, it may not be numerically stable! { - PROFILE_BLOCK(Clipper_ExecuteInternal_Fix_orientations); + CLIPPERLIB_PROFILE_BLOCK(Clipper_ExecuteInternal_Fix_orientations); for (OutRec *outRec : m_PolyOuts) if (outRec->Pts && !outRec->IsOpen && (outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0)) ReversePolyPtLinks(outRec->Pts); @@ -1147,7 +1157,7 @@ bool Clipper::ExecuteInternal() //unfortunately FixupOutPolygon() must be done after JoinCommonEdges() { - PROFILE_BLOCK(Clipper_ExecuteInternal_Fix_fixup); + CLIPPERLIB_PROFILE_BLOCK(Clipper_ExecuteInternal_Fix_fixup); for (OutRec *outRec : m_PolyOuts) if (outRec->Pts) { if (outRec->IsOpen) @@ -1401,7 +1411,7 @@ bool Clipper::IsContributing(const TEdge& edge) const // Called from Clipper::InsertLocalMinimaIntoAEL() and Clipper::IntersectEdges(). OutPt* Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &Pt) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); OutPt* result; TEdge *e, *prevE; if (IsHorizontal(*e2) || ( e1->Dx > e2->Dx )) @@ -1493,7 +1503,7 @@ void Clipper::CopyAELToSEL() // Called from Clipper::ExecuteInternal() void Clipper::InsertLocalMinimaIntoAEL(const cInt botY) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); while (!m_MinimaList.empty() && m_MinimaList.back().Y == botY) { TEdge* lb = m_MinimaList.back().LeftBound; @@ -2043,7 +2053,7 @@ OutPt* Clipper::GetLastOutPt(TEdge *e) void Clipper::ProcessHorizontals() { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); TEdge* horzEdge = m_SortedEdges; while(horzEdge) { @@ -2414,7 +2424,7 @@ void Clipper::UpdateEdgeIntoAEL(TEdge *&e) bool Clipper::ProcessIntersections(const cInt topY) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); if( !m_ActiveEdges ) return true; try { BuildIntersectList(topY); @@ -2569,7 +2579,7 @@ void Clipper::DoMaxima(TEdge *e) void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); TEdge* e = m_ActiveEdges; while( e ) { @@ -3177,7 +3187,7 @@ bool Clipper::JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2) // This is potentially very expensive! O(n^3)! void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) const { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); //tests if NewOutRec contains the polygon before reassigning FirstLeft for (OutRec *outRec : m_PolyOuts) { @@ -3201,7 +3211,7 @@ void Clipper::FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) const void Clipper::JoinCommonEdges() { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); for (Join &join : m_Joins) { OutRec *outRec1 = GetOutRec(join.OutPt1->Idx); @@ -3771,7 +3781,7 @@ void ClipperOffset::DoRound(int j, int k) // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/Clipper/Properties/StrictlySimple.htm void Clipper::DoSimplePolygons() { - PROFILE_FUNC(); + CLIPPERLIB_PROFILE_FUNC(); size_t i = 0; while (i < m_PolyOuts.size()) { diff --git a/src/imgui/imconfig.h b/src/imgui/imconfig.h index d32f64aa4b..4a1d1faa0c 100644 --- a/src/imgui/imconfig.h +++ b/src/imgui/imconfig.h @@ -108,12 +108,19 @@ namespace ImGui const char ColorMarkerEnd = 0x3; // ETX // Special ASCII characters are used here as an ikons markers - const char PrintIconMarker = 0x4; - const char PrinterIconMarker = 0x5; - const char PrinterSlaIconMarker = 0x6; - const char FilamentIconMarker = 0x7; - const char MaterialIconMarker = 0x8; - + const char PrintIconMarker = 0x4; + const char PrinterIconMarker = 0x5; + const char PrinterSlaIconMarker = 0x6; + const char FilamentIconMarker = 0x7; + const char MaterialIconMarker = 0x8; + const char CloseIconMarker = 0xB; + const char CloseIconHoverMarker = 0xC; +// const char TimerDotMarker = 0xE; +// const char TimerDotEmptyMarker = 0xF; + const char MinimalizeMarker = 0xE; + const char MinimalizeHoverMarker = 0xF; + const char WarningMarker = 0x10; + const char ErrorMarker = 0x11; // void MyFunction(const char* name, const MyMatrix44& v); } diff --git a/src/libnest2d/LICENSE.txt b/src/libnest2d/LICENSE.txt index dba13ed2dd..07b1d92c0e 100644 --- a/src/libnest2d/LICENSE.txt +++ b/src/libnest2d/LICENSE.txt @@ -1,661 +1,165 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/src/libslic3r/AABBTreeIndirect.hpp b/src/libslic3r/AABBTreeIndirect.hpp index ec9b14a7ae..17d918aeb3 100644 --- a/src/libslic3r/AABBTreeIndirect.hpp +++ b/src/libslic3r/AABBTreeIndirect.hpp @@ -692,6 +692,40 @@ inline typename VectorType::Scalar squared_distance_to_indexed_triangle_set( detail::squared_distance_to_indexed_triangle_set_recursive(distancer, size_t(0), Scalar(0), std::numeric_limits::infinity(), hit_idx_out, hit_point_out); } +// Decides if exists some triangle in defined radius on a 3D indexed triangle set using a pre-built AABBTreeIndirect::Tree. +// Closest point to triangle test will be performed with the accuracy of VectorType::Scalar +// even if the triangle mesh and the AABB Tree are built with floats. +// Returns true if exists some triangle in defined radius, false otherwise. +template +inline bool is_any_triangle_in_radius( + // Indexed triangle set - 3D vertices. + const std::vector &vertices, + // Indexed triangle set - triangular faces, references to vertices. + const std::vector &faces, + // AABBTreeIndirect::Tree over vertices & faces, bounding boxes built with the accuracy of vertices. + const TreeType &tree, + // Point to which the closest point on the indexed triangle set is searched for. + const VectorType &point, + // Maximum distance in which triangle is search for + typename VectorType::Scalar &max_distance) +{ + using Scalar = typename VectorType::Scalar; + auto distancer = detail::IndexedTriangleSetDistancer + { vertices, faces, tree, point }; + + size_t hit_idx; + VectorType hit_point = VectorType::Ones() * (std::nan("")); + + if(tree.empty()) + { + return false; + } + + detail::squared_distance_to_indexed_triangle_set_recursive(distancer, size_t(0), Scalar(0), max_distance, hit_idx, hit_point); + + return hit_point.allFinite(); +} + } // namespace AABBTreeIndirect } // namespace Slic3r diff --git a/src/slic3r/GUI/AppConfig.cpp b/src/libslic3r/AppConfig.cpp similarity index 96% rename from src/slic3r/GUI/AppConfig.cpp rename to src/libslic3r/AppConfig.cpp index 93589e536e..72c4fd0e92 100644 --- a/src/slic3r/GUI/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -1,6 +1,7 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/Utils.hpp" #include "AppConfig.hpp" +#include "Exception.hpp" #include #include @@ -15,8 +16,8 @@ #include #include -#include -#include "I18N.hpp" +//#include +//#include "I18N.hpp" namespace Slic3r { @@ -101,6 +102,9 @@ void AppConfig::set_defaults() if (get("use_inches").empty()) set("use_inches", "0"); + if (get("show_splash_screen").empty()) + set("show_splash_screen", "1"); + // Remove legacy window positions/sizes erase("", "main_frame_maximized"); erase("", "main_frame_pos"); @@ -110,7 +114,7 @@ void AppConfig::set_defaults() erase("", "object_settings_size"); } -void AppConfig::load() +std::string AppConfig::load() { // 1) Read the complete config file into a boost::property_tree. namespace pt = boost::property_tree; @@ -120,10 +124,15 @@ void AppConfig::load() pt::read_ini(ifs, tree); } catch (pt::ptree_error& ex) { // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - throw std::runtime_error( + // ! But to avoid the use of _utf8 (related to use of wxWidgets) + // we will rethrow this exception from the place of load() call, if returned value wouldn't be empty + /* + throw Slic3r::RuntimeError( _utf8(L("Error parsing PrusaSlicer config file, it is probably corrupted. " "Try to manually delete the file to recover from the error. Your user profiles will not be affected.")) + "\n\n" + AppConfig::config_path() + "\n\n" + ex.what()); + */ + return ex.what(); } // 2) Parse the property_tree, extract the sections and key / value pairs. @@ -169,10 +178,16 @@ void AppConfig::load() // Override missing or keys with their defaults. this->set_defaults(); m_dirty = false; + return ""; } void AppConfig::save() { +#if ENABLE_GCODE_VIEWER + if (!m_save_enabled) + return; +#endif // ENABLE_GCODE_VIEWER + // The config is first written to a file with a PID suffix and then moved // to avoid race conditions with multiple instances of Slic3r const auto path = config_path(); diff --git a/src/slic3r/GUI/AppConfig.hpp b/src/libslic3r/AppConfig.hpp similarity index 94% rename from src/slic3r/GUI/AppConfig.hpp rename to src/libslic3r/AppConfig.hpp index 1e90d32e00..f22a6314ab 100644 --- a/src/slic3r/GUI/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -18,6 +18,9 @@ public: AppConfig() : m_dirty(false), m_orig_version(Semver::invalid()), +#if ENABLE_GCODE_VIEWER + m_save_enabled(true), +#endif // ENABLE_GCODE_VIEWER m_legacy_datadir(false) { this->reset(); @@ -29,7 +32,8 @@ public: void set_defaults(); // Load the slic3r.ini from a user profile directory (or a datadir, if configured). - void load(); + // return error string or empty strinf + std::string load(); // Store the slic3r.ini into a user profile directory (or a datadir, if configured). void save(); @@ -156,6 +160,10 @@ public: bool get_mouse_device_swap_yz(const std::string& name, bool& swap) const { return get_3dmouse_device_numeric_value(name, "swap_yz", swap); } +#if ENABLE_GCODE_VIEWER + void enable_save(bool enable) { m_save_enabled = enable; } +#endif // ENABLE_GCODE_VIEWER + static const std::string SECTION_FILAMENTS; static const std::string SECTION_MATERIALS; @@ -182,6 +190,10 @@ private: bool m_dirty; // Original version found in the ini file before it was overwritten Semver m_orig_version; +#if ENABLE_GCODE_VIEWER + // Whether or not calls to save() should take effect + bool m_save_enabled; +#endif // ENABLE_GCODE_VIEWER // Whether the existing version is before system profiles & configuration updating bool m_legacy_datadir; }; diff --git a/src/libslic3r/BoundingBox.hpp b/src/libslic3r/BoundingBox.hpp index f03733dd86..71746f32ed 100644 --- a/src/libslic3r/BoundingBox.hpp +++ b/src/libslic3r/BoundingBox.hpp @@ -2,6 +2,7 @@ #define slic3r_BoundingBox_hpp_ #include "libslic3r.h" +#include "Exception.hpp" #include "Point.hpp" #include "Polygon.hpp" @@ -22,7 +23,7 @@ public: { if (points.empty()) { this->defined = false; - // throw std::invalid_argument("Empty point set supplied to BoundingBoxBase constructor"); + // throw Slic3r::InvalidArgument("Empty point set supplied to BoundingBoxBase constructor"); } else { typename std::vector::const_iterator it = points.begin(); this->min = *it; @@ -68,7 +69,7 @@ public: BoundingBox3Base(const std::vector& points) { if (points.empty()) - throw std::invalid_argument("Empty point set supplied to BoundingBox3Base constructor"); + throw Slic3r::InvalidArgument("Empty point set supplied to BoundingBox3Base constructor"); typename std::vector::const_iterator it = points.begin(); this->min = *it; this->max = *it; @@ -192,6 +193,20 @@ inline BoundingBox3 scaled(const BoundingBoxf3 &bb) { return {scaled(bb.min), sc inline BoundingBoxf unscaled(const BoundingBox &bb) { return {unscaled(bb.min), unscaled(bb.max)}; } inline BoundingBoxf3 unscaled(const BoundingBox3 &bb) { return {unscaled(bb.min), unscaled(bb.max)}; } +template +auto cast(const BoundingBoxBase &b) +{ + return BoundingBoxBase>{b.min.template cast(), + b.max.template cast()}; +} + +template +auto cast(const BoundingBox3Base &b) +{ + return BoundingBox3Base>{b.min.template cast(), + b.max.template cast()}; +} + } // namespace Slic3r // Serialization through the Cereal library diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 881466b399..e30811133a 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -46,6 +46,8 @@ add_library(libslic3r STATIC Fill/Fill.hpp Fill/Fill3DHoneycomb.cpp Fill/Fill3DHoneycomb.hpp + Fill/FillAdaptive.cpp + Fill/FillAdaptive.hpp Fill/FillBase.cpp Fill/FillBase.hpp Fill/FillConcentric.cpp @@ -99,6 +101,8 @@ add_library(libslic3r STATIC GCode/ToolOrdering.hpp GCode/WipeTower.cpp GCode/WipeTower.hpp + GCode/GCodeProcessor.cpp + GCode/GCodeProcessor.hpp GCode.cpp GCode.hpp GCodeReader.cpp @@ -147,6 +151,12 @@ add_library(libslic3r STATIC PolygonTrimmer.hpp Polyline.cpp Polyline.hpp + Preset.cpp + Preset.hpp + PresetBundle.cpp + PresetBundle.hpp + AppConfig.cpp + AppConfig.hpp Print.cpp Print.hpp PrintBase.cpp @@ -155,6 +165,8 @@ add_library(libslic3r STATIC PrintConfig.hpp PrintObject.cpp PrintRegion.cpp + PNGRead.hpp + PNGRead.cpp Semver.cpp ShortestPath.cpp ShortestPath.hpp @@ -187,6 +199,8 @@ add_library(libslic3r STATIC Utils.hpp Time.cpp Time.hpp + TriangleSelector.cpp + TriangleSelector.hpp MTUtils.hpp VoronoiOffset.cpp VoronoiOffset.hpp @@ -201,12 +215,15 @@ add_library(libslic3r STATIC SimplifyMeshImpl.hpp SimplifyMesh.cpp MarchingSquares.hpp + Optimize/Optimizer.hpp + Optimize/NLoptOptimizer.hpp + Optimize/BruteforceOptimizer.hpp ${OpenVDBUtils_SOURCES} - SLA/Common.hpp - SLA/Common.cpp SLA/Pad.hpp SLA/Pad.cpp SLA/SupportTreeBuilder.hpp + SLA/SupportTreeMesher.hpp + SLA/SupportTreeMesher.cpp SLA/SupportTreeBuildsteps.hpp SLA/SupportTreeBuildsteps.cpp SLA/SupportTreeBuilder.cpp @@ -218,6 +235,7 @@ add_library(libslic3r STATIC SLA/Rotfinder.cpp SLA/BoostAdapter.hpp SLA/SpatIndex.hpp + SLA/SpatIndex.cpp SLA/RasterBase.hpp SLA/RasterBase.cpp SLA/AGGRaster.hpp @@ -233,8 +251,10 @@ add_library(libslic3r STATIC SLA/SupportPointGenerator.cpp SLA/Contour3D.hpp SLA/Contour3D.cpp - SLA/EigenMesh3D.hpp + SLA/IndexedMesh.hpp + SLA/IndexedMesh.cpp SLA/Clustering.hpp + SLA/Clustering.cpp SLA/ReprojectPointsOnMesh.hpp ) @@ -296,6 +316,8 @@ target_link_libraries(libslic3r TBB::tbb libslic3r_cgal ${CMAKE_DL_LIBS} + PNG::PNG + ZLIB::ZLIB ) if (TARGET OpenVDB::openvdb) diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index d40d79b3d8..16d985e9c9 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -8,7 +8,16 @@ #include "SVG.hpp" #endif /* CLIPPER_UTILS_DEBUG */ -#include +// Profiling support using the Shiny intrusive profiler +//#define CLIPPER_UTILS_PROFILE +#if defined(SLIC3R_PROFILE) && defined(CLIPPER_UTILS_PROFILE) + #include + #define CLIPPERUTILS_PROFILE_FUNC() PROFILE_FUNC() + #define CLIPPERUTILS_PROFILE_BLOCK(name) PROFILE_BLOCK(name) +#else + #define CLIPPERUTILS_PROFILE_FUNC() + #define CLIPPERUTILS_PROFILE_BLOCK(name) +#endif #define CLIPPER_OFFSET_SHORTEST_EDGE_FACTOR (0.005f) @@ -50,7 +59,7 @@ err: void scaleClipperPolygon(ClipperLib::Path &polygon) { - PROFILE_FUNC(); + CLIPPERUTILS_PROFILE_FUNC(); for (ClipperLib::Path::iterator pit = polygon.begin(); pit != polygon.end(); ++pit) { pit->X <<= CLIPPER_OFFSET_POWER_OF_2; pit->Y <<= CLIPPER_OFFSET_POWER_OF_2; @@ -59,7 +68,7 @@ void scaleClipperPolygon(ClipperLib::Path &polygon) void scaleClipperPolygons(ClipperLib::Paths &polygons) { - PROFILE_FUNC(); + CLIPPERUTILS_PROFILE_FUNC(); for (ClipperLib::Paths::iterator it = polygons.begin(); it != polygons.end(); ++it) for (ClipperLib::Path::iterator pit = (*it).begin(); pit != (*it).end(); ++pit) { pit->X <<= CLIPPER_OFFSET_POWER_OF_2; @@ -69,7 +78,7 @@ void scaleClipperPolygons(ClipperLib::Paths &polygons) void unscaleClipperPolygon(ClipperLib::Path &polygon) { - PROFILE_FUNC(); + CLIPPERUTILS_PROFILE_FUNC(); for (ClipperLib::Path::iterator pit = polygon.begin(); pit != polygon.end(); ++pit) { pit->X += CLIPPER_OFFSET_SCALE_ROUNDING_DELTA; pit->Y += CLIPPER_OFFSET_SCALE_ROUNDING_DELTA; @@ -80,7 +89,7 @@ void unscaleClipperPolygon(ClipperLib::Path &polygon) void unscaleClipperPolygons(ClipperLib::Paths &polygons) { - PROFILE_FUNC(); + CLIPPERUTILS_PROFILE_FUNC(); for (ClipperLib::Paths::iterator it = polygons.begin(); it != polygons.end(); ++it) for (ClipperLib::Path::iterator pit = (*it).begin(); pit != (*it).end(); ++pit) { pit->X += CLIPPER_OFFSET_SCALE_ROUNDING_DELTA; @@ -790,7 +799,7 @@ ExPolygons simplify_polygons_ex(const Polygons &subject, bool preserve_collinear void safety_offset(ClipperLib::Paths* paths) { - PROFILE_FUNC(); + CLIPPERUTILS_PROFILE_FUNC(); // scale input scaleClipperPolygons(*paths); @@ -812,11 +821,11 @@ void safety_offset(ClipperLib::Paths* paths) if (! ccw) std::reverse(path.begin(), path.end()); { - PROFILE_BLOCK(safety_offset_AddPaths); + CLIPPERUTILS_PROFILE_BLOCK(safety_offset_AddPaths); co.AddPath((*paths)[i], ClipperLib::jtMiter, ClipperLib::etClosedPolygon); } { - PROFILE_BLOCK(safety_offset_Execute); + CLIPPERUTILS_PROFILE_BLOCK(safety_offset_Execute); // offset outside by 10um ClipperLib::Paths out_this; co.Execute(out_this, ccw ? 10.f * float(CLIPPER_OFFSET_SCALE) : -10.f * float(CLIPPER_OFFSET_SCALE)); diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index f3f365b478..25ef93430f 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -5,7 +5,6 @@ #include #include #include -#include // std::runtime_error #include #include #include @@ -218,7 +217,7 @@ ConfigOption* ConfigOptionDef::create_empty_option() const case coInts: return new ConfigOptionIntsNullable(); case coPercents: return new ConfigOptionPercentsNullable(); case coBools: return new ConfigOptionBoolsNullable(); - default: throw std::runtime_error(std::string("Unknown option type for nullable option ") + this->label); + default: throw Slic3r::RuntimeError(std::string("Unknown option type for nullable option ") + this->label); } } else { switch (this->type) { @@ -238,7 +237,7 @@ ConfigOption* ConfigOptionDef::create_empty_option() const case coBool: return new ConfigOptionBool(); case coBools: return new ConfigOptionBools(); case coEnum: return new ConfigOptionEnumGeneric(this->enum_keys_map); - default: throw std::runtime_error(std::string("Unknown option type for option ") + this->label); + default: throw Slic3r::RuntimeError(std::string("Unknown option type for option ") + this->label); } } } @@ -535,7 +534,7 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key) const return opt_def->ratio_over.empty() ? 0. : static_cast(raw_opt)->get_abs_value(this->get_abs_value(opt_def->ratio_over)); } - throw std::runtime_error("ConfigBase::get_abs_value(): Not a valid option type for get_abs_value()"); + throw Slic3r::RuntimeError("ConfigBase::get_abs_value(): Not a valid option type for get_abs_value()"); } // Return an absolute value of a possibly relative config variable. @@ -546,7 +545,7 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key, double rati const ConfigOption *raw_opt = this->option(opt_key); assert(raw_opt != nullptr); if (raw_opt->type() != coFloatOrPercent) - throw std::runtime_error("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent"); + throw Slic3r::RuntimeError("ConfigBase::get_abs_value(): opt_key is not of coFloatOrPercent"); // Compute absolute value. return static_cast(raw_opt)->get_abs_value(ratio_over); } @@ -609,7 +608,7 @@ void ConfigBase::load_from_gcode_file(const std::string &file) std::getline(ifs, firstline); if (strncmp(slic3r_gcode_header, firstline.c_str(), strlen(slic3r_gcode_header)) != 0 && strncmp(prusaslicer_gcode_header, firstline.c_str(), strlen(prusaslicer_gcode_header)) != 0) - throw std::runtime_error("Not a PrusaSlicer / Slic3r PE generated g-code."); + throw Slic3r::RuntimeError("Not a PrusaSlicer / Slic3r PE generated g-code."); } ifs.seekg(0, ifs.end); auto file_length = ifs.tellg(); @@ -621,7 +620,7 @@ void ConfigBase::load_from_gcode_file(const std::string &file) size_t key_value_pairs = load_from_gcode_string(data.data()); if (key_value_pairs < 80) - throw std::runtime_error(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs)); + throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs)); } // Load the config keys from the given string. @@ -750,7 +749,7 @@ ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key, bool cre throw NoDefinitionException(opt_key); const ConfigOptionDef *optdef = def->get(opt_key); if (optdef == nullptr) -// throw std::runtime_error(std::string("Invalid option name: ") + opt_key); +// throw Slic3r::RuntimeError(std::string("Invalid option name: ") + opt_key); // Let the parent decide what to do if the opt_key is not defined by this->def(). return nullptr; ConfigOption *opt = optdef->create_default_option(); diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 87e0208986..28b28b405a 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -13,6 +13,7 @@ #include #include "libslic3r.h" #include "clonable_ptr.hpp" +#include "Exception.hpp" #include "Point.hpp" #include @@ -34,31 +35,31 @@ extern bool unescape_string_cstyle(const std::string &str, std::string & extern bool unescape_strings_cstyle(const std::string &str, std::vector &out); /// Specialization of std::exception to indicate that an unknown config option has been encountered. -class UnknownOptionException : public std::runtime_error { +class UnknownOptionException : public Slic3r::RuntimeError { public: UnknownOptionException() : - std::runtime_error("Unknown option exception") {} + Slic3r::RuntimeError("Unknown option exception") {} UnknownOptionException(const std::string &opt_key) : - std::runtime_error(std::string("Unknown option exception: ") + opt_key) {} + Slic3r::RuntimeError(std::string("Unknown option exception: ") + opt_key) {} }; /// Indicate that the ConfigBase derived class does not provide config definition (the method def() returns null). -class NoDefinitionException : public std::runtime_error +class NoDefinitionException : public Slic3r::RuntimeError { public: NoDefinitionException() : - std::runtime_error("No definition exception") {} + Slic3r::RuntimeError("No definition exception") {} NoDefinitionException(const std::string &opt_key) : - std::runtime_error(std::string("No definition exception: ") + opt_key) {} + Slic3r::RuntimeError(std::string("No definition exception: ") + opt_key) {} }; /// Indicate that an unsupported accessor was called on a config option. -class BadOptionTypeException : public std::runtime_error +class BadOptionTypeException : public Slic3r::RuntimeError { public: - BadOptionTypeException() : std::runtime_error("Bad option type exception") {} - BadOptionTypeException(const std::string &message) : std::runtime_error(message) {} - BadOptionTypeException(const char* message) : std::runtime_error(message) {} + BadOptionTypeException() : Slic3r::RuntimeError("Bad option type exception") {} + BadOptionTypeException(const std::string &message) : Slic3r::RuntimeError(message) {} + BadOptionTypeException(const char* message) : Slic3r::RuntimeError(message) {} }; // Type of a configuration value. @@ -167,7 +168,7 @@ public: void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw std::runtime_error("ConfigOptionSingle: Assigning an incompatible type"); + throw Slic3r::RuntimeError("ConfigOptionSingle: Assigning an incompatible type"); assert(dynamic_cast*>(rhs)); this->value = static_cast*>(rhs)->value; } @@ -175,7 +176,7 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw std::runtime_error("ConfigOptionSingle: Comparing incompatible types"); + throw Slic3r::RuntimeError("ConfigOptionSingle: Comparing incompatible types"); assert(dynamic_cast*>(&rhs)); return this->value == static_cast*>(&rhs)->value; } @@ -239,7 +240,7 @@ public: void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw std::runtime_error("ConfigOptionVector: Assigning an incompatible type"); + throw Slic3r::RuntimeError("ConfigOptionVector: Assigning an incompatible type"); assert(dynamic_cast*>(rhs)); this->values = static_cast*>(rhs)->values; } @@ -256,12 +257,12 @@ public: if (opt->type() == this->type()) { auto other = static_cast*>(opt); if (other->values.empty()) - throw std::runtime_error("ConfigOptionVector::set(): Assigning from an empty vector"); + throw Slic3r::RuntimeError("ConfigOptionVector::set(): Assigning from an empty vector"); this->values.emplace_back(other->values.front()); } else if (opt->type() == this->scalar_type()) this->values.emplace_back(static_cast*>(opt)->value); else - throw std::runtime_error("ConfigOptionVector::set():: Assigning an incompatible type"); + throw Slic3r::RuntimeError("ConfigOptionVector::set():: Assigning an incompatible type"); } } @@ -280,12 +281,12 @@ public: // Assign the first value of the rhs vector. auto other = static_cast*>(rhs); if (other->values.empty()) - throw std::runtime_error("ConfigOptionVector::set_at(): Assigning from an empty vector"); + throw Slic3r::RuntimeError("ConfigOptionVector::set_at(): Assigning from an empty vector"); this->values[i] = other->get_at(j); } else if (rhs->type() == this->scalar_type()) this->values[i] = static_cast*>(rhs)->value; else - throw std::runtime_error("ConfigOptionVector::set_at(): Assigning an incompatible type"); + throw Slic3r::RuntimeError("ConfigOptionVector::set_at(): Assigning an incompatible type"); } const T& get_at(size_t i) const @@ -310,9 +311,9 @@ public: else if (n > this->values.size()) { if (this->values.empty()) { if (opt_default == nullptr) - throw std::runtime_error("ConfigOptionVector::resize(): No default value provided."); + throw Slic3r::RuntimeError("ConfigOptionVector::resize(): No default value provided."); if (opt_default->type() != this->type()) - throw std::runtime_error("ConfigOptionVector::resize(): Extending with an incompatible type."); + throw Slic3r::RuntimeError("ConfigOptionVector::resize(): Extending with an incompatible type."); this->values.resize(n, static_cast*>(opt_default)->values.front()); } else { // Resize by duplicating the last value. @@ -329,7 +330,7 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw std::runtime_error("ConfigOptionVector: Comparing incompatible types"); + throw Slic3r::RuntimeError("ConfigOptionVector: Comparing incompatible types"); assert(dynamic_cast*>(&rhs)); return this->values == static_cast*>(&rhs)->values; } @@ -341,9 +342,9 @@ public: // An option overrides another option if it is not nil and not equal. bool overriden_by(const ConfigOption *rhs) const override { if (this->nullable()) - throw std::runtime_error("Cannot override a nullable ConfigOption."); + throw Slic3r::RuntimeError("Cannot override a nullable ConfigOption."); if (rhs->type() != this->type()) - throw std::runtime_error("ConfigOptionVector.overriden_by() applied to different types."); + throw Slic3r::RuntimeError("ConfigOptionVector.overriden_by() applied to different types."); auto rhs_vec = static_cast*>(rhs); if (! rhs->nullable()) // Overridding a non-nullable object with another non-nullable object. @@ -361,9 +362,9 @@ public: // Apply an override option, possibly a nullable one. bool apply_override(const ConfigOption *rhs) override { if (this->nullable()) - throw std::runtime_error("Cannot override a nullable ConfigOption."); + throw Slic3r::RuntimeError("Cannot override a nullable ConfigOption."); if (rhs->type() != this->type()) - throw std::runtime_error("ConfigOptionVector.apply_override() applied to different types."); + throw Slic3r::RuntimeError("ConfigOptionVector.apply_override() applied to different types."); auto rhs_vec = static_cast*>(rhs); if (! rhs->nullable()) { // Overridding a non-nullable object with another non-nullable object. @@ -452,7 +453,7 @@ public: bool operator==(const ConfigOptionFloatsTempl &rhs) const { return vectors_equal(this->values, rhs.values); } bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw std::runtime_error("ConfigOptionFloatsTempl: Comparing incompatible types"); + throw Slic3r::RuntimeError("ConfigOptionFloatsTempl: Comparing incompatible types"); assert(dynamic_cast*>(&rhs)); return vectors_equal(this->values, static_cast*>(&rhs)->values); } @@ -499,7 +500,7 @@ public: if (NULLABLE) this->values.push_back(nil_value()); else - throw std::runtime_error("Deserializing nil into a non-nullable object"); + throw Slic3r::RuntimeError("Deserializing nil into a non-nullable object"); } else { std::istringstream iss(item_str); double value; @@ -524,9 +525,9 @@ protected: if (NULLABLE) ss << "nil"; else - throw std::runtime_error("Serializing NaN"); + throw Slic3r::RuntimeError("Serializing NaN"); } else - throw std::runtime_error("Serializing invalid number"); + throw Slic3r::RuntimeError("Serializing invalid number"); } static bool vectors_equal(const std::vector &v1, const std::vector &v2) { if (NULLABLE) { @@ -645,7 +646,7 @@ public: if (NULLABLE) this->values.push_back(nil_value()); else - throw std::runtime_error("Deserializing nil into a non-nullable object"); + throw Slic3r::RuntimeError("Deserializing nil into a non-nullable object"); } else { std::istringstream iss(item_str); int value; @@ -662,7 +663,7 @@ private: if (NULLABLE) ss << "nil"; else - throw std::runtime_error("Serializing NaN"); + throw Slic3r::RuntimeError("Serializing NaN"); } else ss << v; } @@ -847,7 +848,7 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw std::runtime_error("ConfigOptionFloatOrPercent: Comparing incompatible types"); + throw Slic3r::RuntimeError("ConfigOptionFloatOrPercent: Comparing incompatible types"); assert(dynamic_cast(&rhs)); return *this == *static_cast(&rhs); } @@ -858,7 +859,7 @@ public: void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw std::runtime_error("ConfigOptionFloatOrPercent: Assigning an incompatible type"); + throw Slic3r::RuntimeError("ConfigOptionFloatOrPercent: Assigning an incompatible type"); assert(dynamic_cast(rhs)); *this = *static_cast(rhs); } @@ -1126,7 +1127,7 @@ public: if (NULLABLE) this->values.push_back(nil_value()); else - throw std::runtime_error("Deserializing nil into a non-nullable object"); + throw Slic3r::RuntimeError("Deserializing nil into a non-nullable object"); } else this->values.push_back(item_str.compare("1") == 0); } @@ -1139,7 +1140,7 @@ protected: if (NULLABLE) ss << "nil"; else - throw std::runtime_error("Serializing NaN"); + throw Slic3r::RuntimeError("Serializing NaN"); } else ss << (v ? "1" : "0"); } @@ -1175,14 +1176,14 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw std::runtime_error("ConfigOptionEnum: Comparing incompatible types"); + throw Slic3r::RuntimeError("ConfigOptionEnum: Comparing incompatible types"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum return this->value == (T)rhs.getInt(); } void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw std::runtime_error("ConfigOptionEnum: Assigning an incompatible type"); + throw Slic3r::RuntimeError("ConfigOptionEnum: Assigning an incompatible type"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum this->value = (T)rhs->getInt(); } @@ -1259,14 +1260,14 @@ public: bool operator==(const ConfigOption &rhs) const override { if (rhs.type() != this->type()) - throw std::runtime_error("ConfigOptionEnumGeneric: Comparing incompatible types"); + throw Slic3r::RuntimeError("ConfigOptionEnumGeneric: Comparing incompatible types"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum return this->value == rhs.getInt(); } void set(const ConfigOption *rhs) override { if (rhs->type() != this->type()) - throw std::runtime_error("ConfigOptionEnumGeneric: Assigning an incompatible type"); + throw Slic3r::RuntimeError("ConfigOptionEnumGeneric: Assigning an incompatible type"); // rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum this->value = rhs->getInt(); } @@ -1321,7 +1322,7 @@ public: case coInts: { auto opt = new ConfigOptionIntsNullable(); archive(*opt); return opt; } case coPercents: { auto opt = new ConfigOptionPercentsNullable();archive(*opt); return opt; } case coBools: { auto opt = new ConfigOptionBoolsNullable(); archive(*opt); return opt; } - default: throw std::runtime_error(std::string("ConfigOptionDef::load_option_from_archive(): Unknown nullable option type for option ") + this->opt_key); + default: throw Slic3r::RuntimeError(std::string("ConfigOptionDef::load_option_from_archive(): Unknown nullable option type for option ") + this->opt_key); } } else { switch (this->type) { @@ -1340,7 +1341,7 @@ public: case coBool: { auto opt = new ConfigOptionBool(); archive(*opt); return opt; } case coBools: { auto opt = new ConfigOptionBools(); archive(*opt); return opt; } case coEnum: { auto opt = new ConfigOptionEnumGeneric(this->enum_keys_map); archive(*opt); return opt; } - default: throw std::runtime_error(std::string("ConfigOptionDef::load_option_from_archive(): Unknown option type for option ") + this->opt_key); + default: throw Slic3r::RuntimeError(std::string("ConfigOptionDef::load_option_from_archive(): Unknown option type for option ") + this->opt_key); } } } @@ -1352,7 +1353,7 @@ public: case coInts: archive(*static_cast(opt)); break; case coPercents: archive(*static_cast(opt));break; case coBools: archive(*static_cast(opt)); break; - default: throw std::runtime_error(std::string("ConfigOptionDef::save_option_to_archive(): Unknown nullable option type for option ") + this->opt_key); + default: throw Slic3r::RuntimeError(std::string("ConfigOptionDef::save_option_to_archive(): Unknown nullable option type for option ") + this->opt_key); } } else { switch (this->type) { @@ -1371,7 +1372,7 @@ public: case coBool: archive(*static_cast(opt)); break; case coBools: archive(*static_cast(opt)); break; case coEnum: archive(*static_cast(opt)); break; - default: throw std::runtime_error(std::string("ConfigOptionDef::save_option_to_archive(): Unknown option type for option ") + this->opt_key); + default: throw Slic3r::RuntimeError(std::string("ConfigOptionDef::save_option_to_archive(): Unknown option type for option ") + this->opt_key); } } // Make the compiler happy, shut up the warnings. diff --git a/src/libslic3r/CustomGCode.cpp b/src/libslic3r/CustomGCode.cpp index ba1890a1ff..fb4f69d06a 100644 --- a/src/libslic3r/CustomGCode.cpp +++ b/src/libslic3r/CustomGCode.cpp @@ -1,6 +1,10 @@ #include "CustomGCode.hpp" #include "Config.hpp" +#if ENABLE_GCODE_VIEWER +#include "GCode.hpp" +#else #include "GCode/PreviewData.hpp" +#endif // ENABLE_GCODE_VIEWER #include "GCodeWriter.hpp" namespace Slic3r { @@ -17,8 +21,12 @@ extern void update_custom_gcode_per_print_z_from_config(Info& info, DynamicPrint return; if (info.gcodes.empty() && ! colorprint_heights->values.empty()) { // Convert the old colorprint_heighs only if there is no equivalent data in a new format. - const std::vector& colors = GCodePreviewData::ColorPrintColors(); - const auto& colorprint_values = colorprint_heights->values; +#if ENABLE_GCODE_VIEWER + const std::vector& colors = ColorPrintColors::get(); +#else + const std::vector& colors = GCodePreviewData::ColorPrintColors(); +#endif // ENABLE_GCODE_VIEWER + const auto& colorprint_values = colorprint_heights->values; info.gcodes.clear(); info.gcodes.reserve(colorprint_values.size()); int i = 0; diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index daaab47555..5bdd5055ec 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -1,5 +1,6 @@ #include "BoundingBox.hpp" #include "ExPolygon.hpp" +#include "Exception.hpp" #include "Geometry.hpp" #include "Polygon.hpp" #include "Line.hpp" @@ -435,7 +436,7 @@ void ExPolygon::triangulate_pp(Polygons* polygons) const std::list output; int res = TPPLPartition().Triangulate_MONO(&input, &output); if (res != 1) - throw std::runtime_error("Triangulation failed"); + throw Slic3r::RuntimeError("Triangulation failed"); // convert output polygons for (std::list::iterator poly = output.begin(); poly != output.end(); ++poly) { @@ -548,7 +549,7 @@ void ExPolygon::triangulate_pp(Points *triangles) const int res = TPPLPartition().Triangulate_MONO(&input, &output); // int TPPLPartition::Triangulate_EC(TPPLPolyList *inpolys, TPPLPolyList *triangles) { if (res != 1) - throw std::runtime_error("Triangulation failed"); + throw Slic3r::RuntimeError("Triangulation failed"); *triangles = polypartition_output_to_triangles(output); } @@ -591,7 +592,7 @@ void ExPolygon::triangulate_p2t(Polygons* polygons) const } polygons->push_back(p); } - } catch (const std::runtime_error & /* err */) { + } catch (const Slic3r::RuntimeError & /* err */) { assert(false); // just ignore, don't triangulate } diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 4aad3603fc..373853f972 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -333,6 +333,14 @@ extern std::list expoly_to_polypartition_input(const ExPolygons &expp) extern std::list expoly_to_polypartition_input(const ExPolygon &ex); extern std::vector polypartition_output_to_triangles(const std::list &output); +inline double area(const ExPolygons &polys) +{ + double s = 0.; + for (auto &p : polys) s += p.area(); + + return s; +} + } // namespace Slic3r // start Boost diff --git a/src/libslic3r/Exception.hpp b/src/libslic3r/Exception.hpp new file mode 100644 index 0000000000..8ec9f20c81 --- /dev/null +++ b/src/libslic3r/Exception.hpp @@ -0,0 +1,28 @@ +#ifndef _libslic3r_Exception_h_ +#define _libslic3r_Exception_h_ + +#include + +namespace Slic3r { + +// PrusaSlicer's own exception hierarchy is derived from std::runtime_error. +// Base for Slicer's own exceptions. +class Exception : public std::runtime_error { using std::runtime_error::runtime_error; }; +#define SLIC3R_DERIVE_EXCEPTION(DERIVED_EXCEPTION, PARENT_EXCEPTION) \ + class DERIVED_EXCEPTION : public PARENT_EXCEPTION { using PARENT_EXCEPTION::PARENT_EXCEPTION; } +// Critical exception produced by Slicer, such exception shall never propagate up to the UI thread. +// If that happens, an ugly fat message box with an ugly fat exclamation mark is displayed. +SLIC3R_DERIVE_EXCEPTION(CriticalException, Exception); +SLIC3R_DERIVE_EXCEPTION(RuntimeError, CriticalException); +SLIC3R_DERIVE_EXCEPTION(LogicError, CriticalException); +SLIC3R_DERIVE_EXCEPTION(InvalidArgument, LogicError); +SLIC3R_DERIVE_EXCEPTION(OutOfRange, LogicError); +SLIC3R_DERIVE_EXCEPTION(IOError, CriticalException); +SLIC3R_DERIVE_EXCEPTION(FileIOError, IOError); +// Runtime exception produced by Slicer. Such exception cancels the slicing process and it shall be shown in notifications. +SLIC3R_DERIVE_EXCEPTION(SlicingError, Exception); +#undef SLIC3R_DERIVE_EXCEPTION + +} // namespace Slic3r + +#endif // _libslic3r_Exception_h_ diff --git a/src/libslic3r/ExtrusionEntity.cpp b/src/libslic3r/ExtrusionEntity.cpp index c482f7edba..b2c5e1350f 100644 --- a/src/libslic3r/ExtrusionEntity.cpp +++ b/src/libslic3r/ExtrusionEntity.cpp @@ -306,14 +306,18 @@ double ExtrusionLoop::min_mm3_per_mm() const std::string ExtrusionEntity::role_to_string(ExtrusionRole role) { switch (role) { +#if ENABLE_GCODE_VIEWER + case erNone : return L("Unknown"); +#else case erNone : return L("None"); +#endif // ENABLE_GCODE_VIEWER case erPerimeter : return L("Perimeter"); case erExternalPerimeter : return L("External perimeter"); case erOverhangPerimeter : return L("Overhang perimeter"); case erInternalInfill : return L("Internal infill"); case erSolidInfill : return L("Solid infill"); - case erIroning : return L("Ironing"); case erTopSolidInfill : return L("Top solid infill"); + case erIroning : return L("Ironing"); case erBridgeInfill : return L("Bridge infill"); case erGapFill : return L("Gap fill"); case erSkirt : return L("Skirt"); @@ -327,4 +331,40 @@ std::string ExtrusionEntity::role_to_string(ExtrusionRole role) return ""; } +ExtrusionRole ExtrusionEntity::string_to_role(const std::string& role) +{ + if (role == L("Perimeter")) + return erPerimeter; + else if (role == L("External perimeter")) + return erExternalPerimeter; + else if (role == L("Overhang perimeter")) + return erOverhangPerimeter; + else if (role == L("Internal infill")) + return erInternalInfill; + else if (role == L("Solid infill")) + return erSolidInfill; + else if (role == L("Top solid infill")) + return erTopSolidInfill; + else if (role == L("Ironing")) + return erIroning; + else if (role == L("Bridge infill")) + return erBridgeInfill; + else if (role == L("Gap fill")) + return erGapFill; + else if (role == L("Skirt")) + return erSkirt; + else if (role == L("Support material")) + return erSupportMaterial; + else if (role == L("Support material interface")) + return erSupportMaterialInterface; + else if (role == L("Wipe tower")) + return erWipeTower; + else if (role == L("Custom")) + return erCustom; + else if (role == L("Mixed")) + return erMixed; + else + return erNone; +} + } diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp index 879f564b6c..0adb2019ee 100644 --- a/src/libslic3r/ExtrusionEntity.hpp +++ b/src/libslic3r/ExtrusionEntity.hpp @@ -106,6 +106,7 @@ public: virtual double total_volume() const = 0; static std::string role_to_string(ExtrusionRole role); + static ExtrusionRole string_to_role(const std::string& role); }; typedef std::vector ExtrusionEntitiesPtr; @@ -121,8 +122,8 @@ public: // Height of the extrusion, used for visualization purposes. float height; - ExtrusionPath(ExtrusionRole role) : mm3_per_mm(-1), width(-1), height(-1), m_role(role) {}; - ExtrusionPath(ExtrusionRole role, double mm3_per_mm, float width, float height) : mm3_per_mm(mm3_per_mm), width(width), height(height), m_role(role) {}; + ExtrusionPath(ExtrusionRole role) : mm3_per_mm(-1), width(-1), height(-1), m_role(role) {} + ExtrusionPath(ExtrusionRole role, double mm3_per_mm, float width, float height) : mm3_per_mm(mm3_per_mm), width(width), height(height), m_role(role) {} ExtrusionPath(const ExtrusionPath& rhs) : polyline(rhs.polyline), mm3_per_mm(rhs.mm3_per_mm), width(rhs.width), height(rhs.height), m_role(rhs.m_role) {} ExtrusionPath(ExtrusionPath&& rhs) : polyline(std::move(rhs.polyline)), mm3_per_mm(rhs.mm3_per_mm), width(rhs.width), height(rhs.height), m_role(rhs.m_role) {} ExtrusionPath(const Polyline &polyline, const ExtrusionPath &rhs) : polyline(polyline), mm3_per_mm(rhs.mm3_per_mm), width(rhs.width), height(rhs.height), m_role(rhs.m_role) {} diff --git a/src/libslic3r/ExtrusionEntityCollection.hpp b/src/libslic3r/ExtrusionEntityCollection.hpp index dfece6949b..5e40ab32ec 100644 --- a/src/libslic3r/ExtrusionEntityCollection.hpp +++ b/src/libslic3r/ExtrusionEntityCollection.hpp @@ -2,6 +2,7 @@ #define slic3r_ExtrusionEntityCollection_hpp_ #include "libslic3r.h" +#include "Exception.hpp" #include "ExtrusionEntity.hpp" namespace Slic3r { @@ -107,7 +108,7 @@ public: // Following methods shall never be called on an ExtrusionEntityCollection. Polyline as_polyline() const override { - throw std::runtime_error("Calling as_polyline() on a ExtrusionEntityCollection"); + throw Slic3r::RuntimeError("Calling as_polyline() on a ExtrusionEntityCollection"); return Polyline(); }; @@ -117,7 +118,7 @@ public: } double length() const override { - throw std::runtime_error("Calling length() on a ExtrusionEntityCollection"); + throw Slic3r::RuntimeError("Calling length() on a ExtrusionEntityCollection"); return 0.; } }; diff --git a/src/libslic3r/FileParserError.hpp b/src/libslic3r/FileParserError.hpp index 3f560fa4f5..b7e63d84e0 100644 --- a/src/libslic3r/FileParserError.hpp +++ b/src/libslic3r/FileParserError.hpp @@ -10,14 +10,14 @@ namespace Slic3r { // Generic file parser error, mostly copied from boost::property_tree::file_parser_error -class file_parser_error: public std::runtime_error +class file_parser_error: public Slic3r::RuntimeError { public: file_parser_error(const std::string &msg, const std::string &file, unsigned long line = 0) : - std::runtime_error(format_what(msg, file, line)), + Slic3r::RuntimeError(format_what(msg, file, line)), m_message(msg), m_filename(file), m_line(line) {} file_parser_error(const std::string &msg, const boost::filesystem::path &file, unsigned long line = 0) : - std::runtime_error(format_what(msg, file.string(), line)), + Slic3r::RuntimeError(format_what(msg, file.string(), line)), m_message(msg), m_filename(file.string()), m_line(line) {} // gcc 3.4.2 complains about lack of throw specifier on compiler // generated dtor @@ -35,7 +35,7 @@ private: std::string m_filename; unsigned long m_line; - // Format error message to be returned by std::runtime_error::what() + // Format error message to be returned by Slic3r::RuntimeError::what() static std::string format_what(const std::string &msg, const std::string &file, unsigned long l) { std::stringstream stream; diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 3c16527f07..d68bc7afb3 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -318,7 +318,7 @@ void export_group_fills_to_svg(const char *path, const std::vector #endif // friend to Layer -void Layer::make_fills() +void Layer::make_fills(FillAdaptive_Internal::Octree* adaptive_fill_octree, FillAdaptive_Internal::Octree* support_fill_octree) { for (LayerRegion *layerm : m_regions) layerm->fills.clear(); @@ -345,6 +345,8 @@ void Layer::make_fills() f->layer_id = this->id(); f->z = this->print_z; f->angle = surface_fill.params.angle; + f->adapt_fill_octree = adaptive_fill_octree; + f->support_fill_octree = support_fill_octree; // calculate flow spacing for infill pattern generation bool using_internal_flow = ! surface_fill.surface.is_solid() && ! surface_fill.params.flow.bridge; diff --git a/src/libslic3r/Fill/FillAdaptive.cpp b/src/libslic3r/Fill/FillAdaptive.cpp new file mode 100644 index 0000000000..3b9212230d --- /dev/null +++ b/src/libslic3r/Fill/FillAdaptive.cpp @@ -0,0 +1,520 @@ +#include "../ClipperUtils.hpp" +#include "../ExPolygon.hpp" +#include "../Surface.hpp" +#include "../Geometry.hpp" +#include "../AABBTreeIndirect.hpp" +#include "../Layer.hpp" +#include "../Print.hpp" +#include "../ShortestPath.hpp" + +#include "FillAdaptive.hpp" + +namespace Slic3r { + +std::pair adaptive_fill_line_spacing(const PrintObject &print_object) +{ + // Output, spacing for icAdaptiveCubic and icSupportCubic + double adaptive_line_spacing = 0.; + double support_line_spacing = 0.; + + enum class Tristate { + Yes, + No, + Maybe + }; + struct RegionFillData { + Tristate has_adaptive_infill; + Tristate has_support_infill; + double density; + double extrusion_width; + }; + std::vector region_fill_data; + region_fill_data.reserve(print_object.print()->regions().size()); + bool build_octree = false; + for (const PrintRegion *region : print_object.print()->regions()) { + const PrintRegionConfig &config = region->config(); + bool nonempty = config.fill_density > 0; + bool has_adaptive_infill = nonempty && config.fill_pattern == ipAdaptiveCubic; + bool has_support_infill = nonempty && config.fill_pattern == ipSupportCubic; + region_fill_data.push_back(RegionFillData({ + has_adaptive_infill ? Tristate::Maybe : Tristate::No, + has_support_infill ? Tristate::Maybe : Tristate::No, + config.fill_density, + config.infill_extrusion_width + })); + build_octree |= has_adaptive_infill || has_support_infill; + } + + if (build_octree) { + // Compute the average of above parameters over all layers + for (const Layer *layer : print_object.layers()) + for (size_t region_id = 0; region_id < layer->regions().size(); ++ region_id) { + RegionFillData &rd = region_fill_data[region_id]; + if (rd.has_adaptive_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty()) + rd.has_adaptive_infill = Tristate::Yes; + if (rd.has_support_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty()) + rd.has_support_infill = Tristate::Yes; + } + + double adaptive_fill_density = 0.; + double adaptive_infill_extrusion_width = 0.; + int adaptive_cnt = 0; + double support_fill_density = 0.; + double support_infill_extrusion_width = 0.; + int support_cnt = 0; + + for (const RegionFillData &rd : region_fill_data) { + if (rd.has_adaptive_infill == Tristate::Yes) { + adaptive_fill_density += rd.density; + adaptive_infill_extrusion_width += rd.extrusion_width; + ++ adaptive_cnt; + } else if (rd.has_support_infill == Tristate::Yes) { + support_fill_density += rd.density; + support_infill_extrusion_width += rd.extrusion_width; + ++ support_cnt; + } + } + + auto to_line_spacing = [](int cnt, double density, double extrusion_width) { + if (cnt) { + density /= double(cnt); + extrusion_width /= double(cnt); + return extrusion_width / ((density / 100.0f) * 0.333333333f); + } else + return 0.; + }; + adaptive_line_spacing = to_line_spacing(adaptive_cnt, adaptive_fill_density, adaptive_infill_extrusion_width); + support_line_spacing = to_line_spacing(support_cnt, support_fill_density, support_infill_extrusion_width); + } + + return std::make_pair(adaptive_line_spacing, support_line_spacing); +} + +void FillAdaptive::_fill_surface_single(const FillParams & params, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon & expolygon, + Polylines & polylines_out) +{ + if(this->adapt_fill_octree != nullptr) + this->generate_infill(params, thickness_layers, direction, expolygon, polylines_out, this->adapt_fill_octree); +} + +void FillAdaptive::generate_infill(const FillParams & params, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon & expolygon, + Polylines & polylines_out, + FillAdaptive_Internal::Octree *octree) +{ + Vec3d rotation = Vec3d((5.0 * M_PI) / 4.0, Geometry::deg2rad(215.264), M_PI / 6.0); + Transform3d rotation_matrix = Geometry::assemble_transform(Vec3d::Zero(), rotation, Vec3d::Ones(), Vec3d::Ones()); + + // Store grouped lines by its direction (multiple of 120°) + std::vector infill_lines_dir(3); + this->generate_infill_lines(octree->root_cube.get(), + this->z, octree->origin, rotation_matrix, + infill_lines_dir, octree->cubes_properties, + int(octree->cubes_properties.size()) - 1); + + Polylines all_polylines; + all_polylines.reserve(infill_lines_dir[0].size() * 3); + for (Lines &infill_lines : infill_lines_dir) + { + for (const Line &line : infill_lines) + { + all_polylines.emplace_back(line.a, line.b); + } + } + + if (params.dont_connect) + { + // Crop all polylines + polylines_out = intersection_pl(all_polylines, to_polygons(expolygon)); + } + else + { + // Crop all polylines + all_polylines = intersection_pl(all_polylines, to_polygons(expolygon)); + + Polylines boundary_polylines; + Polylines non_boundary_polylines; + for (const Polyline &polyline : all_polylines) + { + // connect_infill required all polylines to touch the boundary. + if(polyline.lines().size() == 1 && expolygon.has_boundary_point(polyline.lines().front().a) && expolygon.has_boundary_point(polyline.lines().front().b)) + { + boundary_polylines.push_back(polyline); + } + else + { + non_boundary_polylines.push_back(polyline); + } + } + + if(!boundary_polylines.empty()) + { + boundary_polylines = chain_polylines(boundary_polylines); + FillAdaptive::connect_infill(std::move(boundary_polylines), expolygon, polylines_out, this->spacing, params); + } + + polylines_out.insert(polylines_out.end(), non_boundary_polylines.begin(), non_boundary_polylines.end()); + } + +#ifdef SLIC3R_DEBUG_SLICE_PROCESSING + { + static int iRuna = 0; + BoundingBox bbox_svg = this->bounding_box; + { + ::Slic3r::SVG svg(debug_out_path("FillAdaptive-%d.svg", iRuna), bbox_svg); + for (const Polyline &polyline : polylines_out) + { + for (const Line &line : polyline.lines()) + { + Point from = line.a; + Point to = line.b; + Point diff = to - from; + + float shrink_length = scale_(0.4); + float line_slope = (float)diff.y() / diff.x(); + float shrink_x = shrink_length / (float)std::sqrt(1.0 + (line_slope * line_slope)); + float shrink_y = line_slope * shrink_x; + + to.x() -= shrink_x; + to.y() -= shrink_y; + from.x() += shrink_x; + from.y() += shrink_y; + + svg.draw(Line(from, to)); + } + } + } + + iRuna++; + } +#endif /* SLIC3R_DEBUG */ +} + +void FillAdaptive::generate_infill_lines( + FillAdaptive_Internal::Cube *cube, + double z_position, + const Vec3d &origin, + const Transform3d &rotation_matrix, + std::vector &dir_lines_out, + const std::vector &cubes_properties, + int depth) +{ + using namespace FillAdaptive_Internal; + + if(cube == nullptr) + { + return; + } + + Vec3d cube_center_tranformed = rotation_matrix * cube->center; + double z_diff = std::abs(z_position - cube_center_tranformed.z()); + + if (z_diff > cubes_properties[depth].height / 2) + { + return; + } + + if (z_diff < cubes_properties[depth].line_z_distance) + { + Point from( + scale_((cubes_properties[depth].diagonal_length / 2) * (cubes_properties[depth].line_z_distance - z_diff) / cubes_properties[depth].line_z_distance), + scale_(cubes_properties[depth].line_xy_distance - ((z_position - (cube_center_tranformed.z() - cubes_properties[depth].line_z_distance)) / sqrt(2)))); + Point to(-from.x(), from.y()); + // Relative to cube center + + double rotation_angle = (2.0 * M_PI) / 3.0; + for (Lines &lines : dir_lines_out) + { + Vec3d offset = cube_center_tranformed - (rotation_matrix * origin); + Point from_abs(from), to_abs(to); + + from_abs.x() += int(scale_(offset.x())); + from_abs.y() += int(scale_(offset.y())); + to_abs.x() += int(scale_(offset.x())); + to_abs.y() += int(scale_(offset.y())); + +// lines.emplace_back(from_abs, to_abs); + this->connect_lines(lines, Line(from_abs, to_abs)); + + from.rotate(rotation_angle); + to.rotate(rotation_angle); + } + } + + for(const std::unique_ptr &child : cube->children) + { + if(child != nullptr) + { + generate_infill_lines(child.get(), z_position, origin, rotation_matrix, dir_lines_out, cubes_properties, depth - 1); + } + } +} + +void FillAdaptive::connect_lines(Lines &lines, Line new_line) +{ + auto eps = int(scale_(0.10)); + for (size_t i = 0; i < lines.size(); ++i) + { + if (std::abs(new_line.a.x() - lines[i].b.x()) < eps && std::abs(new_line.a.y() - lines[i].b.y()) < eps) + { + new_line.a = lines[i].a; + lines.erase(lines.begin() + i); + --i; + continue; + } + + if (std::abs(new_line.b.x() - lines[i].a.x()) < eps && std::abs(new_line.b.y() - lines[i].a.y()) < eps) + { + new_line.b = lines[i].b; + lines.erase(lines.begin() + i); + --i; + continue; + } + } + + lines.emplace_back(new_line.a, new_line.b); +} + +std::unique_ptr FillAdaptive::build_octree( + TriangleMesh &triangle_mesh, + coordf_t line_spacing, + const Vec3d &cube_center) +{ + using namespace FillAdaptive_Internal; + + if(line_spacing <= 0 || std::isnan(line_spacing)) + { + return nullptr; + } + + Vec3d bb_size = triangle_mesh.bounding_box().size(); + // The furthest point from the center of the bottom of the mesh bounding box. + double furthest_point = std::sqrt(((bb_size.x() * bb_size.x()) / 4.0) + + ((bb_size.y() * bb_size.y()) / 4.0) + + (bb_size.z() * bb_size.z())); + double max_cube_edge_length = furthest_point * 2; + + std::vector cubes_properties; + for (double edge_length = (line_spacing * 2); edge_length < (max_cube_edge_length * 2); edge_length *= 2) + { + CubeProperties props{}; + props.edge_length = edge_length; + props.height = edge_length * sqrt(3); + props.diagonal_length = edge_length * sqrt(2); + props.line_z_distance = edge_length / sqrt(3); + props.line_xy_distance = edge_length / sqrt(6); + cubes_properties.push_back(props); + } + + if (triangle_mesh.its.vertices.empty()) + { + triangle_mesh.require_shared_vertices(); + } + + AABBTreeIndirect::Tree3f aabbTree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( + triangle_mesh.its.vertices, triangle_mesh.its.indices); + auto octree = std::make_unique(std::make_unique(cube_center), cube_center, cubes_properties); + + FillAdaptive::expand_cube(octree->root_cube.get(), cubes_properties, aabbTree, triangle_mesh, int(cubes_properties.size()) - 1); + + return octree; +} + +void FillAdaptive::expand_cube( + FillAdaptive_Internal::Cube *cube, + const std::vector &cubes_properties, + const AABBTreeIndirect::Tree3f &distance_tree, + const TriangleMesh &triangle_mesh, int depth) +{ + using namespace FillAdaptive_Internal; + + if (cube == nullptr || depth == 0) + { + return; + } + + std::vector child_centers = { + Vec3d(-1, -1, -1), Vec3d( 1, -1, -1), Vec3d(-1, 1, -1), Vec3d( 1, 1, -1), + Vec3d(-1, -1, 1), Vec3d( 1, -1, 1), Vec3d(-1, 1, 1), Vec3d( 1, 1, 1) + }; + + double cube_radius_squared = (cubes_properties[depth].height * cubes_properties[depth].height) / 16; + + for (size_t i = 0; i < 8; ++i) + { + const Vec3d &child_center = child_centers[i]; + Vec3d child_center_transformed = cube->center + (child_center * (cubes_properties[depth].edge_length / 4)); + + if(AABBTreeIndirect::is_any_triangle_in_radius(triangle_mesh.its.vertices, triangle_mesh.its.indices, + distance_tree, child_center_transformed, cube_radius_squared)) + { + cube->children[i] = std::make_unique(child_center_transformed); + FillAdaptive::expand_cube(cube->children[i].get(), cubes_properties, distance_tree, triangle_mesh, depth - 1); + } + } +} + +void FillAdaptive_Internal::Octree::propagate_point( + Vec3d point, + FillAdaptive_Internal::Cube * current, + int depth, + const std::vector &cubes_properties) +{ + using namespace FillAdaptive_Internal; + + if(depth <= 0) + { + return; + } + + size_t octant_idx = Octree::find_octant(point, current->center); + Cube * child = current->children[octant_idx].get(); + + // Octant not exists, then create it + if(child == nullptr) { + std::vector child_centers = { + Vec3d(-1, -1, -1), Vec3d( 1, -1, -1), Vec3d(-1, 1, -1), Vec3d( 1, 1, -1), + Vec3d(-1, -1, 1), Vec3d( 1, -1, 1), Vec3d(-1, 1, 1), Vec3d( 1, 1, 1) + }; + + const Vec3d &child_center = child_centers[octant_idx]; + Vec3d child_center_transformed = current->center + (child_center * (cubes_properties[depth].edge_length / 4)); + + current->children[octant_idx] = std::make_unique(child_center_transformed); + child = current->children[octant_idx].get(); + } + + Octree::propagate_point(point, child, (depth - 1), cubes_properties); +} + +std::unique_ptr FillSupportCubic::build_octree( + TriangleMesh & triangle_mesh, + coordf_t line_spacing, + const Vec3d & cube_center, + const Transform3d &rotation_matrix) +{ + using namespace FillAdaptive_Internal; + + if(line_spacing <= 0 || std::isnan(line_spacing)) + { + return nullptr; + } + + Vec3d bb_size = triangle_mesh.bounding_box().size(); + // The furthest point from the center of the bottom of the mesh bounding box. + double furthest_point = std::sqrt(((bb_size.x() * bb_size.x()) / 4.0) + + ((bb_size.y() * bb_size.y()) / 4.0) + + (bb_size.z() * bb_size.z())); + double max_cube_edge_length = furthest_point * 2; + + std::vector cubes_properties; + for (double edge_length = (line_spacing * 2); edge_length < (max_cube_edge_length * 2); edge_length *= 2) + { + CubeProperties props{}; + props.edge_length = edge_length; + props.height = edge_length * sqrt(3); + props.diagonal_length = edge_length * sqrt(2); + props.line_z_distance = edge_length / sqrt(3); + props.line_xy_distance = edge_length / sqrt(6); + cubes_properties.push_back(props); + } + + if (triangle_mesh.its.vertices.empty()) + { + triangle_mesh.require_shared_vertices(); + } + + AABBTreeIndirect::Tree3f aabbTree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( + triangle_mesh.its.vertices, triangle_mesh.its.indices); + + auto octree = std::make_unique(std::make_unique(cube_center), cube_center, cubes_properties); + + double cube_edge_length = line_spacing / 2.0; + int max_depth = int(octree->cubes_properties.size()) - 1; + BoundingBoxf3 mesh_bb = triangle_mesh.bounding_box(); + Vec3f vertical(0, 0, 1); + + for (size_t facet_idx = 0; facet_idx < triangle_mesh.stl.facet_start.size(); ++facet_idx) + { + if(triangle_mesh.stl.facet_start[facet_idx].normal.dot(vertical) <= 0.707) + { + // The angle is smaller than PI/4, than infill don't to be there + continue; + } + + stl_vertex v_1 = triangle_mesh.stl.facet_start[facet_idx].vertex[0]; + stl_vertex v_2 = triangle_mesh.stl.facet_start[facet_idx].vertex[1]; + stl_vertex v_3 = triangle_mesh.stl.facet_start[facet_idx].vertex[2]; + + std::vector triangle_vertices = + {Vec3d(v_1.x(), v_1.y(), v_1.z()), + Vec3d(v_2.x(), v_2.y(), v_2.z()), + Vec3d(v_3.x(), v_3.y(), v_3.z())}; + + BoundingBoxf3 triangle_bb(triangle_vertices); + + Vec3d triangle_start_relative = triangle_bb.min - mesh_bb.min; + Vec3d triangle_end_relative = triangle_bb.max - mesh_bb.min; + + Vec3crd triangle_start_idx = Vec3crd( + int(std::floor(triangle_start_relative.x() / cube_edge_length)), + int(std::floor(triangle_start_relative.y() / cube_edge_length)), + int(std::floor(triangle_start_relative.z() / cube_edge_length))); + Vec3crd triangle_end_idx = Vec3crd( + int(std::floor(triangle_end_relative.x() / cube_edge_length)), + int(std::floor(triangle_end_relative.y() / cube_edge_length)), + int(std::floor(triangle_end_relative.z() / cube_edge_length))); + + for (int z = triangle_start_idx.z(); z <= triangle_end_idx.z(); ++z) + { + for (int y = triangle_start_idx.y(); y <= triangle_end_idx.y(); ++y) + { + for (int x = triangle_start_idx.x(); x <= triangle_end_idx.x(); ++x) + { + Vec3d cube_center_relative(x * cube_edge_length + (cube_edge_length / 2.0), y * cube_edge_length + (cube_edge_length / 2.0), z * cube_edge_length); + Vec3d cube_center_absolute = cube_center_relative + mesh_bb.min; + + double cube_center_absolute_arr[3] = {cube_center_absolute.x(), cube_center_absolute.y(), cube_center_absolute.z()}; + double distance = 0, cord_u = 0, cord_v = 0; + + double dir[3] = {0.0, 0.0, 1.0}; + + double vert_0[3] = {triangle_vertices[0].x(), + triangle_vertices[0].y(), + triangle_vertices[0].z()}; + double vert_1[3] = {triangle_vertices[1].x(), + triangle_vertices[1].y(), + triangle_vertices[1].z()}; + double vert_2[3] = {triangle_vertices[2].x(), + triangle_vertices[2].y(), + triangle_vertices[2].z()}; + + if(intersect_triangle(cube_center_absolute_arr, dir, vert_0, vert_1, vert_2, &distance, &cord_u, &cord_v) && distance > 0 && distance <= cube_edge_length) + { + Vec3d cube_center_transformed(cube_center_absolute.x(), cube_center_absolute.y(), cube_center_absolute.z() + (cube_edge_length / 2.0)); + Octree::propagate_point(rotation_matrix * cube_center_transformed, octree->root_cube.get(), max_depth, octree->cubes_properties); + } + } + } + } + } + + return octree; +} + +void FillSupportCubic::_fill_surface_single(const FillParams & params, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon & expolygon, + Polylines & polylines_out) +{ + if (this->support_fill_octree != nullptr) + this->generate_infill(params, thickness_layers, direction, expolygon, polylines_out, this->support_fill_octree); +} + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillAdaptive.hpp b/src/libslic3r/Fill/FillAdaptive.hpp new file mode 100644 index 0000000000..b24f206da2 --- /dev/null +++ b/src/libslic3r/Fill/FillAdaptive.hpp @@ -0,0 +1,139 @@ +#ifndef slic3r_FillAdaptive_hpp_ +#define slic3r_FillAdaptive_hpp_ + +#include "../AABBTreeIndirect.hpp" + +#include "FillBase.hpp" + +namespace Slic3r { + +class PrintObject; +class TriangleMesh; + +namespace FillAdaptive_Internal +{ + struct CubeProperties + { + double edge_length; // Lenght of edge of a cube + double height; // Height of rotated cube (standing on the corner) + double diagonal_length; // Length of diagonal of a cube a face + double line_z_distance; // Defines maximal distance from a center of a cube on Z axis on which lines will be created + double line_xy_distance;// Defines maximal distance from a center of a cube on X and Y axis on which lines will be created + }; + + struct Cube + { + Vec3d center; + std::unique_ptr children[8] = {}; + Cube(const Vec3d ¢er) : center(center) {} + }; + + struct Octree + { + std::unique_ptr root_cube; + Vec3d origin; + std::vector cubes_properties; + + Octree(std::unique_ptr rootCube, const Vec3d &origin, const std::vector &cubes_properties) + : root_cube(std::move(rootCube)), origin(origin), cubes_properties(cubes_properties) {} + + inline static int find_octant(const Vec3d &i_cube, const Vec3d ¤t) + { + return (i_cube.z() > current.z()) * 4 + (i_cube.y() > current.y()) * 2 + (i_cube.x() > current.x()); + } + + static void propagate_point( + Vec3d point, + FillAdaptive_Internal::Cube *current_cube, + int depth, + const std::vector &cubes_properties); + }; +}; // namespace FillAdaptive_Internal + +// +// Some of the algorithms used by class FillAdaptive were inspired by +// Cura Engine's class SubDivCube +// https://github.com/Ultimaker/CuraEngine/blob/master/src/infill/SubDivCube.h +// +class FillAdaptive : public Fill +{ +public: + virtual ~FillAdaptive() {} + +protected: + virtual Fill* clone() const { return new FillAdaptive(*this); }; + virtual void _fill_surface_single( + const FillParams ¶ms, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon &expolygon, + Polylines &polylines_out); + + virtual bool no_sort() const { return true; } + + void generate_infill_lines( + FillAdaptive_Internal::Cube *cube, + double z_position, + const Vec3d & origin, + const Transform3d & rotation_matrix, + std::vector & dir_lines_out, + const std::vector &cubes_properties, + int depth); + + static void connect_lines(Lines &lines, Line new_line); + + void generate_infill(const FillParams & params, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon & expolygon, + Polylines & polylines_out, + FillAdaptive_Internal::Octree *octree); + +public: + static std::unique_ptr build_octree( + TriangleMesh &triangle_mesh, + coordf_t line_spacing, + const Vec3d & cube_center); + + static void expand_cube( + FillAdaptive_Internal::Cube *cube, + const std::vector &cubes_properties, + const AABBTreeIndirect::Tree3f &distance_tree, + const TriangleMesh & triangle_mesh, + int depth); +}; + +class FillSupportCubic : public FillAdaptive +{ +public: + virtual ~FillSupportCubic() = default; + +protected: + virtual Fill* clone() const { return new FillSupportCubic(*this); }; + + virtual bool no_sort() const { return true; } + + virtual void _fill_surface_single( + const FillParams ¶ms, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon &expolygon, + Polylines &polylines_out); + +public: + static std::unique_ptr build_octree( + TriangleMesh & triangle_mesh, + coordf_t line_spacing, + const Vec3d & cube_center, + const Transform3d &rotation_matrix); +}; + +// Calculate line spacing for +// 1) adaptive cubic infill +// 2) adaptive internal support cubic infill +// Returns zero for a particular infill type if no such infill is to be generated. +std::pair adaptive_fill_line_spacing(const PrintObject &print_object); + +} // namespace Slic3r + +#endif // slic3r_FillAdaptive_hpp_ diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index c760218c01..077555d2ca 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -16,6 +16,7 @@ #include "FillRectilinear.hpp" #include "FillRectilinear2.hpp" #include "FillRectilinear3.hpp" +#include "FillAdaptive.hpp" namespace Slic3r { @@ -37,7 +38,9 @@ Fill* Fill::new_from_type(const InfillPattern type) case ipArchimedeanChords: return new FillArchimedeanChords(); case ipHilbertCurve: return new FillHilbertCurve(); case ipOctagramSpiral: return new FillOctagramSpiral(); - default: throw std::invalid_argument("unknown type"); + case ipAdaptiveCubic: return new FillAdaptive(); + case ipSupportCubic: return new FillSupportCubic(); + default: throw Slic3r::InvalidArgument("unknown type"); } } diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp index 2e9b647354..77620e1181 100644 --- a/src/libslic3r/Fill/FillBase.hpp +++ b/src/libslic3r/Fill/FillBase.hpp @@ -11,6 +11,7 @@ #include "../libslic3r.h" #include "../BoundingBox.hpp" +#include "../Exception.hpp" #include "../Utils.hpp" namespace Slic3r { @@ -19,9 +20,14 @@ class ExPolygon; class Surface; enum InfillPattern : int; -class InfillFailedException : public std::runtime_error { +namespace FillAdaptive_Internal { + struct Octree; +}; + +// Infill shall never fail, therefore the error is classified as RuntimeError, not SlicingError. +class InfillFailedException : public Slic3r::RuntimeError { public: - InfillFailedException() : std::runtime_error("Infill failed") {} + InfillFailedException() : Slic3r::RuntimeError("Infill failed") {} }; struct FillParams @@ -69,6 +75,11 @@ public: // In scaled coordinates. Bounding box of the 2D projection of the object. BoundingBox bounding_box; + // Octree builds on mesh for usage in the adaptive cubic infill + FillAdaptive_Internal::Octree* adapt_fill_octree = nullptr; + // Octree builds on mesh for usage in the support cubic infill + FillAdaptive_Internal::Octree* support_fill_octree = nullptr; + public: virtual ~Fill() {} diff --git a/src/libslic3r/Flow.cpp b/src/libslic3r/Flow.cpp index 1678be999c..e5dcf07310 100644 --- a/src/libslic3r/Flow.cpp +++ b/src/libslic3r/Flow.cpp @@ -53,7 +53,7 @@ static inline FlowRole opt_key_to_flow_role(const std::string &opt_key) else if (opt_key == "support_material_extrusion_width") return frSupportMaterial; else - throw std::runtime_error("opt_key_to_flow_role: invalid argument"); + throw Slic3r::RuntimeError("opt_key_to_flow_role: invalid argument"); }; static inline void throw_on_missing_variable(const std::string &opt_key, const char *dependent_opt_key) @@ -126,7 +126,7 @@ Flow Flow::new_from_config_width(FlowRole role, const ConfigOptionFloatOrPercent { // we need layer height unless it's a bridge if (height <= 0 && bridge_flow_ratio == 0) - throw std::invalid_argument("Invalid flow height supplied to new_from_config_width()"); + throw Slic3r::InvalidArgument("Invalid flow height supplied to new_from_config_width()"); float w; if (bridge_flow_ratio > 0) { @@ -151,7 +151,7 @@ Flow Flow::new_from_spacing(float spacing, float nozzle_diameter, float height, { // we need layer height unless it's a bridge if (height <= 0 && !bridge) - throw std::invalid_argument("Invalid flow height supplied to new_from_spacing()"); + throw Slic3r::InvalidArgument("Invalid flow height supplied to new_from_spacing()"); // Calculate width from spacing. // For normal extrusons, extrusion width is wider than the spacing due to the rounding and squishing of the extrusions. // For bridge extrusions, the extrusions are placed with a tiny BRIDGE_EXTRA_SPACING gaps between the threads. diff --git a/src/libslic3r/Flow.hpp b/src/libslic3r/Flow.hpp index 7d6e35873d..9e57ce9079 100644 --- a/src/libslic3r/Flow.hpp +++ b/src/libslic3r/Flow.hpp @@ -3,6 +3,7 @@ #include "libslic3r.h" #include "Config.hpp" +#include "Exception.hpp" #include "ExtrusionEntity.hpp" namespace Slic3r { @@ -27,11 +28,11 @@ enum FlowRole { frSupportMaterialInterface, }; -class FlowError : public std::invalid_argument +class FlowError : public Slic3r::InvalidArgument { public: - FlowError(const std::string& what_arg) : invalid_argument(what_arg) {} - FlowError(const char* what_arg) : invalid_argument(what_arg) {} + FlowError(const std::string& what_arg) : Slic3r::InvalidArgument(what_arg) {} + FlowError(const char* what_arg) : Slic3r::InvalidArgument(what_arg) {} }; class FlowErrorNegativeSpacing : public FlowError diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 657e9ec983..46a6c02af1 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -1,9 +1,11 @@ #include "../libslic3r.h" +#include "../Exception.hpp" #include "../Model.hpp" #include "../Utils.hpp" #include "../GCode.hpp" #include "../Geometry.hpp" #include "../GCode/ThumbnailData.hpp" +#include "../Time.hpp" #include "../I18N.hpp" @@ -85,6 +87,8 @@ const char* OBJECTID_ATTR = "objectid"; const char* TRANSFORM_ATTR = "transform"; const char* PRINTABLE_ATTR = "printable"; const char* INSTANCESCOUNT_ATTR = "instances_count"; +const char* CUSTOM_SUPPORTS_ATTR = "slic3rpe:custom_supports"; +const char* CUSTOM_SEAM_ATTR = "slic3rpe:custom_seam"; const char* KEY_ATTR = "key"; const char* VALUE_ATTR = "value"; @@ -120,11 +124,11 @@ const char* INVALID_OBJECT_TYPES[] = "other" }; -class version_error : public std::runtime_error +class version_error : public Slic3r::FileIOError { public: - version_error(const std::string& what_arg) : std::runtime_error(what_arg) {} - version_error(const char* what_arg) : std::runtime_error(what_arg) {} + version_error(const std::string& what_arg) : Slic3r::FileIOError(what_arg) {} + version_error(const char* what_arg) : Slic3r::FileIOError(what_arg) {} }; const char* get_attribute_value_charptr(const char** attributes, unsigned int attributes_size, const char* attribute_key) @@ -282,6 +286,8 @@ namespace Slic3r { { std::vector vertices; std::vector triangles; + std::vector custom_supports; + std::vector custom_seam; bool empty() { @@ -292,6 +298,8 @@ namespace Slic3r { { vertices.clear(); triangles.clear(); + custom_supports.clear(); + custom_seam.clear(); } }; @@ -600,7 +608,7 @@ namespace Slic3r { { // ensure the zip archive is closed and rethrow the exception close_zip_reader(&archive); - throw std::runtime_error(e.what()); + throw Slic3r::FileIOError(e.what()); } } } @@ -773,7 +781,7 @@ namespace Slic3r { { char error_buf[1024]; ::sprintf(error_buf, "Error (%s) while parsing '%s' at line %d", XML_ErrorString(XML_GetErrorCode(data->parser)), data->stat.m_filename, (int)XML_GetCurrentLineNumber(data->parser)); - throw std::runtime_error(error_buf); + throw Slic3r::FileIOError(error_buf); } return n; @@ -782,7 +790,7 @@ namespace Slic3r { catch (const version_error& e) { // rethrow the exception - throw std::runtime_error(e.what()); + throw Slic3r::FileIOError(e.what()); } catch (std::exception& e) { @@ -1110,6 +1118,15 @@ namespace Slic3r { float(std::atof(object_data_points[i+6].c_str())), float(std::atof(object_data_points[i+7].c_str()))); } + + // The holes are saved elevated above the mesh and deeper (bad idea indeed). + // This is retained for compatibility. + // Place the hole to the mesh and make it shallower to compensate. + // The offset is 1 mm above the mesh. + for (sla::DrainHole& hole : sla_drain_holes) { + hole.pos += hole.normal.normalized(); + hole.height -= 1.f; + } if (!sla_drain_holes.empty()) m_sla_drain_holes.insert(IdToSlaDrainHolesMap::value_type(object_id, sla_drain_holes)); @@ -1538,6 +1555,9 @@ namespace Slic3r { m_curr_object.geometry.triangles.push_back((unsigned int)get_attribute_value_int(attributes, num_attributes, V1_ATTR)); m_curr_object.geometry.triangles.push_back((unsigned int)get_attribute_value_int(attributes, num_attributes, V2_ATTR)); m_curr_object.geometry.triangles.push_back((unsigned int)get_attribute_value_int(attributes, num_attributes, V3_ATTR)); + + m_curr_object.geometry.custom_supports.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SUPPORTS_ATTR)); + m_curr_object.geometry.custom_seam.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); return true; } @@ -1871,6 +1891,18 @@ namespace Slic3r { volume->source.transform = Slic3r::Geometry::Transformation(volume_matrix_to_object); volume->calculate_convex_hull(); + // recreate custom supports and seam from previously loaded attribute + for (unsigned i=0; im_supported_facets.set_triangle_from_string(i, geometry.custom_supports[index]); + if (! geometry.custom_seam[index].empty()) + volume->m_seam_facets.set_triangle_from_string(i, geometry.custom_seam[index]); + } + + // apply the remaining volume's metadata for (const Metadata& metadata : volume_data.metadata) { @@ -1991,7 +2023,7 @@ namespace Slic3r { bool _add_content_types_file_to_archive(mz_zip_archive& archive); bool _add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data); bool _add_relationships_file_to_archive(mz_zip_archive& archive); - bool _add_model_file_to_archive(mz_zip_archive& archive, const Model& model, IdToObjectDataMap &objects_data); + bool _add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data); bool _add_object_to_model_stream(std::stringstream& stream, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets); bool _add_mesh_to_object_stream(std::stringstream& stream, ModelObject& object, VolumeToOffsetsMap& volumes_offsets); bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items); @@ -2054,7 +2086,7 @@ namespace Slic3r { // Adds model file ("3D/3dmodel.model"). // This is the one and only file that contains all the geometry (vertices and triangles) of all ModelVolumes. IdToObjectDataMap objects_data; - if (!_add_model_file_to_archive(archive, model, objects_data)) + if (!_add_model_file_to_archive(filename, archive, model, objects_data)) { close_zip_writer(&archive); boost::filesystem::remove(filename); @@ -2203,7 +2235,7 @@ namespace Slic3r { return true; } - bool _3MF_Exporter::_add_model_file_to_archive(mz_zip_archive& archive, const Model& model, IdToObjectDataMap &objects_data) + bool _3MF_Exporter::_add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data) { std::stringstream stream; // https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10 @@ -2214,6 +2246,19 @@ namespace Slic3r { stream << "\n"; stream << "<" << MODEL_TAG << " unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\" xmlns:slic3rpe=\"http://schemas.slic3r.org/3mf/2017/06\">\n"; stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_3MF_VERSION << "\">" << VERSION_3MF << "\n"; + std::string name = boost::filesystem::path(filename).stem().string(); + stream << " <" << METADATA_TAG << " name=\"Title\">" << name << "\n"; + stream << " <" << METADATA_TAG << " name=\"Designer\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"Description\">" << name << "\n"; + stream << " <" << METADATA_TAG << " name=\"Copyright\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"LicenseTerms\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"Rating\">" << "\n"; + std::string date = Slic3r::Utils::utc_timestamp(Slic3r::Utils::get_current_time_utc()); + // keep only the date part of the string + date = date.substr(0, 10); + stream << " <" << METADATA_TAG << " name=\"CreationDate\">" << date << "\n"; + stream << " <" << METADATA_TAG << " name=\"ModificationDate\">" << date << "\n"; + stream << " <" << METADATA_TAG << " name=\"Application\">" << SLIC3R_APP_KEY << "-" << SLIC3R_VERSION << "\n"; stream << " <" << RESOURCES_TAG << ">\n"; // Instance transformations, indexed by the 3MF object ID (which is a linear serialization of all instances of all ModelObjects). @@ -2316,9 +2361,9 @@ namespace Slic3r { continue; if (!volume->mesh().repaired) - throw std::runtime_error("store_3mf() requires repair()"); + throw Slic3r::FileIOError("store_3mf() requires repair()"); if (!volume->mesh().has_shared_vertices()) - throw std::runtime_error("store_3mf() requires shared vertices"); + throw Slic3r::FileIOError("store_3mf() requires shared vertices"); volumes_offsets.insert(VolumeToOffsetsMap::value_type(volume, Offsets(vertices_count))).first; @@ -2369,6 +2414,15 @@ namespace Slic3r { { stream << "v" << j + 1 << "=\"" << its.indices[i][j] + volume_it->second.first_vertex_id << "\" "; } + + std::string custom_supports_data_string = volume->m_supported_facets.get_triangle_as_string(i); + if (! custom_supports_data_string.empty()) + stream << CUSTOM_SUPPORTS_ATTR << "=\"" << custom_supports_data_string << "\" "; + + std::string custom_seam_data_string = volume->m_seam_facets.get_triangle_as_string(i); + if (! custom_seam_data_string.empty()) + stream << CUSTOM_SEAM_ATTR << "=\"" << custom_seam_data_string << "\" "; + stream << "/>\n"; } } @@ -2559,7 +2613,18 @@ namespace Slic3r { for (const ModelObject* object : model.objects) { ++count; - auto& drain_holes = object->sla_drain_holes; + sla::DrainHoles drain_holes = object->sla_drain_holes; + + // The holes were placed 1mm above the mesh in the first implementation. + // This was a bad idea and the reference point was changed in 2.3 so + // to be on the mesh exactly. The elevated position is still saved + // in 3MFs for compatibility reasons. + for (sla::DrainHole& hole : drain_holes) { + hole.pos -= hole.normal.normalized(); + hole.height += 1.f; + } + + if (!drain_holes.empty()) { out += string_printf(fmt, count); diff --git a/src/libslic3r/Format/3mf.hpp b/src/libslic3r/Format/3mf.hpp index 94a7027757..ccfd9356d8 100644 --- a/src/libslic3r/Format/3mf.hpp +++ b/src/libslic3r/Format/3mf.hpp @@ -35,6 +35,6 @@ namespace Slic3r { // The model could be modified during the export process if meshes are not repaired or have no shared vertices extern bool store_3mf(const char* path, Model* model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data = nullptr); -}; // namespace Slic3r +} // namespace Slic3r #endif /* slic3r_Format_3mf_hpp_ */ diff --git a/src/libslic3r/Format/AMF.cpp b/src/libslic3r/Format/AMF.cpp index af7b9b1b60..1a706afa92 100644 --- a/src/libslic3r/Format/AMF.cpp +++ b/src/libslic3r/Format/AMF.cpp @@ -7,6 +7,7 @@ #include #include "../libslic3r.h" +#include "../Exception.hpp" #include "../Model.hpp" #include "../GCode.hpp" #include "../PrintConfig.hpp" @@ -923,7 +924,7 @@ bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_fi { char error_buf[1024]; ::sprintf(error_buf, "Error (%s) while parsing '%s' at line %d", XML_ErrorString(XML_GetErrorCode(data->parser)), data->stat.m_filename, (int)XML_GetCurrentLineNumber(data->parser)); - throw std::runtime_error(error_buf); + throw Slic3r::FileIOError(error_buf); } return n; @@ -948,9 +949,9 @@ bool extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_fi if (check_version && (ctx.m_version > VERSION_AMF_COMPATIBLE)) { // std::string msg = _(L("The selected amf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); - // throw std::runtime_error(msg.c_str()); + // throw Slic3r::FileIOError(msg.c_str()); const std::string msg = (boost::format(_(L("The selected amf file has been saved with a newer version of %1% and is not compatible."))) % std::string(SLIC3R_APP_NAME)).str(); - throw std::runtime_error(msg); + throw Slic3r::FileIOError(msg); } return true; @@ -994,7 +995,7 @@ bool load_amf_archive(const char* path, DynamicPrintConfig* config, Model* model { // ensure the zip archive is closed and rethrow the exception close_zip_reader(&archive); - throw std::runtime_error(e.what()); + throw Slic3r::FileIOError(e.what()); } break; @@ -1147,9 +1148,9 @@ bool store_amf(const char* path, Model* model, const DynamicPrintConfig* config, for (ModelVolume *volume : object->volumes) { vertices_offsets.push_back(num_vertices); if (! volume->mesh().repaired) - throw std::runtime_error("store_amf() requires repair()"); + throw Slic3r::FileIOError("store_amf() requires repair()"); if (! volume->mesh().has_shared_vertices()) - throw std::runtime_error("store_amf() requires shared vertices"); + throw Slic3r::FileIOError("store_amf() requires shared vertices"); const indexed_triangle_set &its = volume->mesh().its; const Transform3d& matrix = volume->get_matrix(); for (size_t i = 0; i < its.vertices.size(); ++i) { diff --git a/src/libslic3r/Format/PRUS.cpp b/src/libslic3r/Format/PRUS.cpp index d6f87197df..e2c38d9576 100644 --- a/src/libslic3r/Format/PRUS.cpp +++ b/src/libslic3r/Format/PRUS.cpp @@ -147,7 +147,7 @@ static void extract_model_from_archive( } } if (! trafo_set) - throw std::runtime_error(std::string("Archive ") + path + " does not contain a valid entry in scene.xml for " + name); + throw Slic3r::FileIOError(std::string("Archive ") + path + " does not contain a valid entry in scene.xml for " + name); // Extract the STL. StlHeader header; @@ -266,7 +266,7 @@ static void extract_model_from_archive( } if (! mesh_valid) - throw std::runtime_error(std::string("Archive ") + path + " does not contain a valid mesh for " + name); + throw Slic3r::FileIOError(std::string("Archive ") + path + " does not contain a valid mesh for " + name); // Add this mesh to the model. ModelVolume *volume = nullptr; @@ -303,7 +303,7 @@ bool load_prus(const char *path, Model *model) mz_bool res = MZ_FALSE; try { if (!open_zip_reader(&archive, path)) - throw std::runtime_error(std::string("Unable to init zip reader for ") + path); + throw Slic3r::FileIOError(std::string("Unable to init zip reader for ") + path); std::vector scene_xml_data; // For grouping multiple STLs into a single ModelObject for multi-material prints. std::map group_to_model_object; @@ -316,10 +316,10 @@ bool load_prus(const char *path, Model *model) buffer.assign((size_t)stat.m_uncomp_size, 0); res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (char*)buffer.data(), (size_t)stat.m_uncomp_size, 0); if (res == MZ_FALSE) - std::runtime_error(std::string("Error while extracting a file from ") + path); + throw Slic3r::FileIOError(std::string("Error while extracting a file from ") + path); if (strcmp(stat.m_filename, "scene.xml") == 0) { if (! scene_xml_data.empty()) - throw std::runtime_error(std::string("Multiple scene.xml were found in the archive.") + path); + throw Slic3r::FileIOError(std::string("Multiple scene.xml were found in the archive.") + path); scene_xml_data = std::move(buffer); } else if (boost::iends_with(stat.m_filename, ".stl")) { // May throw std::exception diff --git a/src/libslic3r/Format/SL1.cpp b/src/libslic3r/Format/SL1.cpp index ba5e89330b..274f84f002 100644 --- a/src/libslic3r/Format/SL1.cpp +++ b/src/libslic3r/Format/SL1.cpp @@ -8,8 +8,317 @@ #include "libslic3r/Zipper.hpp" #include "libslic3r/SLAPrint.hpp" +#include + +#include "libslic3r/Exception.hpp" +#include "libslic3r/SlicesToTriangleMesh.hpp" +#include "libslic3r/MarchingSquares.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/MTUtils.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/SLA/RasterBase.hpp" +#include "libslic3r/miniz_extension.hpp" +#include "libslic3r/PNGRead.hpp" + +#include +#include +#include + +namespace marchsq { + +template<> struct _RasterTraits { + using Rst = Slic3r::png::ImageGreyscale; + + // The type of pixel cell in the raster + using ValueType = uint8_t; + + // Value at a given position + static uint8_t get(const Rst &rst, size_t row, size_t col) + { + return rst.get(row, col); + } + + // Number of rows and cols of the raster + static size_t rows(const Rst &rst) { return rst.rows; } + static size_t cols(const Rst &rst) { return rst.cols; } +}; + +} // namespace marchsq + namespace Slic3r { +namespace { + +struct PNGBuffer { std::vector buf; std::string fname; }; +struct ArchiveData { + boost::property_tree::ptree profile, config; + std::vector images; +}; + +static const constexpr char *CONFIG_FNAME = "config.ini"; +static const constexpr char *PROFILE_FNAME = "prusaslicer.ini"; + +boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry, + MZ_Archive & zip) +{ + std::string buf(size_t(entry.m_uncomp_size), '\0'); + + if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, + buf.data(), buf.size(), 0)) + throw Slic3r::FileIOError(zip.get_errorstr()); + + boost::property_tree::ptree tree; + std::stringstream ss(buf); + boost::property_tree::read_ini(ss, tree); + return tree; +} + +PNGBuffer read_png(const mz_zip_archive_file_stat &entry, + MZ_Archive & zip, + const std::string & name) +{ + std::vector buf(entry.m_uncomp_size); + + if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, + buf.data(), buf.size(), 0)) + throw Slic3r::FileIOError(zip.get_errorstr()); + + return {std::move(buf), (name.empty() ? entry.m_filename : name)}; +} + +ArchiveData extract_sla_archive(const std::string &zipfname, + const std::string &exclude) +{ + ArchiveData arch; + + // Little RAII + struct Arch: public MZ_Archive { + Arch(const std::string &fname) { + if (!open_zip_reader(&arch, fname)) + throw Slic3r::FileIOError(get_errorstr()); + } + + ~Arch() { close_zip_reader(&arch); } + } zip (zipfname); + + mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch); + + for (mz_uint i = 0; i < num_entries; ++i) + { + mz_zip_archive_file_stat entry; + + if (mz_zip_reader_file_stat(&zip.arch, i, &entry)) + { + std::string name = entry.m_filename; + boost::algorithm::to_lower(name); + + if (boost::algorithm::contains(name, exclude)) continue; + + if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip); + if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip); + + if (boost::filesystem::path(name).extension().string() == ".png") { + auto it = std::lower_bound( + arch.images.begin(), arch.images.end(), PNGBuffer{{}, name}, + [](const PNGBuffer &r1, const PNGBuffer &r2) { + return std::less()(r1.fname, r2.fname); + }); + + arch.images.insert(it, read_png(entry, zip, name)); + } + } + } + + return arch; +} + +ExPolygons rings_to_expolygons(const std::vector &rings, + double px_w, double px_h) +{ + ExPolygons polys; polys.reserve(rings.size()); + + for (const marchsq::Ring &ring : rings) { + Polygon poly; Points &pts = poly.points; + pts.reserve(ring.size()); + + for (const marchsq::Coord &crd : ring) + pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h)); + + polys.emplace_back(poly); + } + + // reverse the raster transformations + return union_ex(polys); +} + +template void foreach_vertex(ExPolygon &poly, Fn &&fn) +{ + for (auto &p : poly.contour.points) fn(p); + for (auto &h : poly.holes) + for (auto &p : h.points) fn(p); +} + +void invert_raster_trafo(ExPolygons & expolys, + const sla::RasterBase::Trafo &trafo, + coord_t width, + coord_t height) +{ + if (trafo.flipXY) std::swap(height, width); + + for (auto &expoly : expolys) { + if (trafo.mirror_y) + foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); }); + + if (trafo.mirror_x) + foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); }); + + expoly.translate(-trafo.center_x, -trafo.center_y); + + if (trafo.flipXY) + foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); }); + + if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) { + expoly.contour.reverse(); + for (auto &h : expoly.holes) h.reverse(); + } + } +} + +struct RasterParams { + sla::RasterBase::Trafo trafo; // Raster transformations + coord_t width, height; // scaled raster dimensions (not resolution) + double px_h, px_w; // pixel dimesions + marchsq::Coord win; // marching squares window size +}; + +RasterParams get_raster_params(const DynamicPrintConfig &cfg) +{ + auto *opt_disp_cols = cfg.option("display_pixels_x"); + auto *opt_disp_rows = cfg.option("display_pixels_y"); + auto *opt_disp_w = cfg.option("display_width"); + auto *opt_disp_h = cfg.option("display_height"); + auto *opt_mirror_x = cfg.option("display_mirror_x"); + auto *opt_mirror_y = cfg.option("display_mirror_y"); + auto *opt_orient = cfg.option>("display_orientation"); + + if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h || + !opt_mirror_x || !opt_mirror_y || !opt_orient) + throw Slic3r::FileIOError("Invalid SL1 file"); + + RasterParams rstp; + + rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1); + rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1); + + rstp.trafo = sla::RasterBase::Trafo{opt_orient->value == sladoLandscape ? + sla::RasterBase::roLandscape : + sla::RasterBase::roPortrait, + {opt_mirror_x->value, opt_mirror_y->value}}; + + rstp.height = scaled(opt_disp_h->value); + rstp.width = scaled(opt_disp_w->value); + + return rstp; +} + +struct SliceParams { double layerh = 0., initial_layerh = 0.; }; + +SliceParams get_slice_params(const DynamicPrintConfig &cfg) +{ + auto *opt_layerh = cfg.option("layer_height"); + auto *opt_init_layerh = cfg.option("initial_layer_height"); + + if (!opt_layerh || !opt_init_layerh) + throw Slic3r::FileIOError("Invalid SL1 file"); + + return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()}; +} + +std::vector extract_slices_from_sla_archive( + ArchiveData & arch, + const RasterParams & rstp, + std::function progr) +{ + auto jobdir = arch.config.get("jobDir"); + for (auto &c : jobdir) c = std::tolower(c); + + std::vector slices(arch.images.size()); + + struct Status + { + double incr, val, prev; + bool stop = false; + tbb::spin_mutex mutex; + } st {100. / slices.size(), 0., 0.}; + + tbb::parallel_for(size_t(0), arch.images.size(), + [&arch, &slices, &st, &rstp, progr](size_t i) { + // Status indication guarded with the spinlock + { + std::lock_guard lck(st.mutex); + if (st.stop) return; + + st.val += st.incr; + double curr = std::round(st.val); + if (curr > st.prev) { + st.prev = curr; + st.stop = !progr(int(curr)); + } + } + + png::ImageGreyscale img; + png::ReadBuf rb{arch.images[i].buf.data(), arch.images[i].buf.size()}; + if (!png::decode_png(rb, img)) return; + + auto rings = marchsq::execute(img, 128, rstp.win); + ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h); + + // Invert the raster transformations indicated in + // the profile metadata + invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height); + + slices[i] = std::move(expolys); + }); + + if (st.stop) slices = {}; + + return slices; +} + +} // namespace + +void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) +{ + ArchiveData arch = extract_sla_archive(zipfname, "png"); + out.load(arch.profile); +} + +void import_sla_archive( + const std::string & zipfname, + Vec2i windowsize, + TriangleMesh & out, + DynamicPrintConfig & profile, + std::function progr) +{ + // Ensure minimum window size for marching squares + windowsize.x() = std::max(2, windowsize.x()); + windowsize.y() = std::max(2, windowsize.y()); + + ArchiveData arch = extract_sla_archive(zipfname, "thumbnail"); + profile.load(arch.profile); + + RasterParams rstp = get_raster_params(profile); + rstp.win = {windowsize.y(), windowsize.x()}; + + SliceParams slicp = get_slice_params(profile); + + std::vector slices = + extract_slices_from_sla_archive(arch, rstp, progr); + + if (!slices.empty()) + out = slices_to_triangle_mesh(slices, 0, slicp.layerh, slicp.initial_layerh); +} + using ConfMap = std::map; namespace { @@ -126,9 +435,9 @@ uqptr SL1Archive::create_raster() const return sla::create_raster_grayscale_aa(res, pxdim, gamma, tr); } -sla::EncodedRaster SL1Archive::encode_raster(const sla::RasterBase &rst) const +sla::RasterEncoder SL1Archive::get_encoder() const { - return rst.encode(sla::PNGRasterEncoder()); + return sla::PNGRasterEncoder{}; } void SL1Archive::export_print(Zipper& zipper, diff --git a/src/libslic3r/Format/SL1.hpp b/src/libslic3r/Format/SL1.hpp index 1b9e95392b..ab731ff841 100644 --- a/src/libslic3r/Format/SL1.hpp +++ b/src/libslic3r/Format/SL1.hpp @@ -13,7 +13,7 @@ class SL1Archive: public SLAPrinter { protected: uqptr create_raster() const override; - sla::EncodedRaster encode_raster(const sla::RasterBase &rst) const override; + sla::RasterEncoder get_encoder() const override; public: @@ -38,6 +38,24 @@ public: } }; +void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out); + +void import_sla_archive( + const std::string & zipfname, + Vec2i windowsize, + TriangleMesh & out, + DynamicPrintConfig & profile, + std::function progr = [](int) { return true; }); + +inline void import_sla_archive( + const std::string & zipfname, + Vec2i windowsize, + TriangleMesh & out, + std::function progr = [](int) { return true; }) +{ + DynamicPrintConfig profile; + import_sla_archive(zipfname, windowsize, out, profile, progr); +} } // namespace Slic3r::sla diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f863fd3ee8..88baa44772 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1,6 +1,7 @@ #include "libslic3r.h" #include "I18N.hpp" #include "GCode.hpp" +#include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" #include "Geometry.hpp" @@ -48,606 +49,609 @@ using namespace std::literals::string_view_literals; namespace Slic3r { -//! macro used to mark string used at localization, -//! return same string + //! macro used to mark string used at localization, + //! return same string #define L(s) (s) #define _(s) Slic3r::I18N::translate(s) // Only add a newline in case the current G-code does not end with a newline. -static inline void check_add_eol(std::string &gcode) -{ - if (! gcode.empty() && gcode.back() != '\n') - gcode += '\n'; -} - - -// Return true if tch_prefix is found in custom_gcode -static bool custom_gcode_changes_tool(const std::string& custom_gcode, const std::string& tch_prefix, unsigned next_extruder) -{ - bool ok = false; - size_t from_pos = 0; - size_t pos = 0; - while ((pos = custom_gcode.find(tch_prefix, from_pos)) != std::string::npos) { - if (pos+1 == custom_gcode.size()) - break; - from_pos = pos+1; - // only whitespace is allowed before the command - while (--pos < custom_gcode.size() && custom_gcode[pos] != '\n') { - if (! std::isspace(custom_gcode[pos])) - goto NEXT; - } - { - // we should also check that the extruder changes to what was expected - std::istringstream ss(custom_gcode.substr(from_pos, std::string::npos)); - unsigned num = 0; - if (ss >> num) - ok = (num == next_extruder); - } -NEXT: ; + static inline void check_add_eol(std::string& gcode) + { + if (!gcode.empty() && gcode.back() != '\n') + gcode += '\n'; } - return ok; -} -void AvoidCrossingPerimeters::init_external_mp(const Print &print) -{ - m_external_mp = Slic3r::make_unique(union_ex(this->collect_contours_all_layers(print.objects()))); -} -// Plan a travel move while minimizing the number of perimeter crossings. -// point is in unscaled coordinates, in the coordinate system of the current active object -// (set by gcodegen.set_origin()). -Polyline AvoidCrossingPerimeters::travel_to(const GCode &gcodegen, const Point &point) -{ - // If use_external, then perform the path planning in the world coordinate system (correcting for the gcodegen offset). - // Otherwise perform the path planning in the coordinate system of the active object. - bool use_external = this->use_external_mp || this->use_external_mp_once; - Point scaled_origin = use_external ? Point::new_scale(gcodegen.origin()(0), gcodegen.origin()(1)) : Point(0, 0); - Polyline result = (use_external ? m_external_mp.get() : m_layer_mp.get())-> - shortest_path(gcodegen.last_pos() + scaled_origin, point + scaled_origin); - if (use_external) - result.translate(- scaled_origin); - return result; -} - -// Collect outer contours of all objects over all layers. -// Discard objects only containing thin walls (offset would fail on an empty polygon). -// Used by avoid crossing perimeters feature. -Polygons AvoidCrossingPerimeters::collect_contours_all_layers(const PrintObjectPtrs& objects) -{ - Polygons islands; - for (const PrintObject *object : objects) { - // Reducing all the object slices into the Z projection in a logarithimc fashion. - // First reduce to half the number of layers. - std::vector polygons_per_layer((object->layers().size() + 1) / 2); - tbb::parallel_for(tbb::blocked_range(0, object->layers().size() / 2), - [&object, &polygons_per_layer](const tbb::blocked_range &range) { - for (size_t i = range.begin(); i < range.end(); ++ i) { - const Layer* layer1 = object->layers()[i * 2]; - const Layer* layer2 = object->layers()[i * 2 + 1]; - Polygons polys; - polys.reserve(layer1->lslices.size() + layer2->lslices.size()); - for (const ExPolygon &expoly : layer1->lslices) - //FIXME no holes? - polys.emplace_back(expoly.contour); - for (const ExPolygon &expoly : layer2->lslices) - //FIXME no holes? - polys.emplace_back(expoly.contour); - polygons_per_layer[i] = union_(polys); - } - }); - if (object->layers().size() & 1) { - const Layer *layer = object->layers().back(); - Polygons polys; - polys.reserve(layer->lslices.size()); - for (const ExPolygon &expoly : layer->lslices) - //FIXME no holes? - polys.emplace_back(expoly.contour); - polygons_per_layer.back() = union_(polys); - } - // Now reduce down to a single layer. - size_t cnt = polygons_per_layer.size(); - while (cnt > 1) { - tbb::parallel_for(tbb::blocked_range(0, cnt / 2), - [&polygons_per_layer](const tbb::blocked_range &range) { - for (size_t i = range.begin(); i < range.end(); ++ i) { - Polygons polys; - polys.reserve(polygons_per_layer[i * 2].size() + polygons_per_layer[i * 2 + 1].size()); - polygons_append(polys, polygons_per_layer[i * 2]); - polygons_append(polys, polygons_per_layer[i * 2 + 1]); - polygons_per_layer[i * 2] = union_(polys); - } - }); - for (size_t i = 0; i < cnt / 2; ++ i) - polygons_per_layer[i] = std::move(polygons_per_layer[i * 2]); - if (cnt & 1) - polygons_per_layer[cnt / 2] = std::move(polygons_per_layer[cnt - 1]); - cnt = (cnt + 1) / 2; - } - // And collect copies of the objects. - for (const PrintInstance &instance : object->instances()) { - // All the layers were reduced to the 1st item of polygons_per_layer. - size_t i = islands.size(); - polygons_append(islands, polygons_per_layer.front()); - for (; i < islands.size(); ++ i) - islands[i].translate(instance.shift); - } - } - return islands; -} - -std::string OozePrevention::pre_toolchange(GCode &gcodegen) -{ - std::string gcode; - - // move to the nearest standby point - if (!this->standby_points.empty()) { - // get current position in print coordinates - Vec3d writer_pos = gcodegen.writer().get_position(); - Point pos = Point::new_scale(writer_pos(0), writer_pos(1)); - - // find standby point - Point standby_point; - pos.nearest_point(this->standby_points, &standby_point); - - /* We don't call gcodegen.travel_to() because we don't need retraction (it was already - triggered by the caller) nor avoid_crossing_perimeters and also because the coordinates - of the destination point must not be transformed by origin nor current extruder offset. */ - gcode += gcodegen.writer().travel_to_xy(unscale(standby_point), - "move to standby position"); - } - - if (gcodegen.config().standby_temperature_delta.value != 0) { - // we assume that heating is always slower than cooling, so no need to block - gcode += gcodegen.writer().set_temperature - (this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, gcodegen.writer().extruder()->id()); - } - - return gcode; -} - -std::string OozePrevention::post_toolchange(GCode &gcodegen) -{ - return (gcodegen.config().standby_temperature_delta.value != 0) ? - gcodegen.writer().set_temperature(this->_get_temp(gcodegen), true, gcodegen.writer().extruder()->id()) : - std::string(); -} - -int -OozePrevention::_get_temp(GCode &gcodegen) -{ - return (gcodegen.layer() != NULL && gcodegen.layer()->id() == 0) - ? gcodegen.config().first_layer_temperature.get_at(gcodegen.writer().extruder()->id()) - : gcodegen.config().temperature.get_at(gcodegen.writer().extruder()->id()); -} - -std::string Wipe::wipe(GCode &gcodegen, bool toolchange) -{ - std::string gcode; - - /* Reduce feedrate a bit; travel speed is often too high to move on existing material. - Too fast = ripping of existing material; too slow = short wipe path, thus more blob. */ - double wipe_speed = gcodegen.writer().config.travel_speed.value * 0.8; - - // get the retraction length - double length = toolchange - ? gcodegen.writer().extruder()->retract_length_toolchange() - : gcodegen.writer().extruder()->retract_length(); - // Shorten the retraction length by the amount already retracted before wipe. - length *= (1. - gcodegen.writer().extruder()->retract_before_wipe()); - - if (length > 0) { - /* Calculate how long we need to travel in order to consume the required - amount of retraction. In other words, how far do we move in XY at wipe_speed - for the time needed to consume retract_length at retract_speed? */ - double wipe_dist = scale_(length / gcodegen.writer().extruder()->retract_speed() * wipe_speed); - - /* Take the stored wipe path and replace first point with the current actual position - (they might be different, for example, in case of loop clipping). */ - Polyline wipe_path; - wipe_path.append(gcodegen.last_pos()); - wipe_path.append( - this->path.points.begin() + 1, - this->path.points.end() - ); - - wipe_path.clip_end(wipe_path.length() - wipe_dist); - - // subdivide the retraction in segments - if (! wipe_path.empty()) { - for (const Line &line : wipe_path.lines()) { - double segment_length = line.length(); - /* Reduce retraction length a bit to avoid effective retraction speed to be greater than the configured one - due to rounding (TODO: test and/or better math for this) */ - double dE = length * (segment_length / wipe_dist) * 0.95; - //FIXME one shall not generate the unnecessary G1 Fxxx commands, here wipe_speed is a constant inside this cycle. - // Is it here for the cooling markers? Or should it be outside of the cycle? - gcode += gcodegen.writer().set_speed(wipe_speed*60, "", gcodegen.enable_cooling_markers() ? ";_WIPE" : ""); - gcode += gcodegen.writer().extrude_to_xy( - gcodegen.point_to_gcode(line.b), - -dE, - "wipe and retract" - ); + // Return true if tch_prefix is found in custom_gcode + static bool custom_gcode_changes_tool(const std::string& custom_gcode, const std::string& tch_prefix, unsigned next_extruder) + { + bool ok = false; + size_t from_pos = 0; + size_t pos = 0; + while ((pos = custom_gcode.find(tch_prefix, from_pos)) != std::string::npos) { + if (pos + 1 == custom_gcode.size()) + break; + from_pos = pos + 1; + // only whitespace is allowed before the command + while (--pos < custom_gcode.size() && custom_gcode[pos] != '\n') { + if (!std::isspace(custom_gcode[pos])) + goto NEXT; } - gcodegen.set_last_pos(wipe_path.points.back()); + { + // we should also check that the extruder changes to what was expected + std::istringstream ss(custom_gcode.substr(from_pos, std::string::npos)); + unsigned num = 0; + if (ss >> num) + ok = (num == next_extruder); + } + NEXT:; } - - // prevent wiping again on same path - this->reset_path(); - } - - return gcode; -} - -static inline Point wipe_tower_point_to_object_point(GCode &gcodegen, const Vec2f &wipe_tower_pt) -{ - return Point(scale_(wipe_tower_pt.x() - gcodegen.origin()(0)), scale_(wipe_tower_pt.y() - gcodegen.origin()(1))); -} - -std::string WipeTowerIntegration::append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z) const -{ - if (new_extruder_id != -1 && new_extruder_id != tcr.new_tool) - throw std::invalid_argument("Error: WipeTowerIntegration::append_tcr was asked to do a toolchange it didn't expect."); - - std::string gcode; - - // Toolchangeresult.gcode assumes the wipe tower corner is at the origin (except for priming lines) - // We want to rotate and shift all extrusions (gcode postprocessing) and starting and ending position - float alpha = m_wipe_tower_rotation/180.f * float(M_PI); - Vec2f start_pos = tcr.start_pos; - Vec2f end_pos = tcr.end_pos; - if (!tcr.priming) { - start_pos = Eigen::Rotation2Df(alpha) * start_pos; - start_pos += m_wipe_tower_pos; - end_pos = Eigen::Rotation2Df(alpha) * end_pos; - end_pos += m_wipe_tower_pos; + return ok; } - Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : m_wipe_tower_pos; - float wipe_tower_rotation = tcr.priming ? 0.f : alpha; - - std::string tcr_rotated_gcode = post_process_wipe_tower_moves(tcr, wipe_tower_offset, wipe_tower_rotation); - - if (!tcr.priming) { - // Move over the wipe tower. - // Retract for a tool change, using the toolchange retract value and setting the priming extra length. - gcode += gcodegen.retract(true); - gcodegen.m_avoid_crossing_perimeters.use_external_mp_once = true; - gcode += gcodegen.travel_to( - wipe_tower_point_to_object_point(gcodegen, start_pos), - erMixed, - "Travel to a Wipe Tower"); - gcode += gcodegen.unretract(); + void AvoidCrossingPerimeters::init_external_mp(const Print& print) + { + m_external_mp = Slic3r::make_unique(union_ex(this->collect_contours_all_layers(print.objects()))); } - double current_z = gcodegen.writer().get_position().z(); - if (z == -1.) // in case no specific z was provided, print at current_z pos - z = current_z; - if (! is_approx(z, current_z)) { - gcode += gcodegen.writer().retract(); - gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer."); - gcode += gcodegen.writer().unretract(); + // Plan a travel move while minimizing the number of perimeter crossings. + // point is in unscaled coordinates, in the coordinate system of the current active object + // (set by gcodegen.set_origin()). + Polyline AvoidCrossingPerimeters::travel_to(const GCode& gcodegen, const Point& point) + { + // If use_external, then perform the path planning in the world coordinate system (correcting for the gcodegen offset). + // Otherwise perform the path planning in the coordinate system of the active object. + bool use_external = this->use_external_mp || this->use_external_mp_once; + Point scaled_origin = use_external ? Point::new_scale(gcodegen.origin()(0), gcodegen.origin()(1)) : Point(0, 0); + Polyline result = (use_external ? m_external_mp.get() : m_layer_mp.get())-> + shortest_path(gcodegen.last_pos() + scaled_origin, point + scaled_origin); + if (use_external) + result.translate(-scaled_origin); + return result; } - - // Process the end filament gcode. - std::string end_filament_gcode_str; - if (gcodegen.writer().extruder() != nullptr) { - // Process the custom end_filament_gcode in case of single_extruder_multi_material. - unsigned int old_extruder_id = gcodegen.writer().extruder()->id(); - const std::string &end_filament_gcode = gcodegen.config().end_filament_gcode.get_at(old_extruder_id); - if (gcodegen.writer().extruder() != nullptr && ! end_filament_gcode.empty()) { - end_filament_gcode_str = gcodegen.placeholder_parser_process("end_filament_gcode", end_filament_gcode, old_extruder_id); - check_add_eol(end_filament_gcode_str); + // Collect outer contours of all objects over all layers. + // Discard objects only containing thin walls (offset would fail on an empty polygon). + // Used by avoid crossing perimeters feature. + Polygons AvoidCrossingPerimeters::collect_contours_all_layers(const PrintObjectPtrs& objects) + { + Polygons islands; + for (const PrintObject* object : objects) { + // Reducing all the object slices into the Z projection in a logarithimc fashion. + // First reduce to half the number of layers. + std::vector polygons_per_layer((object->layers().size() + 1) / 2); + tbb::parallel_for(tbb::blocked_range(0, object->layers().size() / 2), + [&object, &polygons_per_layer](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + const Layer* layer1 = object->layers()[i * 2]; + const Layer* layer2 = object->layers()[i * 2 + 1]; + Polygons polys; + polys.reserve(layer1->lslices.size() + layer2->lslices.size()); + for (const ExPolygon& expoly : layer1->lslices) + //FIXME no holes? + polys.emplace_back(expoly.contour); + for (const ExPolygon& expoly : layer2->lslices) + //FIXME no holes? + polys.emplace_back(expoly.contour); + polygons_per_layer[i] = union_(polys); + } + }); + if (object->layers().size() & 1) { + const Layer* layer = object->layers().back(); + Polygons polys; + polys.reserve(layer->lslices.size()); + for (const ExPolygon& expoly : layer->lslices) + //FIXME no holes? + polys.emplace_back(expoly.contour); + polygons_per_layer.back() = union_(polys); + } + // Now reduce down to a single layer. + size_t cnt = polygons_per_layer.size(); + while (cnt > 1) { + tbb::parallel_for(tbb::blocked_range(0, cnt / 2), + [&polygons_per_layer](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + Polygons polys; + polys.reserve(polygons_per_layer[i * 2].size() + polygons_per_layer[i * 2 + 1].size()); + polygons_append(polys, polygons_per_layer[i * 2]); + polygons_append(polys, polygons_per_layer[i * 2 + 1]); + polygons_per_layer[i * 2] = union_(polys); + } + }); + for (size_t i = 0; i < cnt / 2; ++i) + polygons_per_layer[i] = std::move(polygons_per_layer[i * 2]); + if (cnt & 1) + polygons_per_layer[cnt / 2] = std::move(polygons_per_layer[cnt - 1]); + cnt = (cnt + 1) / 2; + } + // And collect copies of the objects. + for (const PrintInstance& instance : object->instances()) { + // All the layers were reduced to the 1st item of polygons_per_layer. + size_t i = islands.size(); + polygons_append(islands, polygons_per_layer.front()); + for (; i < islands.size(); ++i) + islands[i].translate(instance.shift); + } } + return islands; } - // Process the custom toolchange_gcode. If it is empty, provide a simple Tn command to change the filament. - // Otherwise, leave control to the user completely. - std::string toolchange_gcode_str; - if (true /*gcodegen.writer().extruder() != nullptr*/) { - const std::string& toolchange_gcode = gcodegen.config().toolchange_gcode.value; - if (!toolchange_gcode.empty()) { + std::string OozePrevention::pre_toolchange(GCode& gcodegen) + { + std::string gcode; + + // move to the nearest standby point + if (!this->standby_points.empty()) { + // get current position in print coordinates + Vec3d writer_pos = gcodegen.writer().get_position(); + Point pos = Point::new_scale(writer_pos(0), writer_pos(1)); + + // find standby point + Point standby_point; + pos.nearest_point(this->standby_points, &standby_point); + + /* We don't call gcodegen.travel_to() because we don't need retraction (it was already + triggered by the caller) nor avoid_crossing_perimeters and also because the coordinates + of the destination point must not be transformed by origin nor current extruder offset. */ + gcode += gcodegen.writer().travel_to_xy(unscale(standby_point), + "move to standby position"); + } + + if (gcodegen.config().standby_temperature_delta.value != 0) { + // we assume that heating is always slower than cooling, so no need to block + gcode += gcodegen.writer().set_temperature + (this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, gcodegen.writer().extruder()->id()); + } + + return gcode; + } + + std::string OozePrevention::post_toolchange(GCode& gcodegen) + { + return (gcodegen.config().standby_temperature_delta.value != 0) ? + gcodegen.writer().set_temperature(this->_get_temp(gcodegen), true, gcodegen.writer().extruder()->id()) : + std::string(); + } + + int + OozePrevention::_get_temp(GCode& gcodegen) + { + return (gcodegen.layer() != NULL && gcodegen.layer()->id() == 0) + ? gcodegen.config().first_layer_temperature.get_at(gcodegen.writer().extruder()->id()) + : gcodegen.config().temperature.get_at(gcodegen.writer().extruder()->id()); + } + + std::string Wipe::wipe(GCode& gcodegen, bool toolchange) + { + std::string gcode; + + /* Reduce feedrate a bit; travel speed is often too high to move on existing material. + Too fast = ripping of existing material; too slow = short wipe path, thus more blob. */ + double wipe_speed = gcodegen.writer().config.travel_speed.value * 0.8; + + // get the retraction length + double length = toolchange + ? gcodegen.writer().extruder()->retract_length_toolchange() + : gcodegen.writer().extruder()->retract_length(); + // Shorten the retraction length by the amount already retracted before wipe. + length *= (1. - gcodegen.writer().extruder()->retract_before_wipe()); + + if (length > 0) { + /* Calculate how long we need to travel in order to consume the required + amount of retraction. In other words, how far do we move in XY at wipe_speed + for the time needed to consume retract_length at retract_speed? */ + double wipe_dist = scale_(length / gcodegen.writer().extruder()->retract_speed() * wipe_speed); + + /* Take the stored wipe path and replace first point with the current actual position + (they might be different, for example, in case of loop clipping). */ + Polyline wipe_path; + wipe_path.append(gcodegen.last_pos()); + wipe_path.append( + this->path.points.begin() + 1, + this->path.points.end() + ); + + wipe_path.clip_end(wipe_path.length() - wipe_dist); + + // subdivide the retraction in segments + if (!wipe_path.empty()) { + for (const Line& line : wipe_path.lines()) { + double segment_length = line.length(); + /* Reduce retraction length a bit to avoid effective retraction speed to be greater than the configured one + due to rounding (TODO: test and/or better math for this) */ + double dE = length * (segment_length / wipe_dist) * 0.95; + //FIXME one shall not generate the unnecessary G1 Fxxx commands, here wipe_speed is a constant inside this cycle. + // Is it here for the cooling markers? Or should it be outside of the cycle? + gcode += gcodegen.writer().set_speed(wipe_speed * 60, "", gcodegen.enable_cooling_markers() ? ";_WIPE" : ""); + gcode += gcodegen.writer().extrude_to_xy( + gcodegen.point_to_gcode(line.b), + -dE, + "wipe and retract" + ); + } + gcodegen.set_last_pos(wipe_path.points.back()); + } + + // prevent wiping again on same path + this->reset_path(); + } + + return gcode; + } + + static inline Point wipe_tower_point_to_object_point(GCode& gcodegen, const Vec2f& wipe_tower_pt) + { + return Point(scale_(wipe_tower_pt.x() - gcodegen.origin()(0)), scale_(wipe_tower_pt.y() - gcodegen.origin()(1))); + } + + std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_extruder_id, double z) const + { + if (new_extruder_id != -1 && new_extruder_id != tcr.new_tool) + throw Slic3r::InvalidArgument("Error: WipeTowerIntegration::append_tcr was asked to do a toolchange it didn't expect."); + + std::string gcode; + + // Toolchangeresult.gcode assumes the wipe tower corner is at the origin (except for priming lines) + // We want to rotate and shift all extrusions (gcode postprocessing) and starting and ending position + float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); + Vec2f start_pos = tcr.start_pos; + Vec2f end_pos = tcr.end_pos; + if (!tcr.priming) { + start_pos = Eigen::Rotation2Df(alpha) * start_pos; + start_pos += m_wipe_tower_pos; + end_pos = Eigen::Rotation2Df(alpha) * end_pos; + end_pos += m_wipe_tower_pos; + } + + Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : m_wipe_tower_pos; + float wipe_tower_rotation = tcr.priming ? 0.f : alpha; + + std::string tcr_rotated_gcode = post_process_wipe_tower_moves(tcr, wipe_tower_offset, wipe_tower_rotation); + + if (!tcr.priming) { + // Move over the wipe tower. + // Retract for a tool change, using the toolchange retract value and setting the priming extra length. + gcode += gcodegen.retract(true); + gcodegen.m_avoid_crossing_perimeters.use_external_mp_once = true; + gcode += gcodegen.travel_to( + wipe_tower_point_to_object_point(gcodegen, start_pos), + erMixed, + "Travel to a Wipe Tower"); + gcode += gcodegen.unretract(); + } + + double current_z = gcodegen.writer().get_position().z(); + if (z == -1.) // in case no specific z was provided, print at current_z pos + z = current_z; + if (!is_approx(z, current_z)) { + gcode += gcodegen.writer().retract(); + gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer."); + gcode += gcodegen.writer().unretract(); + } + + + // Process the end filament gcode. + std::string end_filament_gcode_str; + if (gcodegen.writer().extruder() != nullptr) { + // Process the custom end_filament_gcode in case of single_extruder_multi_material. + unsigned int old_extruder_id = gcodegen.writer().extruder()->id(); + const std::string& end_filament_gcode = gcodegen.config().end_filament_gcode.get_at(old_extruder_id); + if (gcodegen.writer().extruder() != nullptr && !end_filament_gcode.empty()) { + end_filament_gcode_str = gcodegen.placeholder_parser_process("end_filament_gcode", end_filament_gcode, old_extruder_id); + check_add_eol(end_filament_gcode_str); + } + } + + // Process the custom toolchange_gcode. If it is empty, provide a simple Tn command to change the filament. + // Otherwise, leave control to the user completely. + std::string toolchange_gcode_str; + if (true /*gcodegen.writer().extruder() != nullptr*/) { + const std::string& toolchange_gcode = gcodegen.config().toolchange_gcode.value; + if (!toolchange_gcode.empty()) { + DynamicConfig config; + int previous_extruder_id = gcodegen.writer().extruder() ? (int)gcodegen.writer().extruder()->id() : -1; + config.set_key_value("previous_extruder", new ConfigOptionInt(previous_extruder_id)); + config.set_key_value("next_extruder", new ConfigOptionInt((int)new_extruder_id)); + config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index)); + config.set_key_value("layer_z", new ConfigOptionFloat(tcr.print_z)); + toolchange_gcode_str = gcodegen.placeholder_parser_process("toolchange_gcode", toolchange_gcode, new_extruder_id, &config); + check_add_eol(toolchange_gcode_str); + } + + std::string toolchange_command; + if (tcr.priming || (new_extruder_id >= 0 && gcodegen.writer().need_toolchange(new_extruder_id))) + toolchange_command = gcodegen.writer().toolchange(new_extruder_id); + if (!custom_gcode_changes_tool(toolchange_gcode_str, gcodegen.writer().toolchange_prefix(), new_extruder_id)) + toolchange_gcode_str += toolchange_command; + else { + // We have informed the m_writer about the current extruder_id, we can ignore the generated G-code. + } + } + + gcodegen.placeholder_parser().set("current_extruder", new_extruder_id); + + // Process the start filament gcode. + std::string start_filament_gcode_str; + const std::string& start_filament_gcode = gcodegen.config().start_filament_gcode.get_at(new_extruder_id); + if (!start_filament_gcode.empty()) { + // Process the start_filament_gcode for the active filament only. DynamicConfig config; - int previous_extruder_id = gcodegen.writer().extruder() ? (int)gcodegen.writer().extruder()->id() : -1; - config.set_key_value("previous_extruder", new ConfigOptionInt(previous_extruder_id)); - config.set_key_value("next_extruder", new ConfigOptionInt((int)new_extruder_id)); - config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(tcr.print_z)); - toolchange_gcode_str = gcodegen.placeholder_parser_process("toolchange_gcode", toolchange_gcode, new_extruder_id, &config); - check_add_eol(toolchange_gcode_str); + config.set_key_value("filament_extruder_id", new ConfigOptionInt(new_extruder_id)); + start_filament_gcode_str = gcodegen.placeholder_parser_process("start_filament_gcode", start_filament_gcode, new_extruder_id, &config); + check_add_eol(start_filament_gcode_str); } - std::string toolchange_command; - if (tcr.priming || (new_extruder_id >= 0 && gcodegen.writer().need_toolchange(new_extruder_id))) - toolchange_command = gcodegen.writer().toolchange(new_extruder_id); - if (! custom_gcode_changes_tool(toolchange_gcode_str, gcodegen.writer().toolchange_prefix(), new_extruder_id)) - toolchange_gcode_str += toolchange_command; - else { - // We have informed the m_writer about the current extruder_id, we can ignore the generated G-code. - } - } - - gcodegen.placeholder_parser().set("current_extruder", new_extruder_id); - - // Process the start filament gcode. - std::string start_filament_gcode_str; - const std::string &start_filament_gcode = gcodegen.config().start_filament_gcode.get_at(new_extruder_id); - if (! start_filament_gcode.empty()) { - // Process the start_filament_gcode for the active filament only. + // Insert the end filament, toolchange, and start filament gcode into the generated gcode. DynamicConfig config; - config.set_key_value("filament_extruder_id", new ConfigOptionInt(new_extruder_id)); - start_filament_gcode_str = gcodegen.placeholder_parser_process("start_filament_gcode", start_filament_gcode, new_extruder_id, &config); - check_add_eol(start_filament_gcode_str); + config.set_key_value("end_filament_gcode", new ConfigOptionString(end_filament_gcode_str)); + config.set_key_value("toolchange_gcode", new ConfigOptionString(toolchange_gcode_str)); + config.set_key_value("start_filament_gcode", new ConfigOptionString(start_filament_gcode_str)); + std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config); + unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); + gcode += tcr_gcode; + check_add_eol(toolchange_gcode_str); + + + // A phony move to the end position at the wipe tower. + gcodegen.writer().travel_to_xy(end_pos.cast()); + gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, end_pos)); + if (!is_approx(z, current_z)) { + gcode += gcodegen.writer().retract(); + gcode += gcodegen.writer().travel_to_z(current_z, "Travel back up to the topmost object layer."); + gcode += gcodegen.writer().unretract(); + } + + else { + // Prepare a future wipe. + gcodegen.m_wipe.path.points.clear(); + if (new_extruder_id >= 0) { + // Start the wipe at the current position. + gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, end_pos)); + // Wipe end point: Wipe direction away from the closer tower edge to the further tower edge. + gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, + Vec2f((std::abs(m_left - end_pos.x()) < std::abs(m_right - end_pos.x())) ? m_right : m_left, + end_pos.y()))); + } + } + + // Let the planner know we are traveling between objects. + gcodegen.m_avoid_crossing_perimeters.use_external_mp_once = true; + return gcode; } - // Insert the end filament, toolchange, and start filament gcode into the generated gcode. - DynamicConfig config; - config.set_key_value("end_filament_gcode", new ConfigOptionString(end_filament_gcode_str)); - config.set_key_value("toolchange_gcode", new ConfigOptionString(toolchange_gcode_str)); - config.set_key_value("start_filament_gcode", new ConfigOptionString(start_filament_gcode_str)); - std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config); - unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); - gcode += tcr_gcode; - check_add_eol(toolchange_gcode_str); + // This function postprocesses gcode_original, rotates and moves all G1 extrusions and returns resulting gcode + // Starting position has to be supplied explicitely (otherwise it would fail in case first G1 command only contained one coordinate) + std::string WipeTowerIntegration::post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const + { + Vec2f extruder_offset = m_extruder_offsets[tcr.initial_tool].cast(); + std::istringstream gcode_str(tcr.gcode); + std::string gcode_out; + std::string line; + Vec2f pos = tcr.start_pos; + Vec2f transformed_pos = pos; + Vec2f old_pos(-1000.1f, -1000.1f); - // A phony move to the end position at the wipe tower. - gcodegen.writer().travel_to_xy(end_pos.cast()); - gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, end_pos)); - if (! is_approx(z, current_z)) { - gcode += gcodegen.writer().retract(); - gcode += gcodegen.writer().travel_to_z(current_z, "Travel back up to the topmost object layer."); - gcode += gcodegen.writer().unretract(); + while (gcode_str) { + std::getline(gcode_str, line); // we read the gcode line by line + + // All G1 commands should be translated and rotated. X and Y coords are + // only pushed to the output when they differ from last time. + // WT generator can override this by appending the never_skip_tag + if (line.find("G1 ") == 0) { + bool never_skip = false; + auto it = line.find(WipeTower::never_skip_tag()); + if (it != std::string::npos) { + // remove the tag and remember we saw it + never_skip = true; + line.erase(it, it + WipeTower::never_skip_tag().size()); + } + std::ostringstream line_out; + std::istringstream line_str(line); + line_str >> std::noskipws; // don't skip whitespace + char ch = 0; + while (line_str >> ch) { + if (ch == 'X' || ch == 'Y') + line_str >> (ch == 'X' ? pos.x() : pos.y()); + else + line_out << ch; + } + + transformed_pos = Eigen::Rotation2Df(angle) * pos + translation; + + if (transformed_pos != old_pos || never_skip) { + line = line_out.str(); + std::ostringstream oss; + oss << std::fixed << std::setprecision(3) << "G1 "; + if (transformed_pos.x() != old_pos.x() || never_skip) + oss << " X" << transformed_pos.x() - extruder_offset.x(); + if (transformed_pos.y() != old_pos.y() || never_skip) + oss << " Y" << transformed_pos.y() - extruder_offset.y(); + oss << " "; + line.replace(line.find("G1 "), 3, oss.str()); + old_pos = transformed_pos; + } + } + + gcode_out += line + "\n"; + + // If this was a toolchange command, we should change current extruder offset + if (line == "[toolchange_gcode]") { + extruder_offset = m_extruder_offsets[tcr.new_tool].cast(); + + // If the extruder offset changed, add an extra move so everything is continuous + if (extruder_offset != m_extruder_offsets[tcr.initial_tool].cast()) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(3) + << "G1 X" << transformed_pos.x() - extruder_offset.x() + << " Y" << transformed_pos.y() - extruder_offset.y() + << "\n"; + gcode_out += oss.str(); + } + } + } + return gcode_out; } - else { + + std::string WipeTowerIntegration::prime(GCode& gcodegen) + { + assert(m_layer_idx == 0); + std::string gcode; + + + // Disable linear advance for the wipe tower operations. + //gcode += (gcodegen.config().gcode_flavor == gcfRepRapSprinter ? std::string("M572 D0 S0\n") : std::string("M900 K0\n")); + + for (const WipeTower::ToolChangeResult& tcr : m_priming) { + if (!tcr.extrusions.empty()) + gcode += append_tcr(gcodegen, tcr, tcr.new_tool); + + + // Let the tool change be executed by the wipe tower class. + // Inform the G-code writer about the changes done behind its back. + //gcode += tcr.gcode; + // Let the m_writer know the current extruder_id, but ignore the generated G-code. + // unsigned int current_extruder_id = tcr.extrusions.back().tool; + // gcodegen.writer().toolchange(current_extruder_id); + // gcodegen.placeholder_parser().set("current_extruder", current_extruder_id); + + } + + // A phony move to the end position at the wipe tower. + /* gcodegen.writer().travel_to_xy(Vec2d(m_priming.back().end_pos.x, m_priming.back().end_pos.y)); + gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, m_priming.back().end_pos)); // Prepare a future wipe. gcodegen.m_wipe.path.points.clear(); - if (new_extruder_id >= 0) { - // Start the wipe at the current position. - gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, end_pos)); - // Wipe end point: Wipe direction away from the closer tower edge to the further tower edge. - gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, - Vec2f((std::abs(m_left - end_pos.x()) < std::abs(m_right - end_pos.x())) ? m_right : m_left, - end_pos.y()))); + // Start the wipe at the current position. + gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, m_priming.back().end_pos)); + // Wipe end point: Wipe direction away from the closer tower edge to the further tower edge. + gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, + WipeTower::xy((std::abs(m_left - m_priming.back().end_pos.x) < std::abs(m_right - m_priming.back().end_pos.x)) ? m_right : m_left, + m_priming.back().end_pos.y)));*/ + + return gcode; + } + + std::string WipeTowerIntegration::tool_change(GCode& gcodegen, int extruder_id, bool finish_layer) + { + std::string gcode; + assert(m_layer_idx >= 0); + if (!m_brim_done || gcodegen.writer().need_toolchange(extruder_id) || finish_layer) { + if (m_layer_idx < (int)m_tool_changes.size()) { + if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size())) + throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer."); + + + // Calculate where the wipe tower layer will be printed. -1 means that print z will not change, + // resulting in a wipe tower with sparse layers. + double wipe_tower_z = -1; + bool ignore_sparse = false; + if (gcodegen.config().wipe_tower_no_sparse_layers.value) { + wipe_tower_z = m_last_wipe_tower_print_z; + ignore_sparse = (m_brim_done && m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool); + if (m_tool_change_idx == 0 && !ignore_sparse) + wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height; + } + + if (!ignore_sparse) { + gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z); + m_last_wipe_tower_print_z = wipe_tower_z; + } + } + m_brim_done = true; } + return gcode; } - // Let the planner know we are traveling between objects. - gcodegen.m_avoid_crossing_perimeters.use_external_mp_once = true; - return gcode; -} - -// This function postprocesses gcode_original, rotates and moves all G1 extrusions and returns resulting gcode -// Starting position has to be supplied explicitely (otherwise it would fail in case first G1 command only contained one coordinate) -std::string WipeTowerIntegration::post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const -{ - Vec2f extruder_offset = m_extruder_offsets[tcr.initial_tool].cast(); - - std::istringstream gcode_str(tcr.gcode); - std::string gcode_out; - std::string line; - Vec2f pos = tcr.start_pos; - Vec2f transformed_pos = pos; - Vec2f old_pos(-1000.1f, -1000.1f); - - while (gcode_str) { - std::getline(gcode_str, line); // we read the gcode line by line - - // All G1 commands should be translated and rotated. X and Y coords are - // only pushed to the output when they differ from last time. - // WT generator can override this by appending the never_skip_tag - if (line.find("G1 ") == 0) { - bool never_skip = false; - auto it = line.find(WipeTower::never_skip_tag()); - if (it != std::string::npos) { - // remove the tag and remember we saw it - never_skip = true; - line.erase(it, it+WipeTower::never_skip_tag().size()); - } - std::ostringstream line_out; - std::istringstream line_str(line); - line_str >> std::noskipws; // don't skip whitespace - char ch = 0; - while (line_str >> ch) { - if (ch == 'X' || ch =='Y') - line_str >> (ch == 'X' ? pos.x() : pos.y()); - else - line_out << ch; - } - - transformed_pos = Eigen::Rotation2Df(angle) * pos + translation; - - if (transformed_pos != old_pos || never_skip) { - line = line_out.str(); - std::ostringstream oss; - oss << std::fixed << std::setprecision(3) << "G1 "; - if (transformed_pos.x() != old_pos.x() || never_skip) - oss << " X" << transformed_pos.x() - extruder_offset.x(); - if (transformed_pos.y() != old_pos.y() || never_skip) - oss << " Y" << transformed_pos.y() - extruder_offset.y(); - oss << " "; - line.replace(line.find("G1 "), 3, oss.str()); - old_pos = transformed_pos; - } - } - - gcode_out += line + "\n"; - - // If this was a toolchange command, we should change current extruder offset - if (line == "[toolchange_gcode]") { - extruder_offset = m_extruder_offsets[tcr.new_tool].cast(); - - // If the extruder offset changed, add an extra move so everything is continuous - if (extruder_offset != m_extruder_offsets[tcr.initial_tool].cast()) { - std::ostringstream oss; - oss << std::fixed << std::setprecision(3) - << "G1 X" << transformed_pos.x() - extruder_offset.x() - << " Y" << transformed_pos.y() - extruder_offset.y() - << "\n"; - gcode_out += oss.str(); - } - } - } - return gcode_out; -} - - -std::string WipeTowerIntegration::prime(GCode &gcodegen) -{ - assert(m_layer_idx == 0); - std::string gcode; - - - // Disable linear advance for the wipe tower operations. - //gcode += (gcodegen.config().gcode_flavor == gcfRepRapSprinter ? std::string("M572 D0 S0\n") : std::string("M900 K0\n")); - - for (const WipeTower::ToolChangeResult& tcr : m_priming) { - if (!tcr.extrusions.empty()) - gcode += append_tcr(gcodegen, tcr, tcr.new_tool); - - - // Let the tool change be executed by the wipe tower class. - // Inform the G-code writer about the changes done behind its back. - //gcode += tcr.gcode; - // Let the m_writer know the current extruder_id, but ignore the generated G-code. - // unsigned int current_extruder_id = tcr.extrusions.back().tool; - // gcodegen.writer().toolchange(current_extruder_id); - // gcodegen.placeholder_parser().set("current_extruder", current_extruder_id); - + // Print is finished. Now it remains to unload the filament safely with ramming over the wipe tower. + std::string WipeTowerIntegration::finalize(GCode& gcodegen) + { + std::string gcode; + if (std::abs(gcodegen.writer().get_position()(2) - m_final_purge.print_z) > EPSILON) + gcode += gcodegen.change_layer(m_final_purge.print_z); + gcode += append_tcr(gcodegen, m_final_purge, -1); + return gcode; } - // A phony move to the end position at the wipe tower. - /* gcodegen.writer().travel_to_xy(Vec2d(m_priming.back().end_pos.x, m_priming.back().end_pos.y)); - gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, m_priming.back().end_pos)); - // Prepare a future wipe. - gcodegen.m_wipe.path.points.clear(); - // Start the wipe at the current position. - gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, m_priming.back().end_pos)); - // Wipe end point: Wipe direction away from the closer tower edge to the further tower edge. - gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, - WipeTower::xy((std::abs(m_left - m_priming.back().end_pos.x) < std::abs(m_right - m_priming.back().end_pos.x)) ? m_right : m_left, - m_priming.back().end_pos.y)));*/ - - return gcode; -} - -std::string WipeTowerIntegration::tool_change(GCode &gcodegen, int extruder_id, bool finish_layer) -{ - std::string gcode; - assert(m_layer_idx >= 0); - if (! m_brim_done || gcodegen.writer().need_toolchange(extruder_id) || finish_layer) { - if (m_layer_idx < (int)m_tool_changes.size()) { - if (! (size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size())) - throw std::runtime_error("Wipe tower generation failed, possibly due to empty first layer."); - - - // Calculate where the wipe tower layer will be printed. -1 means that print z will not change, - // resulting in a wipe tower with sparse layers. - double wipe_tower_z = -1; - bool ignore_sparse = false; - if (gcodegen.config().wipe_tower_no_sparse_layers.value) { - wipe_tower_z = m_last_wipe_tower_print_z; - ignore_sparse = (m_brim_done && m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool); - if (m_tool_change_idx == 0 && ! ignore_sparse) - wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height; - } - - if (! ignore_sparse) { - gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z); - m_last_wipe_tower_print_z = wipe_tower_z; - } - } - m_brim_done = true; - } - return gcode; -} - -// Print is finished. Now it remains to unload the filament safely with ramming over the wipe tower. -std::string WipeTowerIntegration::finalize(GCode &gcodegen) -{ - std::string gcode; - if (std::abs(gcodegen.writer().get_position()(2) - m_final_purge.print_z) > EPSILON) - gcode += gcodegen.change_layer(m_final_purge.print_z); - gcode += append_tcr(gcodegen, m_final_purge, -1); - return gcode; -} +#if ENABLE_GCODE_VIEWER + const std::vector ColorPrintColors::Colors = { "#C0392B", "#E67E22", "#F1C40F", "#27AE60", "#1ABC9C", "#2980B9", "#9B59B6" }; +#endif // ENABLE_GCODE_VIEWER #define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.extruder()->id()) // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. -std::vector GCode::collect_layers_to_print(const PrintObject &object) +std::vector GCode::collect_layers_to_print(const PrintObject& object) { std::vector layers_to_print; layers_to_print.reserve(object.layers().size() + object.support_layers().size()); - // Calculate a minimum support layer height as a minimum over all extruders, but not smaller than 10um. - // This is the same logic as in support generator. - //FIXME should we use the printing extruders instead? - double gap_over_supports = object.config().support_material_contact_distance; - // FIXME should we test object.config().support_material_synchronize_layers ? Currently the support layers are synchronized with object layers iff soluble supports. - assert(! object.config().support_material || gap_over_supports != 0. || object.config().support_material_synchronize_layers); + // Calculate a minimum support layer height as a minimum over all extruders, but not smaller than 10um. + // This is the same logic as in support generator. + //FIXME should we use the printing extruders instead? + double gap_over_supports = object.config().support_material_contact_distance; + // FIXME should we test object.config().support_material_synchronize_layers ? Currently the support layers are synchronized with object layers iff soluble supports. + assert(!object.config().support_material || gap_over_supports != 0. || object.config().support_material_synchronize_layers); if (gap_over_supports != 0.) { gap_over_supports = std::max(0., gap_over_supports); - // Not a soluble support, - double support_layer_height_min = 1000000.; - for (auto lh : object.print()->config().min_layer_height.values) - support_layer_height_min = std::min(support_layer_height_min, std::max(0.01, lh)); - gap_over_supports += support_layer_height_min; + // Not a soluble support, + double support_layer_height_min = 1000000.; + for (auto lh : object.print()->config().min_layer_height.values) + support_layer_height_min = std::min(support_layer_height_min, std::max(0.01, lh)); + gap_over_supports += support_layer_height_min; } // Pair the object layers with the support layers by z. - size_t idx_object_layer = 0; + size_t idx_object_layer = 0; size_t idx_support_layer = 0; const LayerToPrint* last_extrusion_layer = nullptr; while (idx_object_layer < object.layers().size() || idx_support_layer < object.support_layers().size()) { LayerToPrint layer_to_print; - layer_to_print.object_layer = (idx_object_layer < object.layers().size()) ? object.layers()[idx_object_layer ++] : nullptr; - layer_to_print.support_layer = (idx_support_layer < object.support_layers().size()) ? object.support_layers()[idx_support_layer ++] : nullptr; + layer_to_print.object_layer = (idx_object_layer < object.layers().size()) ? object.layers()[idx_object_layer++] : nullptr; + layer_to_print.support_layer = (idx_support_layer < object.support_layers().size()) ? object.support_layers()[idx_support_layer++] : nullptr; if (layer_to_print.object_layer && layer_to_print.support_layer) { if (layer_to_print.object_layer->print_z < layer_to_print.support_layer->print_z - EPSILON) { layer_to_print.support_layer = nullptr; - -- idx_support_layer; - } else if (layer_to_print.support_layer->print_z < layer_to_print.object_layer->print_z - EPSILON) { + --idx_support_layer; + } + else if (layer_to_print.support_layer->print_z < layer_to_print.object_layer->print_z - EPSILON) { layer_to_print.object_layer = nullptr; - -- idx_object_layer; + --idx_object_layer; } } layers_to_print.emplace_back(layer_to_print); + bool has_extrusions = (layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions()) + || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions()); + // Check that there are extrusions on the very first layer. if (layers_to_print.size() == 1u) { - if ((layer_to_print.object_layer && ! layer_to_print.object_layer->has_extrusions()) - || (layer_to_print.support_layer && ! layer_to_print.support_layer->has_extrusions())) - throw std::runtime_error(_(L("There is an object with no extrusions on the first layer."))); + if (!has_extrusions) + throw Slic3r::RuntimeError(_(L("There is an object with no extrusions on the first layer."))); } // In case there are extrusions on this layer, check there is a layer to lay it on. if ((layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions()) - // Allow empty support layers, as the support generator may produce no extrusions for non-empty support regions. - || (layer_to_print.support_layer /* && layer_to_print.support_layer->has_extrusions() */)) { + // Allow empty support layers, as the support generator may produce no extrusions for non-empty support regions. + || (layer_to_print.support_layer /* && layer_to_print.support_layer->has_extrusions() */)) { double support_contact_z = (last_extrusion_layer && last_extrusion_layer->support_layer) - ? gap_over_supports - : 0.; + ? gap_over_supports + : 0.; double maximal_print_z = (last_extrusion_layer ? last_extrusion_layer->print_z() : 0.) - + layer_to_print.layer()->height - + support_contact_z; + + layer_to_print.layer()->height + + support_contact_z; // Negative support_contact_z is not taken into account, it can result in false positives in cases // where previous layer has object extrusions too (https://github.com/prusa3d/PrusaSlicer/issues/2752) - // Only check this layer in case it has some extrusions. - bool has_extrusions = (layer_to_print.object_layer && layer_to_print.object_layer->has_extrusions()) - || (layer_to_print.support_layer && layer_to_print.support_layer->has_extrusions()); - if (has_extrusions && layer_to_print.print_z() > maximal_print_z + 2. * EPSILON) { const_cast(object.print())->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, _(L("Empty layers detected, the output would not be printable.")) + "\n\n" + _(L("Object name")) + ": " + object.model_object()->name + "\n" + _(L("Print z")) + ": " + std::to_string(layers_to_print.back().print_z()) + "\n\n" + _(L("This is " - "usually caused by negligibly small extrusions or by a faulty model. Try to repair " - "the model or change its orientation on the bed."))); + "usually caused by negligibly small extrusions or by a faulty model. Try to repair " + "the model or change its orientation on the bed."))); } // Remember last layer with extrusions. @@ -662,7 +666,7 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // Prepare for non-sequential printing of multiple objects: Support resp. object layers with nearly identical print_z // will be printed for all objects at once. // Return a list of items. -std::vector>> GCode::collect_layers_to_print(const Print &print) +std::vector>> GCode::collect_layers_to_print(const Print& print) { struct OrderingItem { coordf_t print_z; @@ -677,30 +681,31 @@ std::vector>> GCode::collec OrderingItem ordering_item; ordering_item.object_idx = i; ordering.reserve(ordering.size() + per_object[i].size()); - const LayerToPrint &front = per_object[i].front(); - for (const LayerToPrint <p : per_object[i]) { - ordering_item.print_z = ltp.print_z(); + const LayerToPrint& front = per_object[i].front(); + for (const LayerToPrint& ltp : per_object[i]) { + ordering_item.print_z = ltp.print_z(); ordering_item.layer_idx = <p - &front; ordering.emplace_back(ordering_item); } } - std::sort(ordering.begin(), ordering.end(), [](const OrderingItem &oi1, const OrderingItem &oi2) { return oi1.print_z < oi2.print_z; }); + std::sort(ordering.begin(), ordering.end(), [](const OrderingItem& oi1, const OrderingItem& oi2) { return oi1.print_z < oi2.print_z; }); std::vector>> layers_to_print; + // Merge numerically very close Z values. for (size_t i = 0; i < ordering.size();) { // Find the last layer with roughly the same print_z. size_t j = i + 1; coordf_t zmax = ordering[i].print_z + EPSILON; - for (; j < ordering.size() && ordering[j].print_z <= zmax; ++ j) ; + for (; j < ordering.size() && ordering[j].print_z <= zmax; ++j); // Merge into layers_to_print. std::pair> merged; // Assign an average print_z to the set of layers with nearly equal print_z. - merged.first = 0.5 * (ordering[i].print_z + ordering[j-1].print_z); + merged.first = 0.5 * (ordering[i].print_z + ordering[j - 1].print_z); merged.second.assign(print.objects().size(), LayerToPrint()); for (; i < j; ++i) { - const OrderingItem &oi = ordering[i]; + const OrderingItem& oi = ordering[i]; assert(merged.second[oi.object_idx].layer() == nullptr); merged.second[oi.object_idx] = std::move(per_object[oi.object_idx][oi.layer_idx]); } @@ -710,7 +715,22 @@ std::vector>> GCode::collec return layers_to_print; } +#if ENABLE_GCODE_VIEWER +// free functions called by GCode::do_export() +namespace DoExport { + static void update_print_estimated_times_stats(const GCodeProcessor& processor, PrintStatistics& print_statistics) + { + const GCodeProcessor::Result& result = processor.get_result(); + print_statistics.estimated_normal_print_time = get_time_dhm(result.time_statistics.modes[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Normal)].time); + print_statistics.estimated_silent_print_time = processor.is_stealth_time_estimator_enabled() ? + get_time_dhm(result.time_statistics.modes[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Stealth)].time) : "N/A"; + } +} // namespace DoExport + +void GCode::do_export(Print* print, const char* path, GCodeProcessor::Result* result, ThumbnailsGeneratorCallback thumbnail_cb) +#else void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_data, ThumbnailsGeneratorCallback thumbnail_cb) +#endif // ENABLE_GCODE_VIEWER { PROFILE_CLEAR(); @@ -730,9 +750,11 @@ void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_ FILE *file = boost::nowide::fopen(path_tmp.c_str(), "wb"); if (file == nullptr) - throw std::runtime_error(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); + throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); +#if !ENABLE_GCODE_VIEWER m_enable_analyzer = preview_data != nullptr; +#endif // !ENABLE_GCODE_VIEWER try { m_placeholder_parser_failed_templates.clear(); @@ -741,7 +763,7 @@ void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_ if (ferror(file)) { fclose(file); boost::nowide::remove(path_tmp.c_str()); - throw std::runtime_error(std::string("G-code export to ") + path + " failed\nIs the disk full?\n"); + throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n"); } } catch (std::exception & /* ex */) { // Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown. @@ -762,9 +784,17 @@ void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_ msg += " !!!!! Failed to process the custom G-code template ...\n"; msg += "and\n"; msg += " !!!!! End of an error report for the custom G-code template ...\n"; - throw std::runtime_error(msg); + throw Slic3r::RuntimeError(msg); } +#if ENABLE_GCODE_VIEWER + BOOST_LOG_TRIVIAL(debug) << "Start processing gcode, " << log_memory_info(); + m_processor.process_file(path_tmp, [print]() { print->throw_if_canceled(); }); + DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics); + if (result != nullptr) + *result = std::move(m_processor.extract_result()); + BOOST_LOG_TRIVIAL(debug) << "Finished processing gcode, " << log_memory_info(); +#else GCodeTimeEstimator::PostProcessData normal_data = m_normal_time_estimator.get_post_process_data(); GCodeTimeEstimator::PostProcessData silent_data = m_silent_time_estimator.get_post_process_data(); @@ -773,8 +803,7 @@ void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_ BOOST_LOG_TRIVIAL(debug) << "Time estimator post processing" << log_memory_info(); GCodeTimeEstimator::post_process(path_tmp, 60.0f, remaining_times_enabled ? &normal_data : nullptr, (remaining_times_enabled && m_silent_time_estimator_enabled) ? &silent_data : nullptr); - if (remaining_times_enabled) - { + if (remaining_times_enabled) { m_normal_time_estimator.reset(); if (m_silent_time_estimator_enabled) m_silent_time_estimator.reset(); @@ -786,9 +815,10 @@ void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_ m_analyzer.calc_gcode_preview_data(*preview_data, [print]() { print->throw_if_canceled(); }); m_analyzer.reset(); } +#endif // ENABLE_GCODE_VIEWER if (rename_file(path_tmp, path)) - throw std::runtime_error( + throw Slic3r::RuntimeError( std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "Is " + path_tmp + " locked?" + '\n'); @@ -802,7 +832,8 @@ void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_ // free functions called by GCode::_do_export() namespace DoExport { - static void init_time_estimators(const PrintConfig &config, GCodeTimeEstimator &normal_time_estimator, GCodeTimeEstimator &silent_time_estimator, bool &silent_time_estimator_enabled) +#if !ENABLE_GCODE_VIEWER + static void init_time_estimators(const PrintConfig &config, GCodeTimeEstimator &normal_time_estimator, GCodeTimeEstimator &silent_time_estimator, bool &silent_time_estimator_enabled) { // resets time estimators normal_time_estimator.reset(); @@ -874,8 +905,18 @@ namespace DoExport { normal_time_estimator.set_filament_unload_times(config.filament_unload_time.values); } } +#endif // !ENABLE_GCODE_VIEWER - static void init_gcode_analyzer(const PrintConfig &config, GCodeAnalyzer &analyzer) +#if ENABLE_GCODE_VIEWER + static void init_gcode_processor(const PrintConfig& config, GCodeProcessor& processor, bool& silent_time_estimator_enabled) + { + silent_time_estimator_enabled = (config.gcode_flavor == gcfMarlin) && config.silent_mode; + processor.reset(); + processor.apply_config(config); + processor.enable_stealth_time_estimator(silent_time_estimator_enabled); + } +#else + static void init_gcode_analyzer(const PrintConfig &config, GCodeAnalyzer &analyzer) { // resets analyzer analyzer.reset(); @@ -899,7 +940,8 @@ namespace DoExport { // tell analyzer about the gcode flavor analyzer.set_gcode_flavor(config.gcode_flavor); - } + } +#endif // ENABLE_GCODE_VIEWER static double autospeed_volumetric_limit(const Print &print) { @@ -1021,24 +1063,28 @@ namespace DoExport { } // Fill in print_statistics and return formatted string containing filament statistics to be inserted into G-code comment section. - static std::string update_print_stats_and_format_filament_stats( - const GCodeTimeEstimator &normal_time_estimator, + static std::string update_print_stats_and_format_filament_stats( +#if !ENABLE_GCODE_VIEWER + const GCodeTimeEstimator &normal_time_estimator, const GCodeTimeEstimator &silent_time_estimator, const bool silent_time_estimator_enabled, - const bool has_wipe_tower, +#endif // !ENABLE_GCODE_VIEWER + const bool has_wipe_tower, const WipeTowerData &wipe_tower_data, const std::vector &extruders, PrintStatistics &print_statistics) - { + { std::string filament_stats_string_out; print_statistics.clear(); - print_statistics.estimated_normal_print_time = normal_time_estimator.get_time_dhm/*s*/(); - print_statistics.estimated_silent_print_time = silent_time_estimator_enabled ? silent_time_estimator.get_time_dhm/*s*/() : "N/A"; - print_statistics.estimated_normal_custom_gcode_print_times = normal_time_estimator.get_custom_gcode_times_dhm(true); - if (silent_time_estimator_enabled) - print_statistics.estimated_silent_custom_gcode_print_times = silent_time_estimator.get_custom_gcode_times_dhm(true); - print_statistics.total_toolchanges = std::max(0, wipe_tower_data.number_of_toolchanges); +#if !ENABLE_GCODE_VIEWER + print_statistics.estimated_normal_print_time = normal_time_estimator.get_time_dhm/*s*/(); + print_statistics.estimated_silent_print_time = silent_time_estimator_enabled ? silent_time_estimator.get_time_dhm/*s*/() : "N/A"; + print_statistics.estimated_normal_custom_gcode_print_times = normal_time_estimator.get_custom_gcode_times_dhm(true); + if (silent_time_estimator_enabled) + print_statistics.estimated_silent_custom_gcode_print_times = silent_time_estimator.get_custom_gcode_times_dhm(true); +#endif // !ENABLE_GCODE_VIEWER + print_statistics.total_toolchanges = std::max(0, wipe_tower_data.number_of_toolchanges); if (! extruders.empty()) { std::pair out_filament_used_mm ("; filament used [mm] = ", 0); std::pair out_filament_used_cm3("; filament used [cm3] = ", 0); @@ -1128,15 +1174,29 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu { PROFILE_FUNC(); - DoExport::init_time_estimators(print.config(), - // modifies the following: - m_normal_time_estimator, m_silent_time_estimator, m_silent_time_estimator_enabled); +#if ENABLE_GCODE_VIEWER + // modifies m_silent_time_estimator_enabled + DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); +#else + DoExport::init_time_estimators(print.config(), + // modifies the following: + m_normal_time_estimator, m_silent_time_estimator, m_silent_time_estimator_enabled); DoExport::init_gcode_analyzer(print.config(), m_analyzer); +#endif // ENABLE_GCODE_VIEWER // resets analyzer's tracking data +#if ENABLE_GCODE_VIEWER + m_last_height = 0.0f; + m_last_layer_z = 0.0f; +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + m_last_mm3_per_mm = 0.0; + m_last_width = 0.0f; +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING +#else m_last_mm3_per_mm = GCodeAnalyzer::Default_mm3_per_mm; m_last_width = GCodeAnalyzer::Default_Width; m_last_height = GCodeAnalyzer::Default_Height; +#endif // ENABLE_GCODE_VIEWER // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. @@ -1227,12 +1287,16 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu print.throw_if_canceled(); // adds tags for time estimators +#if ENABLE_GCODE_VIEWER if (print.config().remaining_times.value) - { + _writeln(file, GCodeProcessor::First_Line_M73_Placeholder_Tag); +#else + if (print.config().remaining_times.value) { _writeln(file, GCodeTimeEstimator::Normal_First_M73_Output_Placeholder_Tag); if (m_silent_time_estimator_enabled) _writeln(file, GCodeTimeEstimator::Silent_First_M73_Output_Placeholder_Tag); } +#endif // ENABLE_GCODE_VIEWER // Prepare the helper object for replacing placeholders in custom G-code and output filename. m_placeholder_parser = print.placeholder_parser(); @@ -1337,9 +1401,14 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu // Set extruder(s) temperature before and after start G-code. this->_print_first_layer_extruder_temperatures(file, print, start_gcode, initial_extruder_id, false); +#if ENABLE_GCODE_VIEWER + // adds tag for processor + _write_format(file, ";%s%s\n", GCodeProcessor::Extrusion_Role_Tag.c_str(), ExtrusionEntity::role_to_string(erCustom).c_str()); +#else if (m_enable_analyzer) // adds tag for analyzer _write_format(file, ";%s%d\n", GCodeAnalyzer::Extrusion_Role_Tag.c_str(), erCustom); +#endif // ENABLE_GCODE_VIEWER // Write the custom start G-code _writeln(file, start_gcode); @@ -1490,9 +1559,14 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu _write(file, this->retract()); _write(file, m_writer.set_fan(false)); +#if ENABLE_GCODE_VIEWER + // adds tag for processor + _write_format(file, ";%s%s\n", GCodeProcessor::Extrusion_Role_Tag.c_str(), ExtrusionEntity::role_to_string(erCustom).c_str()); +#else if (m_enable_analyzer) // adds tag for analyzer _write_format(file, ";%s%d\n", GCodeAnalyzer::Extrusion_Role_Tag.c_str(), erCustom); +#endif // ENABLE_GCODE_VIEWER // Process filament-specific gcode in extruder order. { @@ -1517,25 +1591,33 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu _write(file, m_writer.postamble()); // adds tags for time estimators +#if ENABLE_GCODE_VIEWER if (print.config().remaining_times.value) - { + _writeln(file, GCodeProcessor::Last_Line_M73_Placeholder_Tag); +#else + if (print.config().remaining_times.value) { _writeln(file, GCodeTimeEstimator::Normal_Last_M73_Output_Placeholder_Tag); if (m_silent_time_estimator_enabled) _writeln(file, GCodeTimeEstimator::Silent_Last_M73_Output_Placeholder_Tag); } +#endif // ENABLE_GCODE_VIEWER print.throw_if_canceled(); // calculates estimated printing time +#if !ENABLE_GCODE_VIEWER m_normal_time_estimator.calculate_time(false); if (m_silent_time_estimator_enabled) m_silent_time_estimator.calculate_time(false); +#endif // !ENABLE_GCODE_VIEWER // Get filament stats. _write(file, DoExport::update_print_stats_and_format_filament_stats( // Const inputs - m_normal_time_estimator, m_silent_time_estimator, m_silent_time_estimator_enabled, - has_wipe_tower, print.wipe_tower_data(), +#if !ENABLE_GCODE_VIEWER + m_normal_time_estimator, m_silent_time_estimator, m_silent_time_estimator_enabled, +#endif // !ENABLE_GCODE_VIEWER + has_wipe_tower, print.wipe_tower_data(), m_writer.extruders(), // Modifies print.m_print_statistics)); @@ -1544,9 +1626,13 @@ void GCode::_do_export(Print& print, FILE* file, ThumbnailsGeneratorCallback thu _write_format(file, "; total filament cost = %.1lf\n", print.m_print_statistics.total_cost); if (print.m_print_statistics.total_toolchanges > 0) _write_format(file, "; total toolchanges = %i\n", print.m_print_statistics.total_toolchanges); +#if ENABLE_GCODE_VIEWER + _writeln(file, GCodeProcessor::Estimated_Printing_Time_Placeholder_Tag); +#else _write_format(file, "; estimated printing time (normal mode) = %s\n", m_normal_time_estimator.get_time_dhms().c_str()); if (m_silent_time_estimator_enabled) _write_format(file, "; estimated printing time (silent mode) = %s\n", m_silent_time_estimator.get_time_dhms().c_str()); +#endif // ENABLE_GCODE_VIEWER // Append full config. _write(file, "\n"); @@ -1851,10 +1937,15 @@ namespace ProcessLayer { assert(m600_extruder_before_layer >= 0); // Color Change or Tool Change as Color Change. - // add tag for analyzer - gcode += "; " + GCodeAnalyzer::Color_Change_Tag + ",T" + std::to_string(m600_extruder_before_layer) + "\n"; - // add tag for time estimator - gcode += "; " + GCodeTimeEstimator::Color_Change_Tag + "\n"; +#if ENABLE_GCODE_VIEWER + // add tag for processor + gcode += "; " + GCodeProcessor::Color_Change_Tag + ",T" + std::to_string(m600_extruder_before_layer) + "\n"; +#else + // add tag for analyzer + gcode += "; " + GCodeAnalyzer::Color_Change_Tag + ",T" + std::to_string(m600_extruder_before_layer) + "\n"; + // add tag for time estimator + gcode += "; " + GCodeTimeEstimator::Color_Change_Tag + "\n"; +#endif // ENABLE_GCODE_VIEWER if (!single_extruder_printer && m600_extruder_before_layer >= 0 && first_extruder_id != (unsigned)m600_extruder_before_layer // && !MMU1 @@ -1873,20 +1964,32 @@ namespace ProcessLayer { if (gcode_type == CustomGCode::PausePrint) // Pause print { - // add tag for analyzer - gcode += "; " + GCodeAnalyzer::Pause_Print_Tag + "\n"; - //! FIXME_in_fw show message during print pause +#if ENABLE_GCODE_VIEWER + // add tag for processor + gcode += "; " + GCodeProcessor::Pause_Print_Tag + "\n"; +#else + // add tag for analyzer + gcode += "; " + GCodeAnalyzer::Pause_Print_Tag + "\n"; +#endif // ENABLE_GCODE_VIEWER + //! FIXME_in_fw show message during print pause if (!pause_print_msg.empty()) gcode += "M117 " + pause_print_msg + "\n"; - // add tag for time estimator +#if !ENABLE_GCODE_VIEWER + // add tag for time estimator gcode += "; " + GCodeTimeEstimator::Pause_Print_Tag + "\n"; +#endif // !ENABLE_GCODE_VIEWER gcode += config.pause_print_gcode; - } + } else { - // add tag for analyzer - gcode += "; " + GCodeAnalyzer::Custom_Code_Tag + "\n"; - // add tag for time estimator +#if ENABLE_GCODE_VIEWER + // add tag for processor + gcode += "; " + GCodeProcessor::Custom_Code_Tag + "\n"; +#else + // add tag for analyzer + gcode += "; " + GCodeAnalyzer::Custom_Code_Tag + "\n"; +#endif // ENABLE_GCODE_VIEWER + // add tag for time estimator //gcode += "; " + GCodeTimeEstimator::Custom_Code_Tag + "\n"; if (gcode_type == CustomGCode::Template) // Template Cistom Gcode gcode += config.template_custom_gcode; @@ -2029,6 +2132,22 @@ void GCode::process_layer( std::string gcode; +#if ENABLE_GCODE_VIEWER + // add tag for processor + gcode += "; " + GCodeProcessor::Layer_Change_Tag + "\n"; + // export layer z + char buf[64]; + sprintf(buf, ";Z:%g\n", print_z); + gcode += buf; + // export layer height + float height = first_layer ? static_cast(print_z) : static_cast(print_z) - m_last_layer_z; + sprintf(buf, ";%s%g\n", GCodeProcessor::Height_Tag.c_str(), height); + gcode += buf; + // update caches + m_last_layer_z = static_cast(print_z); + m_last_height = height; +#endif // ENABLE_GCODE_VIEWER + // Set new layer - this will change Z and force a retraction if retract_layer_change is enabled. if (! print.config().before_layer_gcode.value.empty()) { DynamicConfig config; @@ -2242,9 +2361,15 @@ void GCode::process_layer( m_wipe_tower->tool_change(*this, extruder_id, extruder_id == layer_tools.extruders.back()) : this->set_extruder(extruder_id, print_z); +#if ENABLE_GCODE_VIEWER + // let analyzer tag generator aware of a role type change + if (layer_tools.has_wipe_tower && m_wipe_tower) + m_last_processor_extrusion_role = erWipeTower; +#else // let analyzer tag generator aware of a role type change if (m_enable_analyzer && layer_tools.has_wipe_tower && m_wipe_tower) m_last_analyzer_extrusion_role = erWipeTower; +#endif // ENABLE_GCODE_VIEWER if (auto loops_it = skirt_loops_per_extruder.find(extruder_id); loops_it != skirt_loops_per_extruder.end()) { const std::pair loops = loops_it->second; @@ -2348,11 +2473,13 @@ void GCode::process_layer( if (m_cooling_buffer) gcode = m_cooling_buffer->process_layer(gcode, layer.id()); +#if !ENABLE_GCODE_VIEWER // add tag for analyzer if (gcode.find(GCodeAnalyzer::Pause_Print_Tag) != gcode.npos) gcode += "\n; " + GCodeAnalyzer::End_Pause_Print_Or_Custom_Code_Tag + "\n"; else if (gcode.find(GCodeAnalyzer::Custom_Code_Tag) != gcode.npos) gcode += "\n; " + GCodeAnalyzer::End_Pause_Print_Or_Custom_Code_Tag + "\n"; +#endif // !ENABLE_GCODE_VIEWER #ifdef HAS_PRESSURE_EQUALIZER // Apply pressure equalization if enabled; @@ -2363,12 +2490,17 @@ void GCode::process_layer( #endif /* HAS_PRESSURE_EQUALIZER */ _write(file, gcode); - BOOST_LOG_TRIVIAL(trace) << "Exported layer " << layer.id() << " print_z " << print_z << - ", time estimator memory: " << - format_memsize_MB(m_normal_time_estimator.memory_used() + (m_silent_time_estimator_enabled ? m_silent_time_estimator.memory_used() : 0)) << - ", analyzer memory: " << - format_memsize_MB(m_analyzer.memory_used()) << +#if ENABLE_GCODE_VIEWER + BOOST_LOG_TRIVIAL(trace) << "Exported layer " << layer.id() << " print_z " << print_z << log_memory_info(); +#else + BOOST_LOG_TRIVIAL(trace) << "Exported layer " << layer.id() << " print_z " << print_z << + ", time estimator memory: " << + format_memsize_MB(m_normal_time_estimator.memory_used() + (m_silent_time_estimator_enabled ? m_silent_time_estimator.memory_used() : 0)) << + ", analyzer memory: " << + format_memsize_MB(m_analyzer.memory_used()) << + log_memory_info(); +#endif // ENABLE_GCODE_VIEWER } void GCode::apply_print_config(const PrintConfig &print_config) @@ -2499,7 +2631,7 @@ plot(p2.subs(r,0.2).subs(z,1.), (x, -1, 3), adaptive=False, nb_of_points=400) } } -static Points::iterator project_point_to_polygon_and_insert(Polygon &polygon, const Point &pt, double eps) +static Points::const_iterator project_point_to_polygon_and_insert(Polygon &polygon, const Point &pt, double eps) { assert(polygon.points.size() >= 2); if (polygon.points.size() <= 1) @@ -2687,7 +2819,7 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou // Insert a projection of last_pos into the polygon. size_t last_pos_proj_idx; { - Points::iterator it = project_point_to_polygon_and_insert(polygon, last_pos, 0.1 * nozzle_r); + auto it = project_point_to_polygon_and_insert(polygon, last_pos, 0.1 * nozzle_r); last_pos_proj_idx = it - polygon.points.begin(); } @@ -2707,11 +2839,9 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou if (was_clockwise) ccwAngle = - ccwAngle; float penalty = 0; -// if (ccwAngle <- float(PI/3.)) if (ccwAngle <- float(0.6 * PI)) // Sharp reflex vertex. We love that, it hides the seam perfectly. penalty = 0.f; -// else if (ccwAngle > float(PI/3.)) else if (ccwAngle > float(0.6 * PI)) // Seams on sharp convex vertices are more visible than on reflex vertices. penalty = penaltyConvexVertex; @@ -2724,7 +2854,6 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou penalty = penaltyConvexVertex + (penaltyFlatSurface - penaltyConvexVertex) * bspline_kernel(ccwAngle * float(PI * 2. / 3.)); } // Give a negative penalty for points close to the last point or the prefered seam location. - //float dist_to_last_pos_proj = last_pos_proj.distance_to(polygon.points[i]); float dist_to_last_pos_proj = (i < last_pos_proj_idx) ? std::min(lengths[last_pos_proj_idx] - lengths[i], lengths.back() - lengths[last_pos_proj_idx] + lengths[i]) : std::min(lengths[i] - lengths[last_pos_proj_idx], lengths.back() - lengths[i] + lengths[last_pos_proj_idx]); @@ -2744,14 +2873,10 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou // Signed distance is positive outside the object, negative inside the object. // The point is considered at an overhang, if it is more than nozzle radius // outside of the lower layer contour. - #ifdef NDEBUG // to suppress unused variable warning in release mode - (*lower_layer_edge_grid)->signed_distance(p, search_r, dist); - #else - bool found = (*lower_layer_edge_grid)->signed_distance(p, search_r, dist); - #endif + [[maybe_unused]] bool found = (*lower_layer_edge_grid)->signed_distance(p, search_r, dist); // If the approximate Signed Distance Field was initialized over lower_layer_edge_grid, // then the signed distnace shall always be known. - assert(found); + assert(found); penalties[i] += extrudate_overlap_penalty(float(nozzle_r), penaltyOverhangHalf, float(dist)); } } @@ -2759,7 +2884,6 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou // Find a point with a minimum penalty. size_t idx_min = std::min_element(penalties.begin(), penalties.end()) - penalties.begin(); - // if (seam_position == spAligned) // For all (aligned, nearest, rear) seams: { // Very likely the weight of idx_min is very close to the weight of last_pos_proj_idx. @@ -2918,7 +3042,7 @@ std::string GCode::extrude_entity(const ExtrusionEntity &entity, std::string des else if (const ExtrusionLoop* loop = dynamic_cast(&entity)) return this->extrude_loop(*loop, description, speed, lower_layer_edge_grid); else - throw std::invalid_argument("Invalid argument supplied to extrude()"); + throw Slic3r::InvalidArgument("Invalid argument supplied to extrude()"); return ""; } @@ -3008,15 +3132,21 @@ std::string GCode::extrude_support(const ExtrusionEntityCollection &support_fill void GCode::_write(FILE* file, const char *what) { if (what != nullptr) { +#if ENABLE_GCODE_VIEWER + const char* gcode = what; +#else // apply analyzer, if enabled const char* gcode = m_enable_analyzer ? m_analyzer.process_gcode(what).c_str() : what; +#endif // !ENABLE_GCODE_VIEWER // writes string to file fwrite(gcode, 1, ::strlen(gcode), file); +#if !ENABLE_GCODE_VIEWER // updates time estimator and gcode lines vector m_normal_time_estimator.add_gcode_block(gcode); if (m_silent_time_estimator_enabled) m_silent_time_estimator.add_gcode_block(gcode); +#endif // !ENABLE_GCODE_VIEWER } } @@ -3117,7 +3247,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } else if (path.role() == erGapFill) { speed = m_config.get_abs_value("gap_fill_speed"); } else { - throw std::invalid_argument("Invalid speed"); + throw Slic3r::InvalidArgument("Invalid speed"); } } if (this->on_first_layer()) @@ -3155,42 +3285,71 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } } - // adds analyzer tags and updates analyzer's tracking data - if (m_enable_analyzer) - { + // adds processor tags and updates processor tracking data +#if ENABLE_GCODE_VIEWER + // PrusaMultiMaterial::Writer may generate GCodeProcessor::Height_Tag lines without updating m_last_height + // so, if the last role was erWipeTower we force export of GCodeProcessor::Height_Tag lines + bool last_was_wipe_tower = (m_last_processor_extrusion_role == erWipeTower); +#else + if (m_enable_analyzer) { // PrusaMultiMaterial::Writer may generate GCodeAnalyzer::Height_Tag and GCodeAnalyzer::Width_Tag lines without updating m_last_height and m_last_width // so, if the last role was erWipeTower we force export of GCodeAnalyzer::Height_Tag and GCodeAnalyzer::Width_Tag lines bool last_was_wipe_tower = (m_last_analyzer_extrusion_role == erWipeTower); +#endif // ENABLE_GCODE_VIEWER char buf[64]; - if (path.role() != m_last_analyzer_extrusion_role) - { +#if ENABLE_GCODE_VIEWER + if (path.role() != m_last_processor_extrusion_role) { + m_last_processor_extrusion_role = path.role(); + sprintf(buf, ";%s%s\n", GCodeProcessor::Extrusion_Role_Tag.c_str(), ExtrusionEntity::role_to_string(m_last_processor_extrusion_role).c_str()); + gcode += buf; + } + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + if (last_was_wipe_tower || (m_last_mm3_per_mm != path.mm3_per_mm)) { + m_last_mm3_per_mm = path.mm3_per_mm; + sprintf(buf, ";%s%f\n", GCodeProcessor::Mm3_Per_Mm_Tag.c_str(), m_last_mm3_per_mm); + gcode += buf; + } + + if (last_was_wipe_tower || m_last_width != path.width) { + m_last_width = path.width; + sprintf(buf, ";%s%g\n", GCodeProcessor::Width_Tag.c_str(), m_last_width); + gcode += buf; + } +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + if (last_was_wipe_tower || std::abs(m_last_height - path.height) > EPSILON) { + m_last_height = path.height; + sprintf(buf, ";%s%g\n", GCodeProcessor::Height_Tag.c_str(), m_last_height); + gcode += buf; + } +#else + if (path.role() != m_last_analyzer_extrusion_role) { m_last_analyzer_extrusion_role = path.role(); sprintf(buf, ";%s%d\n", GCodeAnalyzer::Extrusion_Role_Tag.c_str(), int(m_last_analyzer_extrusion_role)); gcode += buf; } - if (last_was_wipe_tower || (m_last_mm3_per_mm != path.mm3_per_mm)) - { + if (last_was_wipe_tower || (m_last_mm3_per_mm != path.mm3_per_mm)) { m_last_mm3_per_mm = path.mm3_per_mm; sprintf(buf, ";%s%f\n", GCodeAnalyzer::Mm3_Per_Mm_Tag.c_str(), m_last_mm3_per_mm); gcode += buf; } - if (last_was_wipe_tower || (m_last_width != path.width)) - { + if (last_was_wipe_tower || m_last_width != path.width) { m_last_width = path.width; sprintf(buf, ";%s%f\n", GCodeAnalyzer::Width_Tag.c_str(), m_last_width); gcode += buf; } - if (last_was_wipe_tower || (m_last_height != path.height)) - { + if (last_was_wipe_tower || m_last_height != path.height) { m_last_height = path.height; sprintf(buf, ";%s%f\n", GCodeAnalyzer::Height_Tag.c_str(), m_last_height); gcode += buf; } } +#endif // ENABLE_GCODE_VIEWER std::string comment; if (m_enable_cooling_markers) { @@ -3509,7 +3668,7 @@ void GCode::ObjectByExtruder::Island::Region::append(const Type type, const Extr perimeters_or_infills_overrides = &infills_overrides; break; default: - throw std::invalid_argument("Unknown parameter!"); + throw Slic3r::InvalidArgument("Unknown parameter!"); } // First we append the entities, there are eec->entities.size() of them: diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 8d47337832..8bae2ef43d 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -13,9 +13,13 @@ #include "GCode/SpiralVase.hpp" #include "GCode/ToolOrdering.hpp" #include "GCode/WipeTower.hpp" -#include "GCodeTimeEstimator.hpp" -#include "EdgeGrid.hpp" +#if ENABLE_GCODE_VIEWER +#include "GCode/GCodeProcessor.hpp" +#else #include "GCode/Analyzer.hpp" +#include "GCodeTimeEstimator.hpp" +#endif // ENABLE_GCODE_VIEWER +#include "EdgeGrid.hpp" #include "GCode/ThumbnailData.hpp" #include @@ -29,7 +33,9 @@ namespace Slic3r { // Forward declarations. class GCode; +#if !ENABLE_GCODE_VIEWER class GCodePreviewData; +#endif // !ENABLE_GCODE_VIEWER namespace { struct Item; } struct PrintInstance; @@ -138,6 +144,15 @@ private: double m_last_wipe_tower_print_z = 0.f; }; +#if ENABLE_GCODE_VIEWER +class ColorPrintColors +{ + static const std::vector Colors; +public: + static const std::vector& get() { return Colors; } +}; +#endif // ENABLE_GCODE_VIEWER + class GCode { public: GCode() : @@ -145,21 +160,33 @@ public: m_enable_loop_clipping(true), m_enable_cooling_markers(false), m_enable_extrusion_role_markers(false), +#if ENABLE_GCODE_VIEWER + m_last_processor_extrusion_role(erNone), +#else m_enable_analyzer(false), m_last_analyzer_extrusion_role(erNone), +#endif // ENABLE_GCODE_VIEWER m_layer_count(0), m_layer_index(-1), m_layer(nullptr), m_volumetric_speed(0), m_last_pos_defined(false), m_last_extrusion_role(erNone), +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + m_last_mm3_per_mm(0.0), + m_last_width(0.0f), +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING +#if !ENABLE_GCODE_VIEWER m_last_mm3_per_mm(GCodeAnalyzer::Default_mm3_per_mm), m_last_width(GCodeAnalyzer::Default_Width), m_last_height(GCodeAnalyzer::Default_Height), +#endif // !ENABLE_GCODE_VIEWER m_brim_done(false), m_second_layer_things_done(false), +#if !ENABLE_GCODE_VIEWER m_normal_time_estimator(GCodeTimeEstimator::Normal), m_silent_time_estimator(GCodeTimeEstimator::Silent), +#endif // !ENABLE_GCODE_VIEWER m_silent_time_estimator_enabled(false), m_last_obj_copy(nullptr, Point(std::numeric_limits::max(), std::numeric_limits::max())) {} @@ -167,7 +194,11 @@ public: // throws std::runtime_exception on error, // throws CanceledException through print->throw_if_canceled(). +#if ENABLE_GCODE_VIEWER + void do_export(Print* print, const char* path, GCodeProcessor::Result* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); +#else void do_export(Print* print, const char* path, GCodePreviewData* preview_data = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); +#endif // ENABLE_GCODE_VIEWER // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -327,11 +358,16 @@ private: // Markers for the Pressure Equalizer to recognize the extrusion type. // The Pressure Equalizer removes the markers from the final G-code. bool m_enable_extrusion_role_markers; +#if ENABLE_GCODE_VIEWER + // Keeps track of the last extrusion role passed to the processor + ExtrusionRole m_last_processor_extrusion_role; +#else // Enableds the G-code Analyzer. // Extended markers will be added during G-code generation. // The G-code Analyzer will remove these comments from the final G-code. bool m_enable_analyzer; ExtrusionRole m_last_analyzer_extrusion_role; +#endif // ENABLE_GCODE_VIEWER // How many times will change_layer() be called? // change_layer() will update the progress bar. unsigned int m_layer_count; @@ -344,10 +380,20 @@ private: double m_volumetric_speed; // Support for the extrusion role markers. Which marker is active? ExtrusionRole m_last_extrusion_role; +#if ENABLE_GCODE_VIEWER + // Support for G-Code Processor + float m_last_height{ 0.0f }; + float m_last_layer_z{ 0.0f }; +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + double m_last_mm3_per_mm; + float m_last_width{ 0.0f }; +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING +#else // Support for G-Code Analyzer double m_last_mm3_per_mm; float m_last_width; float m_last_height; +#endif // ENABLE_GCODE_VIEWER Point m_last_pos; bool m_last_pos_defined; @@ -368,13 +414,20 @@ private: // Index of a last object copy extruded. std::pair m_last_obj_copy; +#if !ENABLE_GCODE_VIEWER // Time estimators GCodeTimeEstimator m_normal_time_estimator; GCodeTimeEstimator m_silent_time_estimator; +#endif // !ENABLE_GCODE_VIEWER bool m_silent_time_estimator_enabled; +#if ENABLE_GCODE_VIEWER + // Processor + GCodeProcessor m_processor; +#else // Analyzer GCodeAnalyzer m_analyzer; +#endif // ENABLE_GCODE_VIEWER // Write a string into a file. void _write(FILE* file, const std::string& what) { this->_write(file, what.c_str()); } diff --git a/src/libslic3r/GCode/Analyzer.cpp b/src/libslic3r/GCode/Analyzer.cpp index 73c20f1b58..d022b37983 100644 --- a/src/libslic3r/GCode/Analyzer.cpp +++ b/src/libslic3r/GCode/Analyzer.cpp @@ -12,6 +12,8 @@ #include "Analyzer.hpp" #include "PreviewData.hpp" +#if !ENABLE_GCODE_VIEWER + static const std::string AXIS_STR = "XYZE"; static const float MMMIN_TO_MMSEC = 1.0f / 60.0f; static const float INCHES_TO_MM = 25.4f; @@ -350,7 +352,7 @@ void GCodeAnalyzer::_processG1(const GCodeReader::GCodeLine& line) if (delta_pos[E] < 0.0f) { if ((delta_pos[X] != 0.0f) || (delta_pos[Y] != 0.0f) || (delta_pos[Z] != 0.0f)) - type = GCodeMove::Move; + type = GCodeMove::Move; else type = GCodeMove::Retract; } @@ -651,7 +653,7 @@ bool GCodeAnalyzer::_process_tags(const GCodeReader::GCodeLine& line) return true; } - // color change tag + // pause print tag pos = comment.find(Pause_Print_Tag); if (pos != comment.npos) { @@ -659,7 +661,7 @@ bool GCodeAnalyzer::_process_tags(const GCodeReader::GCodeLine& line) return true; } - // color change tag + // custom code tag pos = comment.find(Custom_Code_Tag); if (pos != comment.npos) { @@ -667,7 +669,7 @@ bool GCodeAnalyzer::_process_tags(const GCodeReader::GCodeLine& line) return true; } - // color change tag + // end pause print or custom code tag pos = comment.find(End_Pause_Print_Or_Custom_Code_Tag); if (pos != comment.npos) { @@ -1191,3 +1193,5 @@ size_t GCodeAnalyzer::memory_used() const } } // namespace Slic3r + +#endif // !ENABLE_GCODE_VIEWER diff --git a/src/libslic3r/GCode/Analyzer.hpp b/src/libslic3r/GCode/Analyzer.hpp index 4ac383fea6..37d9072592 100644 --- a/src/libslic3r/GCode/Analyzer.hpp +++ b/src/libslic3r/GCode/Analyzer.hpp @@ -1,6 +1,8 @@ #ifndef slic3r_GCode_Analyzer_hpp_ #define slic3r_GCode_Analyzer_hpp_ +#if !ENABLE_GCODE_VIEWER + #include "../libslic3r.h" #include "../PrintConfig.hpp" #include "../ExtrusionEntity.hpp" @@ -302,4 +304,6 @@ private: } // namespace Slic3r +#endif // !ENABLE_GCODE_VIEWER + #endif /* slic3r_GCode_Analyzer_hpp_ */ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp new file mode 100644 index 0000000000..0a5617559d --- /dev/null +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -0,0 +1,2181 @@ +#include "libslic3r/libslic3r.h" +#include "libslic3r/Utils.hpp" +#include "libslic3r/Print.hpp" +#include "GCodeProcessor.hpp" + +#include +#include +#include + +#include +#include + +#if ENABLE_GCODE_VIEWER +#include + +static const float INCHES_TO_MM = 25.4f; +static const float MMMIN_TO_MMSEC = 1.0f / 60.0f; + +static const float DEFAULT_ACCELERATION = 1500.0f; // Prusa Firmware 1_75mm_MK2 + +namespace Slic3r { + +const std::string GCodeProcessor::Extrusion_Role_Tag = "TYPE:"; +const std::string GCodeProcessor::Height_Tag = "HEIGHT:"; +const std::string GCodeProcessor::Layer_Change_Tag = "LAYER_CHANGE"; +const std::string GCodeProcessor::Color_Change_Tag = "COLOR_CHANGE"; +const std::string GCodeProcessor::Pause_Print_Tag = "PAUSE_PRINT"; +const std::string GCodeProcessor::Custom_Code_Tag = "CUSTOM_GCODE"; + +const std::string GCodeProcessor::First_Line_M73_Placeholder_Tag = "; _GP_FIRST_LINE_M73_PLACEHOLDER"; +const std::string GCodeProcessor::Last_Line_M73_Placeholder_Tag = "; _GP_LAST_LINE_M73_PLACEHOLDER"; +const std::string GCodeProcessor::Estimated_Printing_Time_Placeholder_Tag = "; _GP_ESTIMATED_PRINTING_TIME_PLACEHOLDER"; + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING +const std::string GCodeProcessor::Width_Tag = "WIDTH:"; +const std::string GCodeProcessor::Mm3_Per_Mm_Tag = "MM3_PER_MM:"; +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + +static bool is_valid_extrusion_role(int value) +{ + return (static_cast(erNone) <= value) && (value <= static_cast(erMixed)); +} + +static void set_option_value(ConfigOptionFloats& option, size_t id, float value) +{ + if (id < option.values.size()) + option.values[id] = static_cast(value); +}; + +static float get_option_value(const ConfigOptionFloats& option, size_t id) +{ + return option.values.empty() ? 0.0f : + ((id < option.values.size()) ? static_cast(option.values[id]) : static_cast(option.values.back())); +} + +static float estimated_acceleration_distance(float initial_rate, float target_rate, float acceleration) +{ + return (acceleration == 0.0f) ? 0.0f : (sqr(target_rate) - sqr(initial_rate)) / (2.0f * acceleration); +} + +static float intersection_distance(float initial_rate, float final_rate, float acceleration, float distance) +{ + return (acceleration == 0.0f) ? 0.0f : (2.0f * acceleration * distance - sqr(initial_rate) + sqr(final_rate)) / (4.0f * acceleration); +} + +static float speed_from_distance(float initial_feedrate, float distance, float acceleration) +{ + // to avoid invalid negative numbers due to numerical errors + float value = std::max(0.0f, sqr(initial_feedrate) + 2.0f * acceleration * distance); + return ::sqrt(value); +} + +// Calculates the maximum allowable speed at this point when you must be able to reach target_velocity using the +// acceleration within the allotted distance. +static float max_allowable_speed(float acceleration, float target_velocity, float distance) +{ + // to avoid invalid negative numbers due to numerical errors + float value = std::max(0.0f, sqr(target_velocity) - 2.0f * acceleration * distance); + return std::sqrt(value); +} + +static float acceleration_time_from_distance(float initial_feedrate, float distance, float acceleration) +{ + return (acceleration != 0.0f) ? (speed_from_distance(initial_feedrate, distance, acceleration) - initial_feedrate) / acceleration : 0.0f; +} + +void GCodeProcessor::CachedPosition::reset() +{ + std::fill(position.begin(), position.end(), FLT_MAX); + feedrate = FLT_MAX; +} + +void GCodeProcessor::CpColor::reset() +{ + counter = 0; + current = 0; +} + +float GCodeProcessor::Trapezoid::acceleration_time(float entry_feedrate, float acceleration) const +{ + return acceleration_time_from_distance(entry_feedrate, accelerate_until, acceleration); +} + +float GCodeProcessor::Trapezoid::cruise_time() const +{ + return (cruise_feedrate != 0.0f) ? cruise_distance() / cruise_feedrate : 0.0f; +} + +float GCodeProcessor::Trapezoid::deceleration_time(float distance, float acceleration) const +{ + return acceleration_time_from_distance(cruise_feedrate, (distance - decelerate_after), -acceleration); +} + +float GCodeProcessor::Trapezoid::cruise_distance() const +{ + return decelerate_after - accelerate_until; +} + +void GCodeProcessor::TimeBlock::calculate_trapezoid() +{ + trapezoid.cruise_feedrate = feedrate_profile.cruise; + + float accelerate_distance = std::max(0.0f, estimated_acceleration_distance(feedrate_profile.entry, feedrate_profile.cruise, acceleration)); + float decelerate_distance = std::max(0.0f, estimated_acceleration_distance(feedrate_profile.cruise, feedrate_profile.exit, -acceleration)); + float cruise_distance = distance - accelerate_distance - decelerate_distance; + + // Not enough space to reach the nominal feedrate. + // This means no cruising, and we'll have to use intersection_distance() to calculate when to abort acceleration + // and start braking in order to reach the exit_feedrate exactly at the end of this block. + if (cruise_distance < 0.0f) { + accelerate_distance = std::clamp(intersection_distance(feedrate_profile.entry, feedrate_profile.exit, acceleration, distance), 0.0f, distance); + cruise_distance = 0.0f; + trapezoid.cruise_feedrate = speed_from_distance(feedrate_profile.entry, accelerate_distance, acceleration); + } + + trapezoid.accelerate_until = accelerate_distance; + trapezoid.decelerate_after = accelerate_distance + cruise_distance; +} + +float GCodeProcessor::TimeBlock::time() const +{ + return trapezoid.acceleration_time(feedrate_profile.entry, acceleration) + + trapezoid.cruise_time() + + trapezoid.deceleration_time(distance, acceleration); +} + +void GCodeProcessor::TimeMachine::State::reset() +{ + feedrate = 0.0f; + safe_feedrate = 0.0f; + axis_feedrate = { 0.0f, 0.0f, 0.0f, 0.0f }; + abs_axis_feedrate = { 0.0f, 0.0f, 0.0f, 0.0f }; +} + +void GCodeProcessor::TimeMachine::CustomGCodeTime::reset() +{ + needed = false; + cache = 0.0f; + times = std::vector>(); +} + +void GCodeProcessor::TimeMachine::reset() +{ + enabled = false; + acceleration = 0.0f; + max_acceleration = 0.0f; + extrude_factor_override_percentage = 1.0f; + time = 0.0f; + curr.reset(); + prev.reset(); + gcode_time.reset(); + blocks = std::vector(); + g1_times_cache = std::vector(); + std::fill(moves_time.begin(), moves_time.end(), 0.0f); + std::fill(roles_time.begin(), roles_time.end(), 0.0f); + layers_time = std::vector(); +} + +void GCodeProcessor::TimeMachine::simulate_st_synchronize(float additional_time) +{ + if (!enabled) + return; + + time += additional_time; + gcode_time.cache += additional_time; + calculate_time(); +} + +static void planner_forward_pass_kernel(GCodeProcessor::TimeBlock& prev, GCodeProcessor::TimeBlock& curr) +{ + // If the previous block is an acceleration block, but it is not long enough to complete the + // full speed change within the block, we need to adjust the entry speed accordingly. Entry + // speeds have already been reset, maximized, and reverse planned by reverse planner. + // If nominal length is true, max junction speed is guaranteed to be reached. No need to recheck. + if (!prev.flags.nominal_length) { + if (prev.feedrate_profile.entry < curr.feedrate_profile.entry) { + float entry_speed = std::min(curr.feedrate_profile.entry, max_allowable_speed(-prev.acceleration, prev.feedrate_profile.entry, prev.distance)); + + // Check for junction speed change + if (curr.feedrate_profile.entry != entry_speed) { + curr.feedrate_profile.entry = entry_speed; + curr.flags.recalculate = true; + } + } + } +} + +void planner_reverse_pass_kernel(GCodeProcessor::TimeBlock& curr, GCodeProcessor::TimeBlock& next) +{ + // If entry speed is already at the maximum entry speed, no need to recheck. Block is cruising. + // If not, block in state of acceleration or deceleration. Reset entry speed to maximum and + // check for maximum allowable speed reductions to ensure maximum possible planned speed. + if (curr.feedrate_profile.entry != curr.max_entry_speed) { + // If nominal length true, max junction speed is guaranteed to be reached. Only compute + // for max allowable speed if block is decelerating and nominal length is false. + if (!curr.flags.nominal_length && curr.max_entry_speed > next.feedrate_profile.entry) + curr.feedrate_profile.entry = std::min(curr.max_entry_speed, max_allowable_speed(-curr.acceleration, next.feedrate_profile.entry, curr.distance)); + else + curr.feedrate_profile.entry = curr.max_entry_speed; + + curr.flags.recalculate = true; + } +} + +static void recalculate_trapezoids(std::vector& blocks) +{ + GCodeProcessor::TimeBlock* curr = nullptr; + GCodeProcessor::TimeBlock* next = nullptr; + + for (size_t i = 0; i < blocks.size(); ++i) { + GCodeProcessor::TimeBlock& b = blocks[i]; + + curr = next; + next = &b; + + if (curr != nullptr) { + // Recalculate if current block entry or exit junction speed has changed. + if (curr->flags.recalculate || next->flags.recalculate) { + // NOTE: Entry and exit factors always > 0 by all previous logic operations. + GCodeProcessor::TimeBlock block = *curr; + block.feedrate_profile.exit = next->feedrate_profile.entry; + block.calculate_trapezoid(); + curr->trapezoid = block.trapezoid; + curr->flags.recalculate = false; // Reset current only to ensure next trapezoid is computed + } + } + } + + // Last/newest block in buffer. Always recalculated. + if (next != nullptr) { + GCodeProcessor::TimeBlock block = *next; + block.feedrate_profile.exit = next->safe_feedrate; + block.calculate_trapezoid(); + next->trapezoid = block.trapezoid; + next->flags.recalculate = false; + } +} + +void GCodeProcessor::TimeMachine::calculate_time(size_t keep_last_n_blocks) +{ + if (!enabled || blocks.size() < 2) + return; + + assert(keep_last_n_blocks <= blocks.size()); + + // forward_pass + for (size_t i = 0; i + 1 < blocks.size(); ++i) { + planner_forward_pass_kernel(blocks[i], blocks[i + 1]); + } + + // reverse_pass + for (int i = static_cast(blocks.size()) - 1; i > 0; --i) + planner_reverse_pass_kernel(blocks[i - 1], blocks[i]); + + recalculate_trapezoids(blocks); + + size_t n_blocks_process = blocks.size() - keep_last_n_blocks; + for (size_t i = 0; i < n_blocks_process; ++i) { + const TimeBlock& block = blocks[i]; + float block_time = block.time(); + time += block_time; + gcode_time.cache += block_time; + moves_time[static_cast(block.move_type)] += block_time; + roles_time[static_cast(block.role)] += block_time; + if (block.layer_id > 0) { + if (block.layer_id >= layers_time.size()) { + size_t curr_size = layers_time.size(); + layers_time.resize(block.layer_id); + for (size_t i = curr_size; i < layers_time.size(); ++i) { + layers_time[i] = 0.0f; + } + } + layers_time[block.layer_id - 1] += block_time; + } + g1_times_cache.push_back(time); + } + + if (keep_last_n_blocks) + blocks.erase(blocks.begin(), blocks.begin() + n_blocks_process); + else + blocks.clear(); +} + +void GCodeProcessor::TimeProcessor::reset() +{ + extruder_unloaded = true; + export_remaining_time_enabled = false; + machine_envelope_processing_enabled = false; + machine_limits = MachineEnvelopeConfig(); + filament_load_times = std::vector(); + filament_unload_times = std::vector(); + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + machines[i].reset(); + } + machines[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Normal)].enabled = true; +} + +void GCodeProcessor::TimeProcessor::post_process(const std::string& filename) +{ + boost::nowide::ifstream in(filename); + if (!in.good()) + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for reading.\n")); + + // temporary file to contain modified gcode + std::string out_path = filename + ".postprocess"; + FILE* out = boost::nowide::fopen(out_path.c_str(), "wb"); + if (out == nullptr) + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for writing.\n")); + + auto time_in_minutes = [](float time_in_seconds) { + return int(::roundf(time_in_seconds / 60.0f)); + }; + + auto format_line_M73 = [](const std::string& mask, int percent, int time) { + char line_M73[64]; + sprintf(line_M73, mask.c_str(), + std::to_string(percent).c_str(), + std::to_string(time).c_str()); + return std::string(line_M73); + }; + + GCodeReader parser; + std::string gcode_line; + size_t g1_lines_counter = 0; + // keeps track of last exported pair + std::array, static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count)> last_exported; + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + last_exported[i] = { 0, time_in_minutes(machines[i].time) }; + } + + // buffer line to export only when greater than 64K to reduce writing calls + std::string export_line; + + // replace placeholder lines with the proper final value + auto process_placeholders = [&](const std::string& gcode_line) { + // remove trailing '\n' + std::string line = gcode_line.substr(0, gcode_line.length() - 1); + + std::string ret; + + if (line == First_Line_M73_Placeholder_Tag || line == Last_Line_M73_Placeholder_Tag) { + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = machines[i]; + if (machine.enabled) { + ret += format_line_M73(machine.line_m73_mask.c_str(), + (line == First_Line_M73_Placeholder_Tag) ? 0 : 100, + (line == First_Line_M73_Placeholder_Tag) ? time_in_minutes(machines[i].time) : 0); + } + } + } + else if (line == Estimated_Printing_Time_Placeholder_Tag) { + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = machines[i]; + if (machine.enabled) { + char buf[128]; + sprintf(buf, "; estimated printing time (%s mode) = %s\n", + (static_cast(i) == PrintEstimatedTimeStatistics::ETimeMode::Normal) ? "normal" : "silent", + get_time_dhms(machine.time).c_str()); + ret += buf; + } + } + } + + return std::make_pair(!ret.empty(), ret.empty() ? gcode_line : ret); + }; + + // check for temporary lines + auto is_temporary_decoration = [](const std::string& gcode_line) { + // remove trailing '\n' + std::string line = gcode_line.substr(0, gcode_line.length() - 1); + if (line == "; " + Layer_Change_Tag) + return true; + else + return false; + }; + + // add lines M73 to exported gcode + auto process_line_G1 = [&]() { + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = machines[i]; + if (machine.enabled && g1_lines_counter < machine.g1_times_cache.size()) { + float elapsed_time = machine.g1_times_cache[g1_lines_counter]; + std::pair to_export = { int(::roundf(100.0f * elapsed_time / machine.time)), + time_in_minutes(machine.time - elapsed_time) }; + if (last_exported[i] != to_export) { + export_line += format_line_M73(machine.line_m73_mask.c_str(), + to_export.first, to_export.second); + last_exported[i] = to_export; + } + } + } + }; + + // helper function to write to disk + auto write_string = [&](const std::string& str) { + fwrite((const void*)export_line.c_str(), 1, export_line.length(), out); + if (ferror(out)) { + in.close(); + fclose(out); + boost::nowide::remove(out_path.c_str()); + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nIs the disk full?\n")); + } + export_line.clear(); + }; + + while (std::getline(in, gcode_line)) { + if (!in.good()) { + fclose(out); + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nError while reading from file.\n")); + } + + gcode_line += "\n"; + // replace placeholder lines + auto [processed, result] = process_placeholders(gcode_line); + gcode_line = result; + if (!processed) { + // remove temporary lines + if (is_temporary_decoration(gcode_line)) + continue; + + // add lines M73 where needed + parser.parse_line(gcode_line, + [&](GCodeReader& reader, const GCodeReader::GCodeLine& line) { + if (line.cmd_is("G1")) { + process_line_G1(); + ++g1_lines_counter; + } + }); + } + + export_line += gcode_line; + if (export_line.length() > 65535) + write_string(export_line); + } + + if (!export_line.empty()) + write_string(export_line); + + fclose(out); + in.close(); + + if (rename_file(out_path, filename)) + throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + filename + '\n' + + "Is " + out_path + " locked?" + '\n'); +} + +const std::vector> GCodeProcessor::Producers = { + { EProducer::PrusaSlicer, "PrusaSlicer" }, + { EProducer::Cura, "Cura_SteamEngine" }, + { EProducer::Simplify3D, "Simplify3D" }, + { EProducer::CraftWare, "CraftWare" }, + { EProducer::ideaMaker, "ideaMaker" } +}; + +unsigned int GCodeProcessor::s_result_id = 0; + +GCodeProcessor::GCodeProcessor() +{ + reset(); + m_time_processor.machines[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Normal)].line_m73_mask = "M73 P%s R%s\n"; + m_time_processor.machines[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Stealth)].line_m73_mask = "M73 Q%s S%s\n"; +} + +void GCodeProcessor::apply_config(const PrintConfig& config) +{ + m_parser.apply_config(config); + + m_flavor = config.gcode_flavor; + + size_t extruders_count = config.nozzle_diameter.values.size(); + + m_extruder_offsets.resize(extruders_count); + for (size_t i = 0; i < extruders_count; ++i) { + Vec2f offset = config.extruder_offset.get_at(i).cast(); + m_extruder_offsets[i] = { offset(0), offset(1), 0.0f }; + } + + m_extruder_colors.resize(extruders_count); + for (size_t i = 0; i < extruders_count; ++i) { + m_extruder_colors[i] = static_cast(i); + } + + m_filament_diameters.resize(config.filament_diameter.values.size()); + for (size_t i = 0; i < config.filament_diameter.values.size(); ++i) { + m_filament_diameters[i] = static_cast(config.filament_diameter.values[i]); + } + + m_time_processor.machine_limits = reinterpret_cast(config); + // Filament load / unload times are not specific to a firmware flavor. Let anybody use it if they find it useful. + // As of now the fields are shown at the UI dialog in the same combo box as the ramming values, so they + // are considered to be active for the single extruder multi-material printers only. + m_time_processor.filament_load_times.resize(config.filament_load_time.values.size()); + for (size_t i = 0; i < config.filament_load_time.values.size(); ++i) { + m_time_processor.filament_load_times[i] = static_cast(config.filament_load_time.values[i]); + } + m_time_processor.filament_unload_times.resize(config.filament_unload_time.values.size()); + for (size_t i = 0; i < config.filament_unload_time.values.size(); ++i) { + m_time_processor.filament_unload_times[i] = static_cast(config.filament_unload_time.values[i]); + } + + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + float max_acceleration = get_option_value(m_time_processor.machine_limits.machine_max_acceleration_extruding, i); + m_time_processor.machines[i].max_acceleration = max_acceleration; + m_time_processor.machines[i].acceleration = (max_acceleration > 0.0f) ? max_acceleration : DEFAULT_ACCELERATION; + } + + m_time_processor.export_remaining_time_enabled = config.remaining_times.value; +} + +void GCodeProcessor::apply_config(const DynamicPrintConfig& config) +{ + m_parser.apply_config(config); + + const ConfigOptionEnum* gcode_flavor = config.option>("gcode_flavor"); + if (gcode_flavor != nullptr) + m_flavor = gcode_flavor->value; + + const ConfigOptionPoints* bed_shape = config.option("bed_shape"); + if (bed_shape != nullptr) + m_result.bed_shape = bed_shape->values; + + const ConfigOptionString* printer_settings_id = config.option("printer_settings_id"); + if (printer_settings_id != nullptr) + m_result.printer_settings_id = printer_settings_id->value; + + const ConfigOptionFloats* filament_diameters = config.option("filament_diameter"); + if (filament_diameters != nullptr) { + for (double diam : filament_diameters->values) { + m_filament_diameters.push_back(static_cast(diam)); + } + } + + const ConfigOptionPoints* extruder_offset = config.option("extruder_offset"); + if (extruder_offset != nullptr) { + m_extruder_offsets.resize(extruder_offset->values.size()); + for (size_t i = 0; i < extruder_offset->values.size(); ++i) { + Vec2f offset = extruder_offset->values[i].cast(); + m_extruder_offsets[i] = { offset(0), offset(1), 0.0f }; + } + } + + // ensure at least one (default) color is defined + std::string default_color = "#FF8000"; + m_result.extruder_colors = std::vector(1, default_color); + const ConfigOptionStrings* extruder_colour = config.option("extruder_colour"); + if (extruder_colour != nullptr) { + // takes colors from config + m_result.extruder_colors = extruder_colour->values; + // try to replace missing values with filament colors + const ConfigOptionStrings* filament_colour = config.option("filament_colour"); + if (filament_colour != nullptr && filament_colour->values.size() == m_result.extruder_colors.size()) { + for (size_t i = 0; i < m_result.extruder_colors.size(); ++i) { + if (m_result.extruder_colors[i].empty()) + m_result.extruder_colors[i] = filament_colour->values[i]; + } + } + } + + // replace missing values with default + for (size_t i = 0; i < m_result.extruder_colors.size(); ++i) { + if (m_result.extruder_colors[i].empty()) + m_result.extruder_colors[i] = default_color; + } + + m_extruder_colors.resize(m_result.extruder_colors.size()); + for (size_t i = 0; i < m_result.extruder_colors.size(); ++i) { + m_extruder_colors[i] = static_cast(i); + } + + const ConfigOptionFloats* filament_load_time = config.option("filament_load_time"); + if (filament_load_time != nullptr) { + m_time_processor.filament_load_times.resize(filament_load_time->values.size()); + for (size_t i = 0; i < filament_load_time->values.size(); ++i) { + m_time_processor.filament_load_times[i] = static_cast(filament_load_time->values[i]); + } + } + + const ConfigOptionFloats* filament_unload_time = config.option("filament_unload_time"); + if (filament_unload_time != nullptr) { + m_time_processor.filament_unload_times.resize(filament_unload_time->values.size()); + for (size_t i = 0; i < filament_unload_time->values.size(); ++i) { + m_time_processor.filament_unload_times[i] = static_cast(filament_unload_time->values[i]); + } + } + + const ConfigOptionFloats* machine_max_acceleration_x = config.option("machine_max_acceleration_x"); + if (machine_max_acceleration_x != nullptr) + m_time_processor.machine_limits.machine_max_acceleration_x.values = machine_max_acceleration_x->values; + + const ConfigOptionFloats* machine_max_acceleration_y = config.option("machine_max_acceleration_y"); + if (machine_max_acceleration_y != nullptr) + m_time_processor.machine_limits.machine_max_acceleration_y.values = machine_max_acceleration_y->values; + + const ConfigOptionFloats* machine_max_acceleration_z = config.option("machine_max_acceleration_z"); + if (machine_max_acceleration_z != nullptr) + m_time_processor.machine_limits.machine_max_acceleration_z.values = machine_max_acceleration_z->values; + + const ConfigOptionFloats* machine_max_acceleration_e = config.option("machine_max_acceleration_e"); + if (machine_max_acceleration_e != nullptr) + m_time_processor.machine_limits.machine_max_acceleration_e.values = machine_max_acceleration_e->values; + + const ConfigOptionFloats* machine_max_feedrate_x = config.option("machine_max_feedrate_x"); + if (machine_max_feedrate_x != nullptr) + m_time_processor.machine_limits.machine_max_feedrate_x.values = machine_max_feedrate_x->values; + + const ConfigOptionFloats* machine_max_feedrate_y = config.option("machine_max_feedrate_y"); + if (machine_max_feedrate_y != nullptr) + m_time_processor.machine_limits.machine_max_feedrate_y.values = machine_max_feedrate_y->values; + + const ConfigOptionFloats* machine_max_feedrate_z = config.option("machine_max_feedrate_z"); + if (machine_max_feedrate_z != nullptr) + m_time_processor.machine_limits.machine_max_feedrate_z.values = machine_max_feedrate_z->values; + + const ConfigOptionFloats* machine_max_feedrate_e = config.option("machine_max_feedrate_e"); + if (machine_max_feedrate_e != nullptr) + m_time_processor.machine_limits.machine_max_feedrate_e.values = machine_max_feedrate_e->values; + + const ConfigOptionFloats* machine_max_jerk_x = config.option("machine_max_jerk_x"); + if (machine_max_jerk_x != nullptr) + m_time_processor.machine_limits.machine_max_jerk_x.values = machine_max_jerk_x->values; + + const ConfigOptionFloats* machine_max_jerk_y = config.option("machine_max_jerk_y"); + if (machine_max_jerk_y != nullptr) + m_time_processor.machine_limits.machine_max_jerk_y.values = machine_max_jerk_y->values; + + const ConfigOptionFloats* machine_max_jerk_z = config.option("machine_max_jerkz"); + if (machine_max_jerk_z != nullptr) + m_time_processor.machine_limits.machine_max_jerk_z.values = machine_max_jerk_z->values; + + const ConfigOptionFloats* machine_max_jerk_e = config.option("machine_max_jerk_e"); + if (machine_max_jerk_e != nullptr) + m_time_processor.machine_limits.machine_max_jerk_e.values = machine_max_jerk_e->values; + + const ConfigOptionFloats* machine_max_acceleration_extruding = config.option("machine_max_acceleration_extruding"); + if (machine_max_acceleration_extruding != nullptr) + m_time_processor.machine_limits.machine_max_acceleration_extruding.values = machine_max_acceleration_extruding->values; + + const ConfigOptionFloats* machine_max_acceleration_retracting = config.option("machine_max_acceleration_retracting"); + if (machine_max_acceleration_retracting != nullptr) + m_time_processor.machine_limits.machine_max_acceleration_retracting.values = machine_max_acceleration_retracting->values; + + const ConfigOptionFloats* machine_min_extruding_rate = config.option("machine_min_extruding_rate"); + if (machine_min_extruding_rate != nullptr) + m_time_processor.machine_limits.machine_min_extruding_rate.values = machine_min_extruding_rate->values; + + const ConfigOptionFloats* machine_min_travel_rate = config.option("machine_min_travel_rate"); + if (machine_min_travel_rate != nullptr) + m_time_processor.machine_limits.machine_min_travel_rate.values = machine_min_travel_rate->values; + + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + float max_acceleration = get_option_value(m_time_processor.machine_limits.machine_max_acceleration_extruding, i); + m_time_processor.machines[i].max_acceleration = max_acceleration; + m_time_processor.machines[i].acceleration = (max_acceleration > 0.0f) ? max_acceleration : DEFAULT_ACCELERATION; + } +} + +void GCodeProcessor::enable_stealth_time_estimator(bool enabled) +{ + m_time_processor.machines[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Stealth)].enabled = enabled; +} + +void GCodeProcessor::reset() +{ + static const size_t Min_Extruder_Count = 5; + + m_units = EUnits::Millimeters; + m_global_positioning_type = EPositioningType::Absolute; + m_e_local_positioning_type = EPositioningType::Absolute; + m_extruder_offsets = std::vector(Min_Extruder_Count, Vec3f::Zero()); + m_flavor = gcfRepRap; + + m_start_position = { 0.0f, 0.0f, 0.0f, 0.0f }; + m_end_position = { 0.0f, 0.0f, 0.0f, 0.0f }; + m_origin = { 0.0f, 0.0f, 0.0f, 0.0f }; + m_cached_position.reset(); + + m_feedrate = 0.0f; + m_width = 0.0f; + m_height = 0.0f; + m_mm3_per_mm = 0.0f; + m_fan_speed = 0.0f; + + m_extrusion_role = erNone; + m_extruder_id = 0; + m_extruder_colors.resize(Min_Extruder_Count); + for (size_t i = 0; i < Min_Extruder_Count; ++i) { + m_extruder_colors[i] = static_cast(i); + } + + m_filament_diameters = std::vector(Min_Extruder_Count, 1.75f); + m_extruded_last_z = 0.0f; + m_layer_id = 0; + m_cp_color.reset(); + + m_producer = EProducer::Unknown; + m_producers_enabled = false; + + m_time_processor.reset(); + + m_result.reset(); + m_result.id = ++s_result_id; + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + m_mm3_per_mm_compare.reset(); + m_height_compare.reset(); + m_width_compare.reset(); +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING +} + +void GCodeProcessor::process_file(const std::string& filename, std::function cancel_callback) +{ + auto last_cancel_callback_time = std::chrono::high_resolution_clock::now(); + +#if ENABLE_GCODE_VIEWER_STATISTICS + auto start_time = std::chrono::high_resolution_clock::now(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + // pre-processing + // parse the gcode file to detect its producer + if (m_producers_enabled) { + m_parser.parse_file(filename, [this](GCodeReader& reader, const GCodeReader::GCodeLine& line) { + std::string cmd = line.cmd(); + if (cmd.length() == 0) { + std::string comment = line.comment(); + if (comment.length() > 1 && detect_producer(comment)) + m_parser.quit_parsing_file(); + } + }); + + // if the gcode was produced by PrusaSlicer, + // extract the config from it + if (m_producer == EProducer::PrusaSlicer) { + DynamicPrintConfig config; + config.apply(FullPrintConfig::defaults()); + config.load_from_gcode_file(filename); + apply_config(config); + } + } + + // process gcode + m_result.id = ++s_result_id; + // 1st move must be a dummy move + m_result.moves.emplace_back(MoveVertex()); + m_parser.parse_file(filename, [this, cancel_callback, &last_cancel_callback_time](GCodeReader& reader, const GCodeReader::GCodeLine& line) { + if (cancel_callback != nullptr) { + // call the cancel callback every 100 ms + auto curr_time = std::chrono::high_resolution_clock::now(); + if (std::chrono::duration_cast(curr_time - last_cancel_callback_time).count() > 100) { + cancel_callback(); + last_cancel_callback_time = curr_time; + } + } + process_gcode_line(line); + }); + + // process the time blocks + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + TimeMachine& machine = m_time_processor.machines[i]; + TimeMachine::CustomGCodeTime& gcode_time = machine.gcode_time; + machine.calculate_time(); + if (gcode_time.needed && gcode_time.cache != 0.0f) + gcode_time.times.push_back({ CustomGCode::ColorChange, gcode_time.cache }); + } + + update_estimated_times_stats(); + + // post-process to add M73 lines into the gcode + if (m_time_processor.export_remaining_time_enabled) + m_time_processor.post_process(filename); + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + std::cout << "\n"; + m_mm3_per_mm_compare.output(); + m_height_compare.output(); + m_width_compare.output(); +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + +#if ENABLE_GCODE_VIEWER_STATISTICS + m_result.time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS +} + +float GCodeProcessor::get_time(PrintEstimatedTimeStatistics::ETimeMode mode) const +{ + return (mode < PrintEstimatedTimeStatistics::ETimeMode::Count) ? m_time_processor.machines[static_cast(mode)].time : 0.0f; +} + +std::string GCodeProcessor::get_time_dhm(PrintEstimatedTimeStatistics::ETimeMode mode) const +{ + return (mode < PrintEstimatedTimeStatistics::ETimeMode::Count) ? short_time(get_time_dhms(m_time_processor.machines[static_cast(mode)].time)) : std::string("N/A"); +} + +std::vector>> GCodeProcessor::get_custom_gcode_times(PrintEstimatedTimeStatistics::ETimeMode mode, bool include_remaining) const +{ + std::vector>> ret; + if (mode < PrintEstimatedTimeStatistics::ETimeMode::Count) { + const TimeMachine& machine = m_time_processor.machines[static_cast(mode)]; + float total_time = 0.0f; + for (const auto& [type, time] : machine.gcode_time.times) { + float remaining = include_remaining ? machine.time - total_time : 0.0f; + ret.push_back({ type, { time, remaining } }); + total_time += time; + } + } + return ret; +} + +std::vector> GCodeProcessor::get_moves_time(PrintEstimatedTimeStatistics::ETimeMode mode) const +{ + std::vector> ret; + if (mode < PrintEstimatedTimeStatistics::ETimeMode::Count) { + for (size_t i = 0; i < m_time_processor.machines[static_cast(mode)].moves_time.size(); ++i) { + float time = m_time_processor.machines[static_cast(mode)].moves_time[i]; + if (time > 0.0f) + ret.push_back({ static_cast(i), time }); + } + } + return ret; +} + +std::vector> GCodeProcessor::get_roles_time(PrintEstimatedTimeStatistics::ETimeMode mode) const +{ + std::vector> ret; + if (mode < PrintEstimatedTimeStatistics::ETimeMode::Count) { + for (size_t i = 0; i < m_time_processor.machines[static_cast(mode)].roles_time.size(); ++i) { + float time = m_time_processor.machines[static_cast(mode)].roles_time[i]; + if (time > 0.0f) + ret.push_back({ static_cast(i), time }); + } + } + return ret; +} + +std::vector GCodeProcessor::get_layers_time(PrintEstimatedTimeStatistics::ETimeMode mode) const +{ + return (mode < PrintEstimatedTimeStatistics::ETimeMode::Count) ? + m_time_processor.machines[static_cast(mode)].layers_time : + std::vector(); +} + +void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line) +{ +/* std::cout << line.raw() << std::endl; */ + + // update start position + m_start_position = m_end_position; + + std::string cmd = line.cmd(); + if (cmd.length() > 1) { + // process command lines + switch (::toupper(cmd[0])) + { + case 'G': + { + switch (::atoi(&cmd[1])) + { + case 0: { process_G0(line); break; } // Move + case 1: { process_G1(line); break; } // Move + case 10: { process_G10(line); break; } // Retract + case 11: { process_G11(line); break; } // Unretract + case 20: { process_G20(line); break; } // Set Units to Inches + case 21: { process_G21(line); break; } // Set Units to Millimeters + case 22: { process_G22(line); break; } // Firmware controlled retract + case 23: { process_G23(line); break; } // Firmware controlled unretract + case 90: { process_G90(line); break; } // Set to Absolute Positioning + case 91: { process_G91(line); break; } // Set to Relative Positioning + case 92: { process_G92(line); break; } // Set Position + default: { break; } + } + break; + } + case 'M': + { + switch (::atoi(&cmd[1])) + { + case 1: { process_M1(line); break; } // Sleep or Conditional stop + case 82: { process_M82(line); break; } // Set extruder to absolute mode + case 83: { process_M83(line); break; } // Set extruder to relative mode + case 106: { process_M106(line); break; } // Set fan speed + case 107: { process_M107(line); break; } // Disable fan + case 108: { process_M108(line); break; } // Set tool (Sailfish) + case 132: { process_M132(line); break; } // Recall stored home offsets + case 135: { process_M135(line); break; } // Set tool (MakerWare) + case 201: { process_M201(line); break; } // Set max printing acceleration + case 203: { process_M203(line); break; } // Set maximum feedrate + case 204: { process_M204(line); break; } // Set default acceleration + case 205: { process_M205(line); break; } // Advanced settings + case 221: { process_M221(line); break; } // Set extrude factor override percentage + case 401: { process_M401(line); break; } // Repetier: Store x, y and z position + case 402: { process_M402(line); break; } // Repetier: Go to stored position + case 566: { process_M566(line); break; } // Set allowable instantaneous speed change + case 702: { process_M702(line); break; } // Unload the current filament into the MK3 MMU2 unit at the end of print. + default: { break; } + } + break; + } + case 'T': + { + process_T(line); // Select Tool + break; + } + default: { break; } + } + } + else { + std::string comment = line.comment(); + if (comment.length() > 1) + // process tags embedded into comments + process_tags(comment); + } +} + +void GCodeProcessor::process_tags(const std::string& comment) +{ + // producers tags + if (m_producers_enabled) { + if (m_producer != EProducer::Unknown) { + if (process_producers_tags(comment)) + return; + } + } + + // extrusion role tag + size_t pos = comment.find(Extrusion_Role_Tag); + if (pos != comment.npos) { + m_extrusion_role = ExtrusionEntity::string_to_role(comment.substr(pos + Extrusion_Role_Tag.length())); + return; + } + + if (!m_producers_enabled || m_producer == EProducer::PrusaSlicer) { + // height tag + pos = comment.find(Height_Tag); + if (pos != comment.npos) { + try { + m_height = std::stof(comment.substr(pos + Height_Tag.length())); + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Height (" << comment << ")."; + } + return; + } + } + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + // width tag + pos = comment.find(Width_Tag); + if (pos != comment.npos) { + try { + m_width_compare.last_tag_value = std::stof(comment.substr(pos + Width_Tag.length())); + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Width (" << comment << ")."; + } + return; + } +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + // color change tag + pos = comment.find(Color_Change_Tag); + if (pos != comment.npos) { + pos = comment.find_last_of(",T"); + try { + unsigned char extruder_id = (pos == comment.npos) ? 0 : static_cast(std::stoi(comment.substr(pos + 1))); + + m_extruder_colors[extruder_id] = static_cast(m_extruder_offsets.size()) + m_cp_color.counter; // color_change position in list of color for preview + ++m_cp_color.counter; + if (m_cp_color.counter == UCHAR_MAX) + m_cp_color.counter = 0; + + if (m_extruder_id == extruder_id) { + m_cp_color.current = m_extruder_colors[extruder_id]; + store_move_vertex(EMoveType::Color_change); + } + + process_custom_gcode_time(CustomGCode::ColorChange); + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Color_Change (" << comment << ")."; + } + + return; + } + + // pause print tag + pos = comment.find(Pause_Print_Tag); + if (pos != comment.npos) { + store_move_vertex(EMoveType::Pause_Print); + process_custom_gcode_time(CustomGCode::PausePrint); + return; + } + + // custom code tag + pos = comment.find(Custom_Code_Tag); + if (pos != comment.npos) { + store_move_vertex(EMoveType::Custom_GCode); + return; + } + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + // mm3_per_mm print tag + pos = comment.find(Mm3_Per_Mm_Tag); + if (pos != comment.npos) { + try { + m_mm3_per_mm_compare.last_tag_value = std::stof(comment.substr(pos + Mm3_Per_Mm_Tag.length())); + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Mm3_Per_Mm (" << comment << ")."; + } + return; + } +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + // layer change tag + pos = comment.find(Layer_Change_Tag); + if (pos != comment.npos) { + ++m_layer_id; + return; + } +} + +bool GCodeProcessor::process_producers_tags(const std::string& comment) +{ + switch (m_producer) + { + case EProducer::PrusaSlicer: { return process_prusaslicer_tags(comment); } + case EProducer::Cura: { return process_cura_tags(comment); } + case EProducer::Simplify3D: { return process_simplify3d_tags(comment); } + case EProducer::CraftWare: { return process_craftware_tags(comment); } + case EProducer::ideaMaker: { return process_ideamaker_tags(comment); } + default: { return false; } + } +} + +bool GCodeProcessor::process_prusaslicer_tags(const std::string& comment) +{ + return false; +} + +bool GCodeProcessor::process_cura_tags(const std::string& comment) +{ + // TYPE -> extrusion role + std::string tag = "TYPE:"; + size_t pos = comment.find(tag); + if (pos != comment.npos) { + std::string type = comment.substr(pos + tag.length()); + if (type == "SKIRT") + m_extrusion_role = erSkirt; + else if (type == "WALL-OUTER") + m_extrusion_role = erExternalPerimeter; + else if (type == "WALL-INNER") + m_extrusion_role = erPerimeter; + else if (type == "SKIN") + m_extrusion_role = erSolidInfill; + else if (type == "FILL") + m_extrusion_role = erInternalInfill; + else if (type == "SUPPORT") + m_extrusion_role = erSupportMaterial; + else if (type == "SUPPORT-INTERFACE") + m_extrusion_role = erSupportMaterialInterface; + else if (type == "PRIME-TOWER") + m_extrusion_role = erWipeTower; + else { + m_extrusion_role = erNone; + BOOST_LOG_TRIVIAL(warning) << "GCodeProcessor found unknown extrusion role: " << type; + } + + return true; + } + + // flavor + tag = "FLAVOR:"; + pos = comment.find(tag); + if (pos != comment.npos) { + std::string flavor = comment.substr(pos + tag.length()); + if (flavor == "BFB") + m_flavor = gcfMarlin; // << ??????????????????????? + else if (flavor == "Mach3") + m_flavor = gcfMach3; + else if (flavor == "Makerbot") + m_flavor = gcfMakerWare; + else if (flavor == "UltiGCode") + m_flavor = gcfMarlin; // << ??????????????????????? + else if (flavor == "Marlin(Volumetric)") + m_flavor = gcfMarlin; // << ??????????????????????? + else if (flavor == "Griffin") + m_flavor = gcfMarlin; // << ??????????????????????? + else if (flavor == "Repetier") + m_flavor = gcfRepetier; + else if (flavor == "RepRap") + m_flavor = gcfRepRap; + else if (flavor == "Marlin") + m_flavor = gcfMarlin; + else + BOOST_LOG_TRIVIAL(warning) << "GCodeProcessor found unknown flavor: " << flavor; + + return true; + } + + return false; +} + +bool GCodeProcessor::process_simplify3d_tags(const std::string& comment) +{ + // extrusion roles + + // ; skirt + size_t pos = comment.find(" skirt"); + if (pos == 0) { + m_extrusion_role = erSkirt; + return true; + } + + // ; outer perimeter + pos = comment.find(" outer perimeter"); + if (pos == 0) { + m_extrusion_role = erExternalPerimeter; + return true; + } + + // ; inner perimeter + pos = comment.find(" inner perimeter"); + if (pos == 0) { + m_extrusion_role = erPerimeter; + return true; + } + + // ; gap fill + pos = comment.find(" gap fill"); + if (pos == 0) { + m_extrusion_role = erGapFill; + return true; + } + + // ; infill + pos = comment.find(" infill"); + if (pos == 0) { + m_extrusion_role = erInternalInfill; + return true; + } + + // ; solid layer + pos = comment.find(" solid layer"); + if (pos == 0) { + m_extrusion_role = erNone; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + return true; + } + + // ; bridge + pos = comment.find(" bridge"); + if (pos == 0) { + m_extrusion_role = erBridgeInfill; + return true; + } + + // ; support + pos = comment.find(" support"); + if (pos == 0) { + m_extrusion_role = erSupportMaterial; + return true; + } + + // ; prime pillar + pos = comment.find(" prime pillar"); + if (pos == 0) { + m_extrusion_role = erWipeTower; + return true; + } + + // ; ooze shield + pos = comment.find(" ooze shield"); + if (pos == 0) { + m_extrusion_role = erNone; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + return true; + } + + // ; raft + pos = comment.find(" raft"); + if (pos == 0) { + m_extrusion_role = erSkirt; + return true; + } + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + // geometry + + // ; tool + std::string tag = " tool"; + pos = comment.find(tag); + if (pos == 0) { + std::string data = comment.substr(pos + tag.length()); + std::string h_tag = "H"; + size_t h_start = data.find(h_tag); + size_t h_end = data.find_first_of(' ', h_start); + std::string w_tag = "W"; + size_t w_start = data.find(w_tag); + size_t w_end = data.find_first_of(' ', w_start); + if (h_start != data.npos) { + try { + m_height_compare.last_tag_value = std::stof(data.substr(h_start + 1, (h_end != data.npos) ? h_end - h_start - 1 : h_end)); + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Height (" << comment << ")."; + } + } + if (w_start != data.npos) { + try { + m_width_compare.last_tag_value = std::stof(data.substr(w_start + 1, (w_end != data.npos) ? w_end - w_start - 1 : w_end)); + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Width (" << comment << ")."; + } + } + + return true; + } +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + return false; +} + +bool GCodeProcessor::process_craftware_tags(const std::string& comment) +{ + // segType -> extrusion role + std::string tag = "segType:"; + size_t pos = comment.find(tag); + if (pos != comment.npos) { + std::string type = comment.substr(pos + tag.length()); + if (type == "Skirt") + m_extrusion_role = erSkirt; + else if (type == "Perimeter") + m_extrusion_role = erExternalPerimeter; + else if (type == "HShell") + m_extrusion_role = erNone; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + else if (type == "InnerHair") + m_extrusion_role = erNone; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + else if (type == "Loop") + m_extrusion_role = erNone; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + else if (type == "Infill") + m_extrusion_role = erInternalInfill; + else if (type == "Raft") + m_extrusion_role = erSkirt; + else if (type == "Support") + m_extrusion_role = erSupportMaterial; + else if (type == "SupportTouch") + m_extrusion_role = erSupportMaterial; + else if (type == "SoftSupport") + m_extrusion_role = erSupportMaterialInterface; + else if (type == "Pillar") + m_extrusion_role = erWipeTower; + else { + m_extrusion_role = erNone; + BOOST_LOG_TRIVIAL(warning) << "GCodeProcessor found unknown extrusion role: " << type; + } + + return true; + } + + return false; +} + +bool GCodeProcessor::process_ideamaker_tags(const std::string& comment) +{ + // TYPE -> extrusion role + std::string tag = "TYPE:"; + size_t pos = comment.find(tag); + if (pos != comment.npos) { + std::string type = comment.substr(pos + tag.length()); + if (type == "RAFT") + m_extrusion_role = erSkirt; + else if (type == "WALL-OUTER") + m_extrusion_role = erExternalPerimeter; + else if (type == "WALL-INNER") + m_extrusion_role = erPerimeter; + else if (type == "SOLID-FILL") + m_extrusion_role = erSolidInfill; + else if (type == "FILL") + m_extrusion_role = erInternalInfill; + else if (type == "BRIDGE") + m_extrusion_role = erBridgeInfill; + else if (type == "SUPPORT") + m_extrusion_role = erSupportMaterial; + else { + m_extrusion_role = erNone; + BOOST_LOG_TRIVIAL(warning) << "GCodeProcessor found unknown extrusion role: " << type; + } + return true; + } + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + // geometry + + // width + tag = "WIDTH:"; + pos = comment.find(tag); + if (pos != comment.npos) { + try { + m_width_compare.last_tag_value = std::stof(comment.substr(pos + tag.length())); + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Width (" << comment << ")."; + } + return true; + } + + // height + tag = "HEIGHT:"; + pos = comment.find(tag); + if (pos != comment.npos) { + try { + m_height_compare.last_tag_value = std::stof(comment.substr(pos + tag.length())); + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Height (" << comment << ")."; + } + return true; + } +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + return false; +} + +bool GCodeProcessor::detect_producer(const std::string& comment) +{ + for (const auto& [id, search_string] : Producers) { + size_t pos = comment.find(search_string); + if (pos != comment.npos) { + m_producer = id; + BOOST_LOG_TRIVIAL(info) << "Detected gcode producer: " << search_string; + return true; + } + } + return false; +} + +void GCodeProcessor::process_G0(const GCodeReader::GCodeLine& line) +{ + process_G1(line); +} + +void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line) +{ + auto absolute_position = [this](Axis axis, const GCodeReader::GCodeLine& lineG1) + { + bool is_relative = (m_global_positioning_type == EPositioningType::Relative); + if (axis == E) + is_relative |= (m_e_local_positioning_type == EPositioningType::Relative); + + if (lineG1.has(Slic3r::Axis(axis))) { + float lengthsScaleFactor = (m_units == EUnits::Inches) ? INCHES_TO_MM : 1.0f; + float ret = lineG1.value(Slic3r::Axis(axis)) * lengthsScaleFactor; + return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret; + } + else + return m_start_position[axis]; + }; + + auto move_type = [this](const AxisCoords& delta_pos) { + EMoveType type = EMoveType::Noop; + + if (delta_pos[E] < 0.0f) { + type = (delta_pos[X] != 0.0f || delta_pos[Y] != 0.0f || delta_pos[Z] != 0.0f) ? EMoveType::Travel : EMoveType::Retract; + } + else if (delta_pos[E] > 0.0f) { + if (delta_pos[X] == 0.0f && delta_pos[Y] == 0.0f && delta_pos[Z] == 0.0f) + type = EMoveType::Unretract; + else if (delta_pos[X] != 0.0f || delta_pos[Y] != 0.0f) + type = EMoveType::Extrude; + } + else if (delta_pos[X] != 0.0f || delta_pos[Y] != 0.0f || delta_pos[Z] != 0.0f) + type = EMoveType::Travel; + + return type; + }; + + // enable processing of lines M201/M203/M204/M205 + m_time_processor.machine_envelope_processing_enabled = true; + + // updates axes positions from line + for (unsigned char a = X; a <= E; ++a) { + m_end_position[a] = absolute_position((Axis)a, line); + } + + // updates feedrate from line, if present + if (line.has_f()) + m_feedrate = line.f() * MMMIN_TO_MMSEC; + + // calculates movement deltas + float max_abs_delta = 0.0f; + AxisCoords delta_pos; + for (unsigned char a = X; a <= E; ++a) { + delta_pos[a] = m_end_position[a] - m_start_position[a]; + max_abs_delta = std::max(max_abs_delta, std::abs(delta_pos[a])); + } + + // no displacement, return + if (max_abs_delta == 0.0f) + return; + + EMoveType type = move_type(delta_pos); + if (type == EMoveType::Extrude && m_end_position[Z] == 0.0f) + type = EMoveType::Travel; + + if (type == EMoveType::Extrude) { + float delta_xyz = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z])); + float filament_diameter = (static_cast(m_extruder_id) < m_filament_diameters.size()) ? m_filament_diameters[m_extruder_id] : m_filament_diameters.back(); + float filament_radius = 0.5f * filament_diameter; + float area_filament_cross_section = static_cast(M_PI) * sqr(filament_radius); + float volume_extruded_filament = area_filament_cross_section * delta_pos[E]; + float area_toolpath_cross_section = volume_extruded_filament / delta_xyz; + + // volume extruded filament / tool displacement = area toolpath cross section + m_mm3_per_mm = area_toolpath_cross_section; +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + m_mm3_per_mm_compare.update(area_toolpath_cross_section, m_extrusion_role); +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + if ((m_producers_enabled && m_producer != EProducer::PrusaSlicer) || m_height == 0.0f) { + if (m_end_position[Z] > m_extruded_last_z + EPSILON) { + m_height = m_end_position[Z] - m_extruded_last_z; +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + m_height_compare.update(m_height, m_extrusion_role); +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + m_extruded_last_z = m_end_position[Z]; + } + } + + if (m_extrusion_role == erExternalPerimeter) + // cross section: rectangle + m_width = delta_pos[E] * static_cast(M_PI * sqr(1.05 * filament_radius)) / (delta_xyz * m_height); + else if (m_extrusion_role == erBridgeInfill || m_extrusion_role == erNone) + // cross section: circle + m_width = static_cast(m_filament_diameters[m_extruder_id]) * std::sqrt(delta_pos[E] / delta_xyz); + else + // cross section: rectangle + 2 semicircles + m_width = delta_pos[E] * static_cast(M_PI * sqr(filament_radius)) / (delta_xyz * m_height) + static_cast(1.0 - 0.25 * M_PI) * m_height; + + // clamp width to avoid artifacts which may arise from wrong values of m_height + m_width = std::min(m_width, 4.0f * m_height); + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + m_width_compare.update(m_width, m_extrusion_role); +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + } + + if (type == EMoveType::Extrude && (m_extrusion_role == erCustom || m_width == 0.0f || m_height == 0.0f)) + type = EMoveType::Travel; + + // time estimate section + auto move_length = [](const AxisCoords& delta_pos) { + float sq_xyz_length = sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z]); + return (sq_xyz_length > 0.0f) ? std::sqrt(sq_xyz_length) : std::abs(delta_pos[E]); + }; + + auto is_extrusion_only_move = [](const AxisCoords& delta_pos) { + return delta_pos[X] == 0.0f && delta_pos[Y] == 0.0f && delta_pos[Z] == 0.0f && delta_pos[E] != 0.0f; + }; + + float distance = move_length(delta_pos); + assert(distance != 0.0f); + float inv_distance = 1.0f / distance; + + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + TimeMachine& machine = m_time_processor.machines[i]; + if (!machine.enabled) + continue; + + TimeMachine::State& curr = machine.curr; + TimeMachine::State& prev = machine.prev; + std::vector& blocks = machine.blocks; + + curr.feedrate = (delta_pos[E] == 0.0f) ? + minimum_travel_feedrate(static_cast(i), m_feedrate) : + minimum_feedrate(static_cast(i), m_feedrate); + + TimeBlock block; + block.move_type = type; + block.role = m_extrusion_role; + block.distance = distance; + block.layer_id = m_layer_id; + + // calculates block cruise feedrate + float min_feedrate_factor = 1.0f; + for (unsigned char a = X; a <= E; ++a) { + curr.axis_feedrate[a] = curr.feedrate * delta_pos[a] * inv_distance; + if (a == E) + curr.axis_feedrate[a] *= machine.extrude_factor_override_percentage; + + curr.abs_axis_feedrate[a] = std::abs(curr.axis_feedrate[a]); + if (curr.abs_axis_feedrate[a] != 0.0f) { + float axis_max_feedrate = get_axis_max_feedrate(static_cast(i), static_cast(a)); + if (axis_max_feedrate != 0.0f) + min_feedrate_factor = std::min(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]); + } + } + + block.feedrate_profile.cruise = min_feedrate_factor * curr.feedrate; + + if (min_feedrate_factor < 1.0f) { + for (unsigned char a = X; a <= E; ++a) { + curr.axis_feedrate[a] *= min_feedrate_factor; + curr.abs_axis_feedrate[a] *= min_feedrate_factor; + } + } + + // calculates block acceleration + float acceleration = is_extrusion_only_move(delta_pos) ? + get_retract_acceleration(static_cast(i)) : + get_acceleration(static_cast(i)); + + for (unsigned char a = X; a <= E; ++a) { + float axis_max_acceleration = get_axis_max_acceleration(static_cast(i), static_cast(a)); + if (acceleration * std::abs(delta_pos[a]) * inv_distance > axis_max_acceleration) + acceleration = axis_max_acceleration; + } + + block.acceleration = acceleration; + + // calculates block exit feedrate + curr.safe_feedrate = block.feedrate_profile.cruise; + + for (unsigned char a = X; a <= E; ++a) { + float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); + if (curr.abs_axis_feedrate[a] > axis_max_jerk) + curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + } + + block.feedrate_profile.exit = curr.safe_feedrate; + + static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; + + // calculates block entry feedrate + float vmax_junction = curr.safe_feedrate; + if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { + bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; + float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); + // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. + vmax_junction = prev_speed_larger ? block.feedrate_profile.cruise : prev.feedrate; + + float v_factor = 1.0f; + bool limited = false; + + for (unsigned char a = X; a <= E; ++a) { + // Limit an axis. We have to differentiate coasting from the reversal of an axis movement, or a full stop. + float v_exit = prev.axis_feedrate[a]; + float v_entry = curr.axis_feedrate[a]; + + if (prev_speed_larger) + v_exit *= smaller_speed_factor; + + if (limited) { + v_exit *= v_factor; + v_entry *= v_factor; + } + + // Calculate the jerk depending on whether the axis is coasting in the same direction or reversing a direction. + float jerk = + (v_exit > v_entry) ? + (((v_entry > 0.0f) || (v_exit < 0.0f)) ? + // coasting + (v_exit - v_entry) : + // axis reversal + std::max(v_exit, -v_entry)) : + // v_exit <= v_entry + (((v_entry < 0.0f) || (v_exit > 0.0f)) ? + // coasting + (v_entry - v_exit) : + // axis reversal + std::max(-v_exit, v_entry)); + + float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); + if (jerk > axis_max_jerk) { + v_factor *= axis_max_jerk / jerk; + limited = true; + } + } + + if (limited) + vmax_junction *= v_factor; + + // Now the transition velocity is known, which maximizes the shared exit / entry velocity while + // respecting the jerk factors, it may be possible, that applying separate safe exit / entry velocities will achieve faster prints. + float vmax_junction_threshold = vmax_junction * 0.99f; + + // Not coasting. The machine will stop and start the movements anyway, better to start the segment from start. + if ((prev.safe_feedrate > vmax_junction_threshold) && (curr.safe_feedrate > vmax_junction_threshold)) + vmax_junction = curr.safe_feedrate; + } + + float v_allowable = max_allowable_speed(-acceleration, curr.safe_feedrate, block.distance); + block.feedrate_profile.entry = std::min(vmax_junction, v_allowable); + + block.max_entry_speed = vmax_junction; + block.flags.nominal_length = (block.feedrate_profile.cruise <= v_allowable); + block.flags.recalculate = true; + block.safe_feedrate = curr.safe_feedrate; + + // calculates block trapezoid + block.calculate_trapezoid(); + + // updates previous + prev = curr; + + blocks.push_back(block); + + if (blocks.size() > TimeProcessor::Planner::refresh_threshold) + machine.calculate_time(TimeProcessor::Planner::queue_size); + } + + // store move + store_move_vertex(type); +} + +void GCodeProcessor::process_G10(const GCodeReader::GCodeLine& line) +{ + // stores retract move + store_move_vertex(EMoveType::Retract); +} + +void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line) +{ + // stores unretract move + store_move_vertex(EMoveType::Unretract); +} + +void GCodeProcessor::process_G20(const GCodeReader::GCodeLine& line) +{ + m_units = EUnits::Inches; +} + +void GCodeProcessor::process_G21(const GCodeReader::GCodeLine& line) +{ + m_units = EUnits::Millimeters; +} + +void GCodeProcessor::process_G22(const GCodeReader::GCodeLine& line) +{ + // stores retract move + store_move_vertex(EMoveType::Retract); +} + +void GCodeProcessor::process_G23(const GCodeReader::GCodeLine& line) +{ + // stores unretract move + store_move_vertex(EMoveType::Unretract); +} + +void GCodeProcessor::process_G90(const GCodeReader::GCodeLine& line) +{ + m_global_positioning_type = EPositioningType::Absolute; +} + +void GCodeProcessor::process_G91(const GCodeReader::GCodeLine& line) +{ + m_global_positioning_type = EPositioningType::Relative; +} + +void GCodeProcessor::process_G92(const GCodeReader::GCodeLine& line) +{ + float lengths_scale_factor = (m_units == EUnits::Inches) ? INCHES_TO_MM : 1.0f; + bool any_found = false; + + if (line.has_x()) { + m_origin[X] = m_end_position[X] - line.x() * lengths_scale_factor; + any_found = true; + } + + if (line.has_y()) { + m_origin[Y] = m_end_position[Y] - line.y() * lengths_scale_factor; + any_found = true; + } + + if (line.has_z()) { + m_origin[Z] = m_end_position[Z] - line.z() * lengths_scale_factor; + any_found = true; + } + + if (line.has_e()) { + // extruder coordinate can grow to the point where its float representation does not allow for proper addition with small increments, + // we set the value taken from the G92 line as the new current position for it + m_end_position[E] = line.e() * lengths_scale_factor; + any_found = true; + } + else + simulate_st_synchronize(); + + if (!any_found && !line.has_unknown_axis()) { + // The G92 may be called for axes that PrusaSlicer does not recognize, for example see GH issue #3510, + // where G92 A0 B0 is called although the extruder axis is till E. + for (unsigned char a = X; a <= E; ++a) { + m_origin[a] = m_end_position[a]; + } + } +} + +void GCodeProcessor::process_M1(const GCodeReader::GCodeLine& line) +{ + simulate_st_synchronize(); +} + +void GCodeProcessor::process_M82(const GCodeReader::GCodeLine& line) +{ + m_e_local_positioning_type = EPositioningType::Absolute; +} + +void GCodeProcessor::process_M83(const GCodeReader::GCodeLine& line) +{ + m_e_local_positioning_type = EPositioningType::Relative; +} + +void GCodeProcessor::process_M106(const GCodeReader::GCodeLine& line) +{ + if (!line.has('P')) { + // The absence of P means the print cooling fan, so ignore anything else. + float new_fan_speed; + if (line.has_value('S', new_fan_speed)) + m_fan_speed = (100.0f / 255.0f) * new_fan_speed; + else + m_fan_speed = 100.0f; + } +} + +void GCodeProcessor::process_M107(const GCodeReader::GCodeLine& line) +{ + m_fan_speed = 0.0f; +} + +void GCodeProcessor::process_M108(const GCodeReader::GCodeLine& line) +{ + // These M-codes are used by Sailfish to change active tool. + // They have to be processed otherwise toolchanges will be unrecognised + // by the analyzer - see https://github.com/prusa3d/PrusaSlicer/issues/2566 + + if (m_flavor != gcfSailfish) + return; + + std::string cmd = line.raw(); + size_t pos = cmd.find("T"); + if (pos != std::string::npos) + process_T(cmd.substr(pos)); +} + +void GCodeProcessor::process_M132(const GCodeReader::GCodeLine& line) +{ + // This command is used by Makerbot to load the current home position from EEPROM + // see: https://github.com/makerbot/s3g/blob/master/doc/GCodeProtocol.md + // Using this command to reset the axis origin to zero helps in fixing: https://github.com/prusa3d/PrusaSlicer/issues/3082 + + if (line.has_x()) + m_origin[X] = 0.0f; + + if (line.has_y()) + m_origin[Y] = 0.0f; + + if (line.has_z()) + m_origin[Z] = 0.0f; + + if (line.has_e()) + m_origin[E] = 0.0f; +} + +void GCodeProcessor::process_M135(const GCodeReader::GCodeLine& line) +{ + // These M-codes are used by MakerWare to change active tool. + // They have to be processed otherwise toolchanges will be unrecognised + // by the analyzer - see https://github.com/prusa3d/PrusaSlicer/issues/2566 + + if (m_flavor != gcfMakerWare) + return; + + std::string cmd = line.raw(); + size_t pos = cmd.find("T"); + if (pos != std::string::npos) + process_T(cmd.substr(pos)); +} + +void GCodeProcessor::process_M201(const GCodeReader::GCodeLine& line) +{ + if (!m_time_processor.machine_envelope_processing_enabled) + return; + + // see http://reprap.org/wiki/G-code#M201:_Set_max_printing_acceleration + float factor = (m_flavor != gcfRepRap && m_units == EUnits::Inches) ? INCHES_TO_MM : 1.0f; + + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + if (line.has_x()) + set_option_value(m_time_processor.machine_limits.machine_max_acceleration_x, i, line.x() * factor); + + if (line.has_y()) + set_option_value(m_time_processor.machine_limits.machine_max_acceleration_y, i, line.y() * factor); + + if (line.has_z()) + set_option_value(m_time_processor.machine_limits.machine_max_acceleration_z, i, line.z() * factor); + + if (line.has_e()) + set_option_value(m_time_processor.machine_limits.machine_max_acceleration_e, i, line.e() * factor); + } +} + +void GCodeProcessor::process_M203(const GCodeReader::GCodeLine& line) +{ + if (!m_time_processor.machine_envelope_processing_enabled) + return; + + // see http://reprap.org/wiki/G-code#M203:_Set_maximum_feedrate + if (m_flavor == gcfRepetier) + return; + + // see http://reprap.org/wiki/G-code#M203:_Set_maximum_feedrate + // http://smoothieware.org/supported-g-codes + float factor = (m_flavor == gcfMarlin || m_flavor == gcfSmoothie) ? 1.0f : MMMIN_TO_MMSEC; + + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + if (line.has_x()) + set_option_value(m_time_processor.machine_limits.machine_max_feedrate_x, i, line.x() * factor); + + if (line.has_y()) + set_option_value(m_time_processor.machine_limits.machine_max_feedrate_y, i, line.y() * factor); + + if (line.has_z()) + set_option_value(m_time_processor.machine_limits.machine_max_feedrate_z, i, line.z() * factor); + + if (line.has_e()) + set_option_value(m_time_processor.machine_limits.machine_max_feedrate_e, i, line.e() * factor); + } +} + +void GCodeProcessor::process_M204(const GCodeReader::GCodeLine& line) +{ + if (!m_time_processor.machine_envelope_processing_enabled) + return; + + float value; + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + if (line.has_value('S', value)) { + // Legacy acceleration format. This format is used by the legacy Marlin, MK2 or MK3 firmware, + // and it is also generated by Slic3r to control acceleration per extrusion type + // (there is a separate acceleration settings in Slicer for perimeter, first layer etc). + set_acceleration(static_cast(i), value); + if (line.has_value('T', value)) + set_option_value(m_time_processor.machine_limits.machine_max_acceleration_retracting, i, value); + } + else { + // New acceleration format, compatible with the upstream Marlin. + if (line.has_value('P', value)) + set_acceleration(static_cast(i), value); + if (line.has_value('R', value)) + set_option_value(m_time_processor.machine_limits.machine_max_acceleration_retracting, i, value); + if (line.has_value('T', value)) { + // Interpret the T value as the travel acceleration in the new Marlin format. + //FIXME Prusa3D firmware currently does not support travel acceleration value independent from the extruding acceleration value. + // set_travel_acceleration(value); + } + } + } +} + +void GCodeProcessor::process_M205(const GCodeReader::GCodeLine& line) +{ + if (!m_time_processor.machine_envelope_processing_enabled) + return; + + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + if (line.has_x()) { + float max_jerk = line.x(); + set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, max_jerk); + set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, max_jerk); + } + + if (line.has_y()) + set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, line.y()); + + if (line.has_z()) + set_option_value(m_time_processor.machine_limits.machine_max_jerk_z, i, line.z()); + + if (line.has_e()) + set_option_value(m_time_processor.machine_limits.machine_max_jerk_e, i, line.e()); + + float value; + if (line.has_value('S', value)) + set_option_value(m_time_processor.machine_limits.machine_min_extruding_rate, i, value); + + if (line.has_value('T', value)) + set_option_value(m_time_processor.machine_limits.machine_min_travel_rate, i, value); + } +} + +void GCodeProcessor::process_M221(const GCodeReader::GCodeLine& line) +{ + float value_s; + float value_t; + if (line.has_value('S', value_s) && !line.has_value('T', value_t)) { + value_s *= 0.01f; + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + m_time_processor.machines[i].extrude_factor_override_percentage = value_s; + } + } +} + +void GCodeProcessor::process_M401(const GCodeReader::GCodeLine& line) +{ + if (m_flavor != gcfRepetier) + return; + + for (unsigned char a = 0; a <= 3; ++a) { + m_cached_position.position[a] = m_start_position[a]; + } + m_cached_position.feedrate = m_feedrate; +} + +void GCodeProcessor::process_M402(const GCodeReader::GCodeLine& line) +{ + if (m_flavor != gcfRepetier) + return; + + // see for reference: + // https://github.com/repetier/Repetier-Firmware/blob/master/src/ArduinoAVR/Repetier/Printer.cpp + // void Printer::GoToMemoryPosition(bool x, bool y, bool z, bool e, float feed) + + bool has_xyz = !(line.has_x() || line.has_y() || line.has_z()); + + float p = FLT_MAX; + for (unsigned char a = X; a <= Z; ++a) { + if (has_xyz || line.has(a)) { + p = m_cached_position.position[a]; + if (p != FLT_MAX) + m_start_position[a] = p; + } + } + + p = m_cached_position.position[E]; + if (p != FLT_MAX) + m_start_position[E] = p; + + p = FLT_MAX; + if (!line.has_value(4, p)) + p = m_cached_position.feedrate; + + if (p != FLT_MAX) + m_feedrate = p; +} + +void GCodeProcessor::process_M566(const GCodeReader::GCodeLine& line) +{ + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + if (line.has_x()) + set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, line.x() * MMMIN_TO_MMSEC); + + if (line.has_y()) + set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, line.y() * MMMIN_TO_MMSEC); + + if (line.has_z()) + set_option_value(m_time_processor.machine_limits.machine_max_jerk_z, i, line.z() * MMMIN_TO_MMSEC); + + if (line.has_e()) + set_option_value(m_time_processor.machine_limits.machine_max_jerk_e, i, line.e() * MMMIN_TO_MMSEC); + } +} + +void GCodeProcessor::process_M702(const GCodeReader::GCodeLine& line) +{ + if (line.has('C')) { + // MK3 MMU2 specific M code: + // M702 C is expected to be sent by the custom end G-code when finalizing a print. + // The MK3 unit shall unload and park the active filament into the MMU2 unit. + m_time_processor.extruder_unloaded = true; + simulate_st_synchronize(get_filament_unload_time(m_extruder_id)); + } +} + +void GCodeProcessor::process_T(const GCodeReader::GCodeLine& line) +{ + process_T(line.cmd()); +} + +void GCodeProcessor::process_T(const std::string& command) +{ + if (command.length() > 1) { + try { + unsigned char id = static_cast(std::stoi(command.substr(1))); + if (m_extruder_id != id) { + unsigned char extruders_count = static_cast(m_extruder_offsets.size()); + if (id >= extruders_count) + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid toolchange, maybe from a custom gcode."; + else { + unsigned char old_extruder_id = m_extruder_id; + m_extruder_id = id; + m_cp_color.current = m_extruder_colors[id]; + // Specific to the MK3 MMU2: + // The initial value of extruder_unloaded is set to true indicating + // that the filament is parked in the MMU2 unit and there is nothing to be unloaded yet. + float extra_time = get_filament_unload_time(static_cast(old_extruder_id)); + m_time_processor.extruder_unloaded = false; + extra_time += get_filament_load_time(static_cast(m_extruder_id)); + simulate_st_synchronize(extra_time); + } + + // store tool change move + store_move_vertex(EMoveType::Tool_change); + } + } + catch (...) { + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid toolchange (" << command << ")."; + } + } +} + +void GCodeProcessor::store_move_vertex(EMoveType type) +{ + MoveVertex vertex = { + type, + m_extrusion_role, + m_extruder_id, + m_cp_color.current, + Vec3f(m_end_position[X], m_end_position[Y], m_end_position[Z]) + m_extruder_offsets[m_extruder_id], + m_end_position[E] - m_start_position[E], + m_feedrate, + m_width, + m_height, + m_mm3_per_mm, + m_fan_speed, + static_cast(m_result.moves.size()) + }; + m_result.moves.emplace_back(vertex); +} + +float GCodeProcessor::minimum_feedrate(PrintEstimatedTimeStatistics::ETimeMode mode, float feedrate) const +{ + if (m_time_processor.machine_limits.machine_min_extruding_rate.empty()) + return feedrate; + + return std::max(feedrate, get_option_value(m_time_processor.machine_limits.machine_min_extruding_rate, static_cast(mode))); +} + +float GCodeProcessor::minimum_travel_feedrate(PrintEstimatedTimeStatistics::ETimeMode mode, float feedrate) const +{ + if (m_time_processor.machine_limits.machine_min_travel_rate.empty()) + return feedrate; + + return std::max(feedrate, get_option_value(m_time_processor.machine_limits.machine_min_travel_rate, static_cast(mode))); +} + +float GCodeProcessor::get_axis_max_feedrate(PrintEstimatedTimeStatistics::ETimeMode mode, Axis axis) const +{ + switch (axis) + { + case X: { return get_option_value(m_time_processor.machine_limits.machine_max_feedrate_x, static_cast(mode)); } + case Y: { return get_option_value(m_time_processor.machine_limits.machine_max_feedrate_y, static_cast(mode)); } + case Z: { return get_option_value(m_time_processor.machine_limits.machine_max_feedrate_z, static_cast(mode)); } + case E: { return get_option_value(m_time_processor.machine_limits.machine_max_feedrate_e, static_cast(mode)); } + default: { return 0.0f; } + } +} + +float GCodeProcessor::get_axis_max_acceleration(PrintEstimatedTimeStatistics::ETimeMode mode, Axis axis) const +{ + switch (axis) + { + case X: { return get_option_value(m_time_processor.machine_limits.machine_max_acceleration_x, static_cast(mode)); } + case Y: { return get_option_value(m_time_processor.machine_limits.machine_max_acceleration_y, static_cast(mode)); } + case Z: { return get_option_value(m_time_processor.machine_limits.machine_max_acceleration_z, static_cast(mode)); } + case E: { return get_option_value(m_time_processor.machine_limits.machine_max_acceleration_e, static_cast(mode)); } + default: { return 0.0f; } + } +} + +float GCodeProcessor::get_axis_max_jerk(PrintEstimatedTimeStatistics::ETimeMode mode, Axis axis) const +{ + switch (axis) + { + case X: { return get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, static_cast(mode)); } + case Y: { return get_option_value(m_time_processor.machine_limits.machine_max_jerk_y, static_cast(mode)); } + case Z: { return get_option_value(m_time_processor.machine_limits.machine_max_jerk_z, static_cast(mode)); } + case E: { return get_option_value(m_time_processor.machine_limits.machine_max_jerk_e, static_cast(mode)); } + default: { return 0.0f; } + } +} + +float GCodeProcessor::get_retract_acceleration(PrintEstimatedTimeStatistics::ETimeMode mode) const +{ + return get_option_value(m_time_processor.machine_limits.machine_max_acceleration_retracting, static_cast(mode)); +} + +float GCodeProcessor::get_acceleration(PrintEstimatedTimeStatistics::ETimeMode mode) const +{ + size_t id = static_cast(mode); + return (id < m_time_processor.machines.size()) ? m_time_processor.machines[id].acceleration : DEFAULT_ACCELERATION; +} + +void GCodeProcessor::set_acceleration(PrintEstimatedTimeStatistics::ETimeMode mode, float value) +{ + size_t id = static_cast(mode); + if (id < m_time_processor.machines.size()) { + m_time_processor.machines[id].acceleration = (m_time_processor.machines[id].max_acceleration == 0.0f) ? value : + // Clamp the acceleration with the maximum. + std::min(value, m_time_processor.machines[id].max_acceleration); + } +} + +float GCodeProcessor::get_filament_load_time(size_t extruder_id) +{ + return (m_time_processor.filament_load_times.empty() || m_time_processor.extruder_unloaded) ? + 0.0f : + ((extruder_id < m_time_processor.filament_load_times.size()) ? + m_time_processor.filament_load_times[extruder_id] : m_time_processor.filament_load_times.front()); +} + +float GCodeProcessor::get_filament_unload_time(size_t extruder_id) +{ + return (m_time_processor.filament_unload_times.empty() || m_time_processor.extruder_unloaded) ? + 0.0f : + ((extruder_id < m_time_processor.filament_unload_times.size()) ? + m_time_processor.filament_unload_times[extruder_id] : m_time_processor.filament_unload_times.front()); +} + +void GCodeProcessor::process_custom_gcode_time(CustomGCode::Type code) +{ + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + TimeMachine& machine = m_time_processor.machines[i]; + if (!machine.enabled) + continue; + + TimeMachine::CustomGCodeTime& gcode_time = machine.gcode_time; + gcode_time.needed = true; + //FIXME this simulates st_synchronize! is it correct? + // The estimated time may be longer than the real print time. + machine.simulate_st_synchronize(); + if (gcode_time.cache != 0.0f) { + gcode_time.times.push_back({ code, gcode_time.cache }); + gcode_time.cache = 0.0f; + } + } +} + +void GCodeProcessor::simulate_st_synchronize(float additional_time) +{ + for (size_t i = 0; i < static_cast(PrintEstimatedTimeStatistics::ETimeMode::Count); ++i) { + m_time_processor.machines[i].simulate_st_synchronize(additional_time); + } +} + +void GCodeProcessor::update_estimated_times_stats() +{ + auto update_mode = [this](PrintEstimatedTimeStatistics::ETimeMode mode) { + PrintEstimatedTimeStatistics::Mode& data = m_result.time_statistics.modes[static_cast(mode)]; + data.time = get_time(mode); + data.custom_gcode_times = get_custom_gcode_times(mode, true); + data.moves_times = get_moves_time(mode); + data.roles_times = get_roles_time(mode); + data.layers_times = get_layers_time(mode); + }; + + update_mode(PrintEstimatedTimeStatistics::ETimeMode::Normal); + if (m_time_processor.machines[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Stealth)].enabled) + update_mode(PrintEstimatedTimeStatistics::ETimeMode::Stealth); + else + m_result.time_statistics.modes[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Stealth)].reset(); +} + +} /* namespace Slic3r */ + +#endif // ENABLE_GCODE_VIEWER diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp new file mode 100644 index 0000000000..b31591ca86 --- /dev/null +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -0,0 +1,560 @@ +#ifndef slic3r_GCodeProcessor_hpp_ +#define slic3r_GCodeProcessor_hpp_ + +#if ENABLE_GCODE_VIEWER +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/CustomGCode.hpp" + +#include +#include +#include + +namespace Slic3r { + + enum class EMoveType : unsigned char + { + Noop, + Retract, + Unretract, + Tool_change, + Color_change, + Pause_Print, + Custom_GCode, + Travel, + Extrude, + Count + }; + + struct PrintEstimatedTimeStatistics + { + enum class ETimeMode : unsigned char + { + Normal, + Stealth, + Count + }; + + struct Mode + { + float time; + std::vector>> custom_gcode_times; + std::vector> moves_times; + std::vector> roles_times; + std::vector layers_times; + + void reset() { + time = 0.0f; + custom_gcode_times.clear(); + moves_times.clear(); + roles_times.clear(); + layers_times.clear(); + } + }; + + std::array(ETimeMode::Count)> modes; + + PrintEstimatedTimeStatistics() { reset(); } + + void reset() { + for (auto m : modes) { + m.reset(); + } + } + }; + + class GCodeProcessor + { + public: + static const std::string Extrusion_Role_Tag; + static const std::string Height_Tag; + static const std::string Layer_Change_Tag; + static const std::string Color_Change_Tag; + static const std::string Pause_Print_Tag; + static const std::string Custom_Code_Tag; + static const std::string First_Line_M73_Placeholder_Tag; + static const std::string Last_Line_M73_Placeholder_Tag; + static const std::string Estimated_Printing_Time_Placeholder_Tag; + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + static const std::string Width_Tag; + static const std::string Mm3_Per_Mm_Tag; +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + private: + using AxisCoords = std::array; + using ExtruderColors = std::vector; + + enum class EUnits : unsigned char + { + Millimeters, + Inches + }; + + enum class EPositioningType : unsigned char + { + Absolute, + Relative + }; + + struct CachedPosition + { + AxisCoords position; // mm + float feedrate; // mm/s + + void reset(); + }; + + struct CpColor + { + unsigned char counter; + unsigned char current; + + void reset(); + }; + + public: + struct FeedrateProfile + { + float entry{ 0.0f }; // mm/s + float cruise{ 0.0f }; // mm/s + float exit{ 0.0f }; // mm/s + }; + + struct Trapezoid + { + float accelerate_until{ 0.0f }; // mm + float decelerate_after{ 0.0f }; // mm + float cruise_feedrate{ 0.0f }; // mm/sec + + float acceleration_time(float entry_feedrate, float acceleration) const; + float cruise_time() const; + float deceleration_time(float distance, float acceleration) const; + float cruise_distance() const; + }; + + struct TimeBlock + { + struct Flags + { + bool recalculate{ false }; + bool nominal_length{ false }; + }; + + EMoveType move_type{ EMoveType::Noop }; + ExtrusionRole role{ erNone }; + unsigned int layer_id{ 0 }; + float distance{ 0.0f }; // mm + float acceleration{ 0.0f }; // mm/s^2 + float max_entry_speed{ 0.0f }; // mm/s + float safe_feedrate{ 0.0f }; // mm/s + Flags flags; + FeedrateProfile feedrate_profile; + Trapezoid trapezoid; + + // Calculates this block's trapezoid + void calculate_trapezoid(); + + float time() const; + }; + + private: + struct TimeMachine + { + struct State + { + float feedrate; // mm/s + float safe_feedrate; // mm/s + AxisCoords axis_feedrate; // mm/s + AxisCoords abs_axis_feedrate; // mm/s + + void reset(); + }; + + struct CustomGCodeTime + { + bool needed; + float cache; + std::vector> times; + + void reset(); + }; + + bool enabled; + float acceleration; // mm/s^2 + // hard limit for the acceleration, to which the firmware will clamp. + float max_acceleration; // mm/s^2 + float extrude_factor_override_percentage; + float time; // s + std::string line_m73_mask; + State curr; + State prev; + CustomGCodeTime gcode_time; + std::vector blocks; + std::vector g1_times_cache; + std::array(EMoveType::Count)> moves_time; + std::array(ExtrusionRole::erCount)> roles_time; + std::vector layers_time; + + void reset(); + + // Simulates firmware st_synchronize() call + void simulate_st_synchronize(float additional_time = 0.0f); + void calculate_time(size_t keep_last_n_blocks = 0); + }; + + struct TimeProcessor + { + struct Planner + { + // Size of the firmware planner queue. The old 8-bit Marlins usually just managed 16 trapezoidal blocks. + // Let's be conservative and plan for newer boards with more memory. + static constexpr size_t queue_size = 64; + // The firmware recalculates last planner_queue_size trapezoidal blocks each time a new block is added. + // We are not simulating the firmware exactly, we calculate a sequence of blocks once a reasonable number of blocks accumulate. + static constexpr size_t refresh_threshold = queue_size * 4; + }; + + // extruder_id is currently used to correctly calculate filament load / unload times into the total print time. + // This is currently only really used by the MK3 MMU2: + // extruder_unloaded = true means no filament is loaded yet, all the filaments are parked in the MK3 MMU2 unit. + bool extruder_unloaded; + // whether or not to export post-process the gcode to export lines M73 in it + bool export_remaining_time_enabled; + // allow to skip the lines M201/M203/M204/M205 generated by GCode::print_machine_envelope() + bool machine_envelope_processing_enabled; + MachineEnvelopeConfig machine_limits; + // Additional load / unload times for a filament exchange sequence. + std::vector filament_load_times; + std::vector filament_unload_times; + std::array(PrintEstimatedTimeStatistics::ETimeMode::Count)> machines; + + void reset(); + + // post process the file with the given filename to add remaining time lines M73 + void post_process(const std::string& filename); + }; + + public: + struct MoveVertex + { + EMoveType type{ EMoveType::Noop }; + ExtrusionRole extrusion_role{ erNone }; + unsigned char extruder_id{ 0 }; + unsigned char cp_color_id{ 0 }; + Vec3f position{ Vec3f::Zero() }; // mm + float delta_extruder{ 0.0f }; // mm + float feedrate{ 0.0f }; // mm/s + float width{ 0.0f }; // mm + float height{ 0.0f }; // mm + float mm3_per_mm{ 0.0f }; + float fan_speed{ 0.0f }; // percentage + float time{ 0.0f }; // s + + float volumetric_rate() const { return feedrate * mm3_per_mm; } + }; + + struct Result + { + unsigned int id; + std::vector moves; + Pointfs bed_shape; + std::string printer_settings_id; + std::vector extruder_colors; + PrintEstimatedTimeStatistics time_statistics; + +#if ENABLE_GCODE_VIEWER_STATISTICS + long long time{ 0 }; + void reset() + { + time = 0; + moves = std::vector(); + bed_shape = Pointfs(); + extruder_colors = std::vector(); + } +#else + void reset() + { + moves = std::vector(); + bed_shape = Pointfs(); + extruder_colors = std::vector(); + } +#endif // ENABLE_GCODE_VIEWER_STATISTICS + }; + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + struct DataChecker + { + struct Error + { + float value; + float tag_value; + ExtrusionRole role; + }; + + std::string type; + float threshold{ 0.01f }; + float last_tag_value{ 0.0f }; + unsigned int count{ 0 }; + std::vector errors; + + DataChecker(const std::string& type, float threshold) + : type(type), threshold(threshold) + {} + + void update(float value, ExtrusionRole role) { + ++count; + if (last_tag_value != 0.0f) { + if (std::abs(value - last_tag_value) / last_tag_value > threshold) + errors.push_back({ value, last_tag_value, role }); + } + } + + void reset() { last_tag_value = 0.0f; errors.clear(); count = 0; } + + std::pair get_min() const { + float delta_min = FLT_MAX; + float perc_min = 0.0f; + for (const Error& e : errors) { + if (delta_min > e.value - e.tag_value) { + delta_min = e.value - e.tag_value; + perc_min = 100.0f * delta_min / e.tag_value; + } + } + return { delta_min, perc_min }; + } + + std::pair get_max() const { + float delta_max = -FLT_MAX; + float perc_max = 0.0f; + for (const Error& e : errors) { + if (delta_max < e.value - e.tag_value) { + delta_max = e.value - e.tag_value; + perc_max = 100.0f * delta_max / e.tag_value; + } + } + return { delta_max, perc_max }; + } + + void output() const { + if (!errors.empty()) { + std::cout << type << ":\n"; + std::cout << "Errors: " << errors.size() << " (" << 100.0f * float(errors.size()) / float(count) << "%)\n"; + auto [min, perc_min] = get_min(); + auto [max, perc_max] = get_max(); + std::cout << "min: " << min << "(" << perc_min << "%) - max: " << max << "(" << perc_max << "%)\n"; + } + } + }; +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + private: + GCodeReader m_parser; + + EUnits m_units; + EPositioningType m_global_positioning_type; + EPositioningType m_e_local_positioning_type; + std::vector m_extruder_offsets; + GCodeFlavor m_flavor; + + AxisCoords m_start_position; // mm + AxisCoords m_end_position; // mm + AxisCoords m_origin; // mm + CachedPosition m_cached_position; + + float m_feedrate; // mm/s + float m_width; // mm + float m_height; // mm + float m_mm3_per_mm; + float m_fan_speed; // percentage + ExtrusionRole m_extrusion_role; + unsigned char m_extruder_id; + ExtruderColors m_extruder_colors; + std::vector m_filament_diameters; + float m_extruded_last_z; + unsigned int m_layer_id; + CpColor m_cp_color; + + enum class EProducer + { + Unknown, + PrusaSlicer, + Cura, + Simplify3D, + CraftWare, + ideaMaker + }; + + static const std::vector> Producers; + EProducer m_producer; + bool m_producers_enabled; + + TimeProcessor m_time_processor; + + Result m_result; + static unsigned int s_result_id; + +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + DataChecker m_mm3_per_mm_compare{ "mm3_per_mm", 0.01f }; + DataChecker m_height_compare{ "height", 0.01f }; + DataChecker m_width_compare{ "width", 0.01f }; +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + + public: + GCodeProcessor(); + + void apply_config(const PrintConfig& config); + void apply_config(const DynamicPrintConfig& config); + void enable_stealth_time_estimator(bool enabled); + bool is_stealth_time_estimator_enabled() const { + return m_time_processor.machines[static_cast(PrintEstimatedTimeStatistics::ETimeMode::Stealth)].enabled; + } + void enable_machine_envelope_processing(bool enabled) { m_time_processor.machine_envelope_processing_enabled = enabled; } + void enable_producers(bool enabled) { m_producers_enabled = enabled; } + void reset(); + + const Result& get_result() const { return m_result; } + Result&& extract_result() { return std::move(m_result); } + + // Process the gcode contained in the file with the given filename + // throws CanceledException through print->throw_if_canceled() (sent by the caller as callback). + void process_file(const std::string& filename, std::function cancel_callback = nullptr); + + float get_time(PrintEstimatedTimeStatistics::ETimeMode mode) const; + std::string get_time_dhm(PrintEstimatedTimeStatistics::ETimeMode mode) const; + std::vector>> get_custom_gcode_times(PrintEstimatedTimeStatistics::ETimeMode mode, bool include_remaining) const; + + std::vector> get_moves_time(PrintEstimatedTimeStatistics::ETimeMode mode) const; + std::vector> get_roles_time(PrintEstimatedTimeStatistics::ETimeMode mode) const; + std::vector get_layers_time(PrintEstimatedTimeStatistics::ETimeMode mode) const; + + private: + void process_gcode_line(const GCodeReader::GCodeLine& line); + + // Process tags embedded into comments + void process_tags(const std::string& comment); + bool process_producers_tags(const std::string& comment); + bool process_prusaslicer_tags(const std::string& comment); + bool process_cura_tags(const std::string& comment); + bool process_simplify3d_tags(const std::string& comment); + bool process_craftware_tags(const std::string& comment); + bool process_ideamaker_tags(const std::string& comment); + + bool detect_producer(const std::string& comment); + + // Move + void process_G0(const GCodeReader::GCodeLine& line); + void process_G1(const GCodeReader::GCodeLine& line); + + // Retract + void process_G10(const GCodeReader::GCodeLine& line); + + // Unretract + void process_G11(const GCodeReader::GCodeLine& line); + + // Set Units to Inches + void process_G20(const GCodeReader::GCodeLine& line); + + // Set Units to Millimeters + void process_G21(const GCodeReader::GCodeLine& line); + + // Firmware controlled Retract + void process_G22(const GCodeReader::GCodeLine& line); + + // Firmware controlled Unretract + void process_G23(const GCodeReader::GCodeLine& line); + + // Set to Absolute Positioning + void process_G90(const GCodeReader::GCodeLine& line); + + // Set to Relative Positioning + void process_G91(const GCodeReader::GCodeLine& line); + + // Set Position + void process_G92(const GCodeReader::GCodeLine& line); + + // Sleep or Conditional stop + void process_M1(const GCodeReader::GCodeLine& line); + + // Set extruder to absolute mode + void process_M82(const GCodeReader::GCodeLine& line); + + // Set extruder to relative mode + void process_M83(const GCodeReader::GCodeLine& line); + + // Set fan speed + void process_M106(const GCodeReader::GCodeLine& line); + + // Disable fan + void process_M107(const GCodeReader::GCodeLine& line); + + // Set tool (Sailfish) + void process_M108(const GCodeReader::GCodeLine& line); + + // Recall stored home offsets + void process_M132(const GCodeReader::GCodeLine& line); + + // Set tool (MakerWare) + void process_M135(const GCodeReader::GCodeLine& line); + + // Set max printing acceleration + void process_M201(const GCodeReader::GCodeLine& line); + + // Set maximum feedrate + void process_M203(const GCodeReader::GCodeLine& line); + + // Set default acceleration + void process_M204(const GCodeReader::GCodeLine& line); + + // Advanced settings + void process_M205(const GCodeReader::GCodeLine& line); + + // Set extrude factor override percentage + void process_M221(const GCodeReader::GCodeLine& line); + + // Repetier: Store x, y and z position + void process_M401(const GCodeReader::GCodeLine& line); + + // Repetier: Go to stored position + void process_M402(const GCodeReader::GCodeLine& line); + + // Set allowable instantaneous speed change + void process_M566(const GCodeReader::GCodeLine& line); + + // Unload the current filament into the MK3 MMU2 unit at the end of print. + void process_M702(const GCodeReader::GCodeLine& line); + + // Processes T line (Select Tool) + void process_T(const GCodeReader::GCodeLine& line); + void process_T(const std::string& command); + + void store_move_vertex(EMoveType type); + + float minimum_feedrate(PrintEstimatedTimeStatistics::ETimeMode mode, float feedrate) const; + float minimum_travel_feedrate(PrintEstimatedTimeStatistics::ETimeMode mode, float feedrate) const; + float get_axis_max_feedrate(PrintEstimatedTimeStatistics::ETimeMode mode, Axis axis) const; + float get_axis_max_acceleration(PrintEstimatedTimeStatistics::ETimeMode mode, Axis axis) const; + float get_axis_max_jerk(PrintEstimatedTimeStatistics::ETimeMode mode, Axis axis) const; + float get_retract_acceleration(PrintEstimatedTimeStatistics::ETimeMode mode) const; + float get_acceleration(PrintEstimatedTimeStatistics::ETimeMode mode) const; + void set_acceleration(PrintEstimatedTimeStatistics::ETimeMode mode, float value); + float get_filament_load_time(size_t extruder_id); + float get_filament_unload_time(size_t extruder_id); + + void process_custom_gcode_time(CustomGCode::Type code); + + // Simulates firmware st_synchronize() call + void simulate_st_synchronize(float additional_time = 0.0f); + + void update_estimated_times_stats(); + }; + +} /* namespace Slic3r */ + +#endif // ENABLE_GCODE_VIEWER + +#endif /* slic3r_GCodeProcessor_hpp_ */ + + diff --git a/src/libslic3r/GCode/PostProcessor.cpp b/src/libslic3r/GCode/PostProcessor.cpp index 25982959be..17aa76fb9d 100644 --- a/src/libslic3r/GCode/PostProcessor.cpp +++ b/src/libslic3r/GCode/PostProcessor.cpp @@ -79,7 +79,7 @@ static DWORD execute_process_winapi(const std::wstring &command_line) if (! ::CreateProcessW( nullptr /* lpApplicationName */, (LPWSTR)command_line.c_str(), nullptr /* lpProcessAttributes */, nullptr /* lpThreadAttributes */, false /* bInheritHandles */, CREATE_UNICODE_ENVIRONMENT /* | CREATE_NEW_CONSOLE */ /* dwCreationFlags */, (LPVOID)envstr.c_str(), nullptr /* lpCurrentDirectory */, &startup_info, &process_info)) - throw std::runtime_error(std::string("Failed starting the script ") + boost::nowide::narrow(command_line) + ", Win32 error: " + std::to_string(int(::GetLastError()))); + throw Slic3r::RuntimeError(std::string("Failed starting the script ") + boost::nowide::narrow(command_line) + ", Win32 error: " + std::to_string(int(::GetLastError()))); ::WaitForSingleObject(process_info.hProcess, INFINITE); ULONG rc = 0; ::GetExitCodeProcess(process_info.hProcess, &rc); @@ -98,13 +98,13 @@ static int run_script(const std::string &script, const std::string &gcode, std:: LPWSTR *szArglist = CommandLineToArgvW(boost::nowide::widen(script).c_str(), &nArgs); if (szArglist == nullptr || nArgs <= 0) { // CommandLineToArgvW failed. Maybe the command line escapment is invalid? - throw std::runtime_error(std::string("Post processing script ") + script + " on file " + gcode + " failed. CommandLineToArgvW() refused to parse the command line path."); + throw Slic3r::RuntimeError(std::string("Post processing script ") + script + " on file " + gcode + " failed. CommandLineToArgvW() refused to parse the command line path."); } std::wstring command_line; std::wstring command = szArglist[0]; if (! boost::filesystem::exists(boost::filesystem::path(command))) - throw std::runtime_error(std::string("The configured post-processing script does not exist: ") + boost::nowide::narrow(command)); + throw Slic3r::RuntimeError(std::string("The configured post-processing script does not exist: ") + boost::nowide::narrow(command)); if (boost::iends_with(command, L".pl")) { // This is a perl script. Run it through the perl interpreter. // The current process may be slic3r.exe or slic3r-console.exe. @@ -115,7 +115,7 @@ static int run_script(const std::string &script, const std::string &gcode, std:: boost::filesystem::path path_perl = path_exe.parent_path() / "perl" / "perl.exe"; if (! boost::filesystem::exists(path_perl)) { LocalFree(szArglist); - throw std::runtime_error(std::string("Perl interpreter ") + path_perl.string() + " does not exist."); + throw Slic3r::RuntimeError(std::string("Perl interpreter ") + path_perl.string() + " does not exist."); } // Replace it with the current perl interpreter. quote_argv_winapi(boost::nowide::widen(path_perl.string()), command_line); @@ -187,7 +187,7 @@ void run_post_process_scripts(const std::string &path, const PrintConfig &config config.setenv_(); auto gcode_file = boost::filesystem::path(path); if (! boost::filesystem::exists(gcode_file)) - throw std::runtime_error(std::string("Post-processor can't find exported gcode file")); + throw Slic3r::RuntimeError(std::string("Post-processor can't find exported gcode file")); for (const std::string &scripts : config.post_process.values) { std::vector lines; @@ -205,7 +205,7 @@ void run_post_process_scripts(const std::string &path, const PrintConfig &config const std::string msg = std_err.empty() ? (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%") % script % path % result).str() : (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%") % script % path % result % std_err).str(); BOOST_LOG_TRIVIAL(error) << msg; - throw std::runtime_error(msg); + throw Slic3r::RuntimeError(msg); } } } diff --git a/src/libslic3r/GCode/PressureEqualizer.cpp b/src/libslic3r/GCode/PressureEqualizer.cpp index 3b2a58a884..33601e5e9f 100644 --- a/src/libslic3r/GCode/PressureEqualizer.cpp +++ b/src/libslic3r/GCode/PressureEqualizer.cpp @@ -148,7 +148,7 @@ static inline int parse_int(const char *&line) char *endptr = NULL; long result = strtol(line, &endptr, 10); if (endptr == NULL || !is_ws_or_eol(*endptr)) - throw std::runtime_error("PressureEqualizer: Error parsing an int"); + throw Slic3r::RuntimeError("PressureEqualizer: Error parsing an int"); line = endptr; return int(result); }; @@ -160,7 +160,7 @@ static inline float parse_float(const char *&line) char *endptr = NULL; float result = strtof(line, &endptr); if (endptr == NULL || !is_ws_or_eol(*endptr)) - throw std::runtime_error("PressureEqualizer: Error parsing a float"); + throw Slic3r::RuntimeError("PressureEqualizer: Error parsing a float"); line = endptr; return result; }; @@ -229,7 +229,7 @@ bool PressureEqualizer::process_line(const char *line, const size_t len, GCodeLi assert(false); } if (i == -1) - throw std::runtime_error(std::string("GCode::PressureEqualizer: Invalid axis for G0/G1: ") + axis); + throw Slic3r::RuntimeError(std::string("GCode::PressureEqualizer: Invalid axis for G0/G1: ") + axis); buf.pos_provided[i] = true; new_pos[i] = parse_float(line); if (i == 3 && m_config->use_relative_e_distances.value) @@ -298,7 +298,7 @@ bool PressureEqualizer::process_line(const char *line, const size_t len, GCodeLi set = true; break; default: - throw std::runtime_error(std::string("GCode::PressureEqualizer: Incorrect axis in a G92 G-code: ") + axis); + throw Slic3r::RuntimeError(std::string("GCode::PressureEqualizer: Incorrect axis in a G92 G-code: ") + axis); } eatws(line); } diff --git a/src/libslic3r/GCode/PreviewData.cpp b/src/libslic3r/GCode/PreviewData.cpp index 551c133450..8aec327db3 100644 --- a/src/libslic3r/GCode/PreviewData.cpp +++ b/src/libslic3r/GCode/PreviewData.cpp @@ -5,6 +5,8 @@ #include +#if !ENABLE_GCODE_VIEWER + //! macro used to mark string used at localization, #define L(s) (s) @@ -516,3 +518,5 @@ Color operator * (float f, const Color& color) } } // namespace Slic3r + +#endif // !ENABLE_GCODE_VIEWER diff --git a/src/libslic3r/GCode/PreviewData.hpp b/src/libslic3r/GCode/PreviewData.hpp index 35bbfa50ac..930c1659e3 100644 --- a/src/libslic3r/GCode/PreviewData.hpp +++ b/src/libslic3r/GCode/PreviewData.hpp @@ -1,6 +1,8 @@ #ifndef slic3r_GCode_PreviewData_hpp_ #define slic3r_GCode_PreviewData_hpp_ +#if !ENABLE_GCODE_VIEWER + #include "../libslic3r.h" #include "../ExtrusionEntity.hpp" #include "../Point.hpp" @@ -56,8 +58,7 @@ public: // Color mapping to convert a float into a smooth rainbow of 10 colors. class RangeBase { - public: - + public: virtual void reset() = 0; virtual bool empty() const = 0; virtual float min() const = 0; @@ -73,7 +74,7 @@ public: // Color mapping converting a float in a range between a min and a max into a smooth rainbow of 10 colors. class Range : public RangeBase { - public: + public: Range(); // RangeBase Overrides @@ -97,8 +98,7 @@ public: template class MultiRange : public RangeBase { - public: - + public: void reset() override { bounds = decltype(bounds){}; @@ -160,8 +160,7 @@ public: mode.set(static_cast(range_type_value), enable); } - private: - + private: // Interval bounds struct Bounds { @@ -394,4 +393,6 @@ public: } // namespace Slic3r +#endif // !ENABLE_GCODE_VIEWER + #endif /* slic3r_GCode_PreviewData_hpp_ */ diff --git a/src/libslic3r/GCode/PrintExtents.cpp b/src/libslic3r/GCode/PrintExtents.cpp index 4a6624531b..a86411519f 100644 --- a/src/libslic3r/GCode/PrintExtents.cpp +++ b/src/libslic3r/GCode/PrintExtents.cpp @@ -94,7 +94,7 @@ static BoundingBoxf extrusionentity_extents(const ExtrusionEntity *extrusion_ent auto *extrusion_entity_collection = dynamic_cast(extrusion_entity); if (extrusion_entity_collection != nullptr) return extrusionentity_extents(*extrusion_entity_collection); - throw std::runtime_error("Unexpected extrusion_entity type in extrusionentity_extents()"); + throw Slic3r::RuntimeError("Unexpected extrusion_entity type in extrusionentity_extents()"); return BoundingBoxf(); } diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index b2534361f3..6b94b213a3 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -21,7 +21,11 @@ TODO LIST #include #include +#if ENABLE_GCODE_VIEWER +#include "GCodeProcessor.hpp" +#else #include "Analyzer.hpp" +#endif // ENABLE_GCODE_VIEWER #include "BoundingBox.hpp" #if defined(__linux) || defined(__GNUC__ ) @@ -47,36 +51,69 @@ public: m_extrusion_flow(0.f), m_preview_suppressed(false), m_elapsed_time(0.f), +#if !ENABLE_GCODE_VIEWER || ENABLE_GCODE_VIEWER_DATA_CHECKING m_default_analyzer_line_width(line_width), +#endif // !ENABLE_GCODE_VIEWER || ENABLE_GCODE_VIEWER_DATA_CHECKING m_gcode_flavor(flavor), m_filpar(filament_parameters) { // adds tag for analyzer: char buf[64]; +#if ENABLE_GCODE_VIEWER + sprintf(buf, ";%s%f\n", GCodeProcessor::Height_Tag.c_str(), m_layer_height); // don't rely on GCodeAnalyzer knowing the layer height - it knows nothing at priming + m_gcode += buf; + sprintf(buf, ";%s%s\n", GCodeProcessor::Extrusion_Role_Tag.c_str(), ExtrusionEntity::role_to_string(erWipeTower).c_str()); + m_gcode += buf; +#else sprintf(buf, ";%s%f\n", GCodeAnalyzer::Height_Tag.c_str(), m_layer_height); // don't rely on GCodeAnalyzer knowing the layer height - it knows nothing at priming m_gcode += buf; sprintf(buf, ";%s%d\n", GCodeAnalyzer::Extrusion_Role_Tag.c_str(), erWipeTower); m_gcode += buf; +#endif // ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER_DATA_CHECKING change_analyzer_line_width(line_width); +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING } - WipeTowerWriter& change_analyzer_line_width(float line_width) { - // adds tag for analyzer: - char buf[64]; - sprintf(buf, ";%s%f\n", GCodeAnalyzer::Width_Tag.c_str(), line_width); - m_gcode += buf; - return *this; +#if ENABLE_GCODE_VIEWER_DATA_CHECKING + WipeTowerWriter& change_analyzer_line_width(float line_width) { + // adds tag for analyzer: + char buf[64]; + sprintf(buf, ";%s%f\n", GCodeProcessor::Width_Tag.c_str(), line_width); + m_gcode += buf; + return *this; } - WipeTowerWriter& change_analyzer_mm3_per_mm(float len, float e) { - static const float area = float(M_PI) * 1.75f * 1.75f / 4.f; - float mm3_per_mm = (len == 0.f ? 0.f : area * e / len); - // adds tag for analyzer: - char buf[64]; - sprintf(buf, ";%s%f\n", GCodeAnalyzer::Mm3_Per_Mm_Tag.c_str(), mm3_per_mm); - m_gcode += buf; - return *this; + WipeTowerWriter& change_analyzer_mm3_per_mm(float len, float e) { + static const float area = float(M_PI) * 1.75f * 1.75f / 4.f; + float mm3_per_mm = (len == 0.f ? 0.f : area * e / len); + // adds tag for processor: + char buf[64]; + sprintf(buf, ";%s%f\n", GCodeProcessor::Mm3_Per_Mm_Tag.c_str(), mm3_per_mm); + m_gcode += buf; + return *this; } +#else +#if !ENABLE_GCODE_VIEWER + WipeTowerWriter& change_analyzer_line_width(float line_width) { + // adds tag for analyzer: + char buf[64]; + sprintf(buf, ";%s%f\n", GCodeAnalyzer::Width_Tag.c_str(), line_width); + m_gcode += buf; + return *this; + } + + WipeTowerWriter& change_analyzer_mm3_per_mm(float len, float e) { + static const float area = float(M_PI) * 1.75f * 1.75f / 4.f; + float mm3_per_mm = (len == 0.f ? 0.f : area * e / len); + // adds tag for analyzer: + char buf[64]; + sprintf(buf, ";%s%f\n", GCodeAnalyzer::Mm3_Per_Mm_Tag.c_str(), mm3_per_mm); + m_gcode += buf; + return *this; + } +#endif // !ENABLE_GCODE_VIEWER +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING WipeTowerWriter& set_initial_position(const Vec2f &pos, float width = 0.f, float depth = 0.f, float internal_angle = 0.f) { m_wipe_tower_width = width; @@ -111,8 +148,13 @@ public: // Suppress / resume G-code preview in Slic3r. Slic3r will have difficulty to differentiate the various // filament loading and cooling moves from normal extrusion moves. Therefore the writer // is asked to suppres output of some lines, which look like extrusions. - WipeTowerWriter& suppress_preview() { change_analyzer_line_width(0.f); m_preview_suppressed = true; return *this; } - WipeTowerWriter& resume_preview() { change_analyzer_line_width(m_default_analyzer_line_width); m_preview_suppressed = false; return *this; } +#if !ENABLE_GCODE_VIEWER || ENABLE_GCODE_VIEWER_DATA_CHECKING + WipeTowerWriter& suppress_preview() { change_analyzer_line_width(0.f); m_preview_suppressed = true; return *this; } + WipeTowerWriter& resume_preview() { change_analyzer_line_width(m_default_analyzer_line_width); m_preview_suppressed = false; return *this; } +#else + WipeTowerWriter& suppress_preview() { m_preview_suppressed = true; return *this; } + WipeTowerWriter& resume_preview() { m_preview_suppressed = false; return *this; } +#endif // !ENABLE_GCODE_VIEWER || ENABLE_GCODE_VIEWER_DATA_CHECKING WipeTowerWriter& feedrate(float f) { @@ -149,8 +191,14 @@ public: Vec2f rot(this->rotate(Vec2f(x,y))); // this is where we want to go if (! m_preview_suppressed && e > 0.f && len > 0.f) { +#if ENABLE_GCODE_VIEWER_DATA_CHECKING change_analyzer_mm3_per_mm(len, e); - // Width of a squished extrusion, corrected for the roundings of the squished extrusions. +#else +#if !ENABLE_GCODE_VIEWER + change_analyzer_mm3_per_mm(len, e); +#endif // !ENABLE_GCODE_VIEWER +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING + // Width of a squished extrusion, corrected for the roundings of the squished extrusions. // This is left zero if it is a travel move. float width = e * m_filpar[0].filament_area / (len * m_layer_height); // Correct for the roundings of a squished extrusion. @@ -411,7 +459,9 @@ private: float m_wipe_tower_depth = 0.f; unsigned m_last_fan_speed = 0; int current_temp = -1; +#if !ENABLE_GCODE_VIEWER || ENABLE_GCODE_VIEWER_DATA_CHECKING const float m_default_analyzer_line_width; +#endif // !ENABLE_GCODE_VIEWER || ENABLE_GCODE_VIEWER_DATA_CHECKING float m_used_filament_length = 0.f; GCodeFlavor m_gcode_flavor; const std::vector& m_filpar; @@ -852,8 +902,12 @@ void WipeTower::toolchange_Unload( const float line_width = m_perimeter_width * m_filpar[m_current_tool].ramming_line_width_multiplicator; // desired ramming line thickness const float y_step = line_width * m_filpar[m_current_tool].ramming_step_multiplicator * m_extra_spacing; // spacing between lines in mm +#if ENABLE_GCODE_VIEWER_DATA_CHECKING writer.append("; CP TOOLCHANGE UNLOAD\n") - .change_analyzer_line_width(line_width); + .change_analyzer_line_width(line_width); +#else + writer.append("; CP TOOLCHANGE UNLOAD\n"); +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING unsigned i = 0; // iterates through ramming_speed m_left_to_right = true; // current direction of ramming @@ -930,7 +984,9 @@ void WipeTower::toolchange_Unload( } } Vec2f end_of_ramming(writer.x(),writer.y()); +#if ENABLE_GCODE_VIEWER_DATA_CHECKING writer.change_analyzer_line_width(m_perimeter_width); // so the next lines are not affected by ramming_line_width_multiplier +#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING // Retraction: float old_x = writer.x(); diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp index e68bc5ad29..ee24d5bb76 100644 --- a/src/libslic3r/GCodeReader.cpp +++ b/src/libslic3r/GCodeReader.cpp @@ -1,6 +1,9 @@ #include "GCodeReader.hpp" #include #include +#if ENABLE_GCODE_VIEWER +#include +#endif // ENABLE_GCODE_VIEWER #include #include #include @@ -113,9 +116,18 @@ void GCodeReader::update_coordinates(GCodeLine &gline, std::pairparse_line(line, callback); } diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp index 9503ddcc16..7e0793cd9b 100644 --- a/src/libslic3r/GCodeReader.hpp +++ b/src/libslic3r/GCodeReader.hpp @@ -107,6 +107,9 @@ public: { GCodeLine gline; this->parse_line(line.c_str(), gline, callback); } void parse_file(const std::string &file, callback_t callback); +#if ENABLE_GCODE_VIEWER + void quit_parsing_file() { m_parsing_file = false; } +#endif // ENABLE_GCODE_VIEWER float& x() { return m_position[X]; } float x() const { return m_position[X]; } @@ -145,6 +148,9 @@ private: char m_extrusion_axis; float m_position[NUM_AXES]; bool m_verbose; +#if ENABLE_GCODE_VIEWER + bool m_parsing_file{ false }; +#endif // ENABLE_GCODE_VIEWER }; } /* namespace Slic3r */ diff --git a/src/libslic3r/GCodeSender.cpp b/src/libslic3r/GCodeSender.cpp index 9567e07d28..7bda299923 100644 --- a/src/libslic3r/GCodeSender.cpp +++ b/src/libslic3r/GCodeSender.cpp @@ -153,7 +153,7 @@ GCodeSender::set_baud_rate(unsigned int baud_rate) if (::tcsetattr(handle, TCSAFLUSH, &ios) != 0) printf("Failed to set baud rate: %s\n", strerror(errno)); #else - //throw invalid_argument ("OS does not currently support custom bauds"); + //throw Slic3r::InvalidArgument("OS does not currently support custom bauds"); #endif } } diff --git a/src/libslic3r/GCodeTimeEstimator.cpp b/src/libslic3r/GCodeTimeEstimator.cpp index 795fecbeb1..68075c1239 100644 --- a/src/libslic3r/GCodeTimeEstimator.cpp +++ b/src/libslic3r/GCodeTimeEstimator.cpp @@ -1,3 +1,4 @@ +#include "Exception.hpp" #include "GCodeTimeEstimator.hpp" #include "Utils.hpp" #include @@ -9,6 +10,8 @@ #include #include +#if !ENABLE_GCODE_VIEWER + static const float MMMIN_TO_MMSEC = 1.0f / 60.0f; static const float MILLISEC_TO_SEC = 0.001f; static const float INCHES_TO_MM = 25.4f; @@ -252,13 +255,13 @@ namespace Slic3r { { boost::nowide::ifstream in(filename); if (!in.good()) - throw std::runtime_error(std::string("Time estimator post process export failed.\nCannot open file for reading.\n")); + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for reading.\n")); std::string path_tmp = filename + ".postprocess"; FILE* out = boost::nowide::fopen(path_tmp.c_str(), "wb"); if (out == nullptr) - throw std::runtime_error(std::string("Time estimator post process export failed.\nCannot open file for writing.\n")); + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nCannot open file for writing.\n")); std::string normal_time_mask = "M73 P%s R%s\n"; std::string silent_time_mask = "M73 Q%s S%s\n"; @@ -276,7 +279,7 @@ namespace Slic3r { in.close(); fclose(out); boost::nowide::remove(path_tmp.c_str()); - throw std::runtime_error(std::string("Time estimator post process export failed.\nIs the disk full?\n")); + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nIs the disk full?\n")); } export_line.clear(); }; @@ -324,7 +327,7 @@ namespace Slic3r { if (!in.good()) { fclose(out); - throw std::runtime_error(std::string("Time estimator post process export failed.\nError while reading from file.\n")); + throw Slic3r::RuntimeError(std::string("Time estimator post process export failed.\nError while reading from file.\n")); } // check tags @@ -381,7 +384,7 @@ namespace Slic3r { in.close(); if (rename_file(path_tmp, filename)) - throw std::runtime_error(std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + filename + '\n' + + throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + filename + '\n' + "Is " + path_tmp + " locked?" + '\n'); return true; @@ -1672,3 +1675,5 @@ namespace Slic3r { } #endif // ENABLE_MOVE_STATS } + +#endif // !ENABLE_GCODE_VIEWER diff --git a/src/libslic3r/GCodeTimeEstimator.hpp b/src/libslic3r/GCodeTimeEstimator.hpp index 63e11c4faa..0dd3407cb0 100644 --- a/src/libslic3r/GCodeTimeEstimator.hpp +++ b/src/libslic3r/GCodeTimeEstimator.hpp @@ -6,6 +6,8 @@ #include "GCodeReader.hpp" #include "CustomGCode.hpp" +#if !ENABLE_GCODE_VIEWER + #define ENABLE_MOVE_STATS 0 namespace Slic3r { @@ -481,4 +483,6 @@ namespace Slic3r { } /* namespace Slic3r */ +#endif // !ENABLE_GCODE_VIEWER + #endif /* slic3r_GCodeTimeEstimator_hpp_ */ diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index 00a4ad47c3..3b9fcd6176 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -1,4 +1,5 @@ #include "libslic3r.h" +#include "Exception.hpp" #include "Geometry.hpp" #include "ClipperUtils.hpp" #include "ExPolygon.hpp" @@ -471,7 +472,7 @@ Pointfs arrange(size_t num_parts, const Vec2d &part_size, coordf_t gap, const Bo size_t cellw = size_t(floor((bed_bbox.size()(0) + gap) / cell_size(0))); size_t cellh = size_t(floor((bed_bbox.size()(1) + gap) / cell_size(1))); if (num_parts > cellw * cellh) - throw std::invalid_argument("%zu parts won't fit in your print area!\n", num_parts); + throw Slic3r::InvalidArgument("%zu parts won't fit in your print area!\n", num_parts); // Get a bounding box of cellw x cellh cells, centered at the center of the bed. Vec2d cells_size(cellw * cell_size(0) - gap, cellh * cell_size(1) - gap); diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index c104d46da1..8d5db42fc0 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -13,6 +13,10 @@ class Layer; class PrintRegion; class PrintObject; +namespace FillAdaptive_Internal { + struct Octree; +}; + class LayerRegion { public: @@ -134,7 +138,8 @@ public: return false; } void make_perimeters(); - void make_fills(); + void make_fills() { this->make_fills(nullptr, nullptr); }; + void make_fills(FillAdaptive_Internal::Octree* adaptive_fill_octree, FillAdaptive_Internal::Octree* support_fill_octree); void make_ironing(); void export_region_slices_to_svg(const char *path) const; diff --git a/src/libslic3r/Line.cpp b/src/libslic3r/Line.cpp index 974f585dc2..0c43154a9b 100644 --- a/src/libslic3r/Line.cpp +++ b/src/libslic3r/Line.cpp @@ -33,24 +33,6 @@ bool Line::intersection_infinite(const Line &other, Point* point) const return true; } -// Distance to the closest point of line. -double Line::distance_to_squared(const Point &point, const Point &a, const Point &b) -{ - const Vec2d v = (b - a).cast(); - const Vec2d va = (point - a).cast(); - const double l2 = v.squaredNorm(); // avoid a sqrt - if (l2 == 0.0) - // a == b case - return va.squaredNorm(); - // Consider the line extending the segment, parameterized as a + t (b - a). - // We find projection of this point onto the line. - // It falls where t = [(this-a) . (b-a)] / |b-a|^2 - const double t = va.dot(v) / l2; - if (t < 0.0) return va.squaredNorm(); // beyond the 'a' end of the segment - else if (t > 1.0) return (point - b).cast().squaredNorm(); // beyond the 'b' end of the segment - return (t * v - va).squaredNorm(); -} - double Line::perp_distance_to(const Point &point) const { const Line &line = *this; diff --git a/src/libslic3r/Line.hpp b/src/libslic3r/Line.hpp index caab809f5c..980303feda 100644 --- a/src/libslic3r/Line.hpp +++ b/src/libslic3r/Line.hpp @@ -18,6 +18,35 @@ typedef std::vector ThickLines; Linef3 transform(const Linef3& line, const Transform3d& t); +namespace line_alg { + +// Distance to the closest point of line. +template +double distance_to_squared(const L &line, const Vec &point) +{ + const Vec v = line.vector().template cast(); + const Vec va = (point - line.a).template cast(); + const double l2 = v.squaredNorm(); // avoid a sqrt + if (l2 == 0.0) + // a == b case + return va.squaredNorm(); + // Consider the line extending the segment, parameterized as a + t (b - a). + // We find projection of this point onto the line. + // It falls where t = [(this-a) . (b-a)] / |b-a|^2 + const double t = va.dot(v) / l2; + if (t < 0.0) return va.squaredNorm(); // beyond the 'a' end of the segment + else if (t > 1.0) return (point - line.b).template cast().squaredNorm(); // beyond the 'b' end of the segment + return (t * v - va).squaredNorm(); +} + +template +double distance_to(const L &line, const Vec &point) +{ + return std::sqrt(distance_to_squared(line, point)); +} + +} // namespace line_alg + class Line { public: @@ -47,7 +76,7 @@ public: // Clip a line with a bounding box. Returns false if the line is completely outside of the bounding box. bool clip_with_bbox(const BoundingBox &bbox); - static double distance_to_squared(const Point &point, const Point &a, const Point &b); + static inline double distance_to_squared(const Point &point, const Point &a, const Point &b) { return line_alg::distance_to_squared(Line{a, b}, Vec<2, coord_t>{point}); } static double distance_to(const Point &point, const Point &a, const Point &b) { return sqrt(distance_to_squared(point, a, b)); } Point a; diff --git a/src/libslic3r/MTUtils.hpp b/src/libslic3r/MTUtils.hpp index a6c8d61622..555cfe5019 100644 --- a/src/libslic3r/MTUtils.hpp +++ b/src/libslic3r/MTUtils.hpp @@ -114,15 +114,6 @@ template struct remove_cvref template using remove_cvref_t = typename remove_cvref::type; -template // Arbitrary allocator can be used -inline IntegerOnly> reserve_vector(I capacity) -{ - std::vector ret; - if (capacity > I(0)) ret.reserve(size_t(capacity)); - - return ret; -} - /// Exactly like Matlab https://www.mathworks.com/help/matlab/ref/linspace.html template> inline std::vector linspace_vector(const ArithmeticOnly &start, diff --git a/src/libslic3r/MeshBoolean.cpp b/src/libslic3r/MeshBoolean.cpp index 66167c7209..dd34b28300 100644 --- a/src/libslic3r/MeshBoolean.cpp +++ b/src/libslic3r/MeshBoolean.cpp @@ -1,3 +1,4 @@ +#include "Exception.hpp" #include "MeshBoolean.hpp" #include "libslic3r/TriangleMesh.hpp" #undef PI @@ -136,7 +137,7 @@ template void triangle_mesh_to_cgal(const TriangleMesh &M, _Mesh &o if(CGAL::is_closed(out)) CGALProc::orient_to_bound_a_volume(out); else - std::runtime_error("Mesh not watertight"); + throw Slic3r::RuntimeError("Mesh not watertight"); } inline Vec3d to_vec3d(const _EpicMesh::Point &v) @@ -222,7 +223,7 @@ template void _cgal_do(Op &&op, CGALMesh &A, CGALMesh &B) } if (! success) - throw std::runtime_error("CGAL mesh boolean operation failed."); + throw Slic3r::RuntimeError("CGAL mesh boolean operation failed."); } void minus(CGALMesh &A, CGALMesh &B) { _cgal_do(_cgal_diff, A, B); } diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 0719cac8cf..dc06e3102d 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1,7 +1,9 @@ +#include "Exception.hpp" #include "Model.hpp" #include "ModelArrange.hpp" #include "Geometry.hpp" #include "MTUtils.hpp" +#include "TriangleSelector.hpp" #include "Format/AMF.hpp" #include "Format/OBJ.hpp" @@ -20,7 +22,9 @@ #include "SVG.hpp" #include #include "GCodeWriter.hpp" +#if !ENABLE_GCODE_VIEWER #include "GCode/PreviewData.hpp" +#endif // !ENABLE_GCODE_VIEWER namespace Slic3r { @@ -113,13 +117,13 @@ Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* c else if (boost::algorithm::iends_with(input_file, ".prusa")) result = load_prus(input_file.c_str(), &model); else - throw std::runtime_error("Unknown file format. Input file must have .stl, .obj, .amf(.xml) or .prusa extension."); + throw Slic3r::RuntimeError("Unknown file format. Input file must have .stl, .obj, .amf(.xml) or .prusa extension."); if (! result) - throw std::runtime_error("Loading of a model file failed."); + throw Slic3r::RuntimeError("Loading of a model file failed."); if (model.objects.empty()) - throw std::runtime_error("The supplied file couldn't be read because it's empty"); + throw Slic3r::RuntimeError("The supplied file couldn't be read because it's empty"); for (ModelObject *o : model.objects) o->input_file = input_file; @@ -143,13 +147,13 @@ Model Model::read_from_archive(const std::string& input_file, DynamicPrintConfig else if (boost::algorithm::iends_with(input_file, ".zip.amf")) result = load_amf(input_file.c_str(), config, &model, check_version); else - throw std::runtime_error("Unknown file format. Input file must have .3mf or .zip.amf extension."); + throw Slic3r::RuntimeError("Unknown file format. Input file must have .3mf or .zip.amf extension."); if (!result) - throw std::runtime_error("Loading of a model file failed."); + throw Slic3r::RuntimeError("Loading of a model file failed."); if (model.objects.empty()) - throw std::runtime_error("The supplied file couldn't be read because it's empty"); + throw Slic3r::RuntimeError("The supplied file couldn't be read because it's empty"); for (ModelObject *o : model.objects) { @@ -814,7 +818,7 @@ const BoundingBoxf3& ModelObject::raw_bounding_box() const m_raw_bounding_box_valid = true; m_raw_bounding_box.reset(); if (this->instances.empty()) - throw std::invalid_argument("Can't call raw_bounding_box() with no instances"); + throw Slic3r::InvalidArgument("Can't call raw_bounding_box() with no instances"); const Transform3d& inst_matrix = this->instances.front()->get_transformation().get_matrix(true); for (const ModelVolume *v : this->volumes) @@ -1006,6 +1010,7 @@ void ModelObject::convert_units(ModelObjectPtrs& new_objects, bool from_imperial for (ModelVolume* volume : volumes) { volume->m_supported_facets.clear(); + volume->m_seam_facets.clear(); if (!volume->mesh().empty()) { TriangleMesh mesh(volume->mesh()); mesh.require_shared_vertices(); @@ -1111,6 +1116,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b const auto volume_matrix = volume->get_matrix(); volume->m_supported_facets.clear(); + volume->m_seam_facets.clear(); if (! volume->is_model_part()) { // Modifiers are not cut, but we still need to add the instance transformation @@ -1830,28 +1836,25 @@ arrangement::ArrangePolygon ModelInstance::get_arrange_polygon() const } -std::vector FacetsAnnotation::get_facets(FacetSupportType type) const +indexed_triangle_set FacetsAnnotation::get_facets(const ModelVolume& mv, EnforcerBlockerType type) const { - std::vector out; - for (auto& [facet_idx, this_type] : m_data) - if (this_type == type) - out.push_back(facet_idx); + TriangleSelector selector(mv.mesh()); + selector.deserialize(m_data); + indexed_triangle_set out = selector.get_facets(type); return out; } -void FacetsAnnotation::set_facet(int idx, FacetSupportType type) +bool FacetsAnnotation::set(const TriangleSelector& selector) { - bool changed = true; - - if (type == FacetSupportType::NONE) - changed = m_data.erase(idx) != 0; - else - m_data[idx] = type; - - if (changed) + std::map> sel_map = selector.serialize(); + if (sel_map != m_data) { + m_data = sel_map; update_timestamp(); + return true; + } + return false; } @@ -1864,6 +1867,64 @@ void FacetsAnnotation::clear() +// Following function takes data from a triangle and encodes it as string +// of hexadecimal numbers (one digit per triangle). Used for 3MF export, +// changing it may break backwards compatibility !!!!! +std::string FacetsAnnotation::get_triangle_as_string(int triangle_idx) const +{ + std::string out; + + auto triangle_it = m_data.find(triangle_idx); + if (triangle_it != m_data.end()) { + const std::vector& code = triangle_it->second; + int offset = 0; + while (offset < int(code.size())) { + int next_code = 0; + for (int i=3; i>=0; --i) { + next_code = next_code << 1; + next_code |= int(code[offset + i]); + } + offset += 4; + + assert(next_code >=0 && next_code <= 15); + char digit = next_code < 10 ? next_code + '0' : (next_code-10)+'A'; + out.insert(out.begin(), digit); + } + } + return out; +} + + + +// Recover triangle splitting & state from string of hexadecimal values previously +// generated by get_triangle_as_string. Used to load from 3MF. +void FacetsAnnotation::set_triangle_from_string(int triangle_id, const std::string& str) +{ + assert(! str.empty()); + m_data[triangle_id] = std::vector(); // zero current state or create new + std::vector& code = m_data[triangle_id]; + + for (auto it = str.crbegin(); it != str.crend(); ++it) { + const char ch = *it; + int dec = 0; + if (ch >= '0' && ch<='9') + dec = int(ch - '0'); + else if (ch >='A' && ch <= 'F') + dec = 10 + int(ch - 'A'); + else + assert(false); + + // Convert to binary and append into code. + for (int i=0; i<4; ++i) { + code.insert(code.end(), bool(dec & (1 << i))); + } + } + + +} + + + // Test whether the two models contain the same number of ModelObjects with the same set of IDs // ordered in the same order. In that case it is not necessary to kill the background processing. bool model_object_list_equal(const Model &model_old, const Model &model_new) @@ -1935,7 +1996,17 @@ bool model_custom_supports_data_changed(const ModelObject& mo, const ModelObject return true; } return false; -}; +} + +bool model_custom_seam_data_changed(const ModelObject& mo, const ModelObject& mo_new) { + assert(! model_volume_list_changed(mo, mo_new, ModelVolumeType::MODEL_PART)); + assert(mo.volumes.size() == mo_new.volumes.size()); + for (size_t i=0; im_seam_facets.is_same_as(mo.volumes[i]->m_seam_facets)) + return true; + } + return false; +} extern bool model_has_multi_part_objects(const Model &model) { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index e5930fb8ac..a623f5cca0 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -39,6 +39,7 @@ class ModelVolume; class ModelWipeTower; class Print; class SLAPrint; +class TriangleSelector; namespace UndoRedo { class StackImpl; @@ -393,7 +394,8 @@ enum class ModelVolumeType : int { SUPPORT_BLOCKER, }; -enum class FacetSupportType : int8_t { +enum class EnforcerBlockerType : int8_t { + // Maximum is 3. The value is serialized in TriangleSelector into 2 bits! NONE = 0, ENFORCER = 1, BLOCKER = 2 @@ -403,9 +405,12 @@ class FacetsAnnotation { public: using ClockType = std::chrono::steady_clock; - std::vector get_facets(FacetSupportType type) const; - void set_facet(int idx, FacetSupportType type); + const std::map>& get_data() const { return m_data; } + bool set(const TriangleSelector& selector); + indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const; void clear(); + std::string get_triangle_as_string(int i) const; + void set_triangle_from_string(int triangle_id, const std::string& str); ClockType::time_point get_timestamp() const { return timestamp; } bool is_same_as(const FacetsAnnotation& other) const { @@ -418,7 +423,7 @@ public: } private: - std::map m_data; + std::map> m_data; ClockType::time_point timestamp; void update_timestamp() { @@ -459,6 +464,9 @@ public: // List of mesh facets to be supported/unsupported. FacetsAnnotation m_supported_facets; + // List of seam enforcers/blockers. + FacetsAnnotation m_seam_facets; + // A parent object owning this modifier volume. ModelObject* get_object() const { return this->object; } ModelVolumeType type() const { return m_type; } @@ -588,7 +596,7 @@ private: ObjectBase(other), 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), - m_supported_facets(other.m_supported_facets) + m_supported_facets(other.m_supported_facets), m_seam_facets(other.m_seam_facets) { assert(this->id().valid()); assert(this->config.id().valid()); assert(this->id() != this->config.id()); assert(this->id() == other.id() && this->config.id() == other.config.id()); @@ -607,6 +615,7 @@ private: assert(this->config.id().valid()); assert(this->config.id() != other.config.id()); assert(this->id() != this->config.id()); m_supported_facets.clear(); + m_seam_facets.clear(); } ModelVolume& operator=(ModelVolume &rhs) = delete; @@ -620,7 +629,7 @@ private: template void load(Archive &ar) { bool has_convex_hull; ar(name, source, m_mesh, m_type, m_material_id, m_transformation, - m_is_splittable, has_convex_hull, m_supported_facets); + m_is_splittable, has_convex_hull, m_supported_facets, m_seam_facets); cereal::load_by_value(ar, config); assert(m_mesh); if (has_convex_hull) { @@ -634,7 +643,7 @@ 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_supported_facets); + m_is_splittable, has_convex_hull, m_supported_facets, m_seam_facets); cereal::save_by_value(ar, config); if (has_convex_hull) cereal::save_optional(ar, m_convex_hull); @@ -899,6 +908,10 @@ extern bool model_volume_list_changed(const ModelObject &model_object_old, const // The function assumes that volumes list is synchronized. extern bool model_custom_supports_data_changed(const ModelObject& mo, const ModelObject& mo_new); +// Test whether the now ModelObject has newer custom seam data than the old one. +// The function assumes that volumes list is synchronized. +extern bool model_custom_seam_data_changed(const ModelObject& mo, const ModelObject& mo_new); + // If the model has multi-part objects, then it is currently not supported by the SLA mode. // Either the model cannot be loaded, or a SLA printer has to be activated. extern bool model_has_multi_part_objects(const Model &model); diff --git a/src/libslic3r/ModelArrange.hpp b/src/libslic3r/ModelArrange.hpp index afe146d438..124c5c018f 100644 --- a/src/libslic3r/ModelArrange.hpp +++ b/src/libslic3r/ModelArrange.hpp @@ -20,7 +20,7 @@ using VirtualBedFn = std::function; [[noreturn]] inline void throw_if_out_of_bed(arrangement::ArrangePolygon&) { - throw std::runtime_error("Objects could not fit on the bed"); + throw Slic3r::RuntimeError("Objects could not fit on the bed"); } ArrangePolygons get_arrange_polys(const Model &model, ModelInstancePtrs &instances); diff --git a/src/libslic3r/ObjectID.hpp b/src/libslic3r/ObjectID.hpp index 484d1173ba..920f512de3 100644 --- a/src/libslic3r/ObjectID.hpp +++ b/src/libslic3r/ObjectID.hpp @@ -42,6 +42,8 @@ private: // Base for Model, ModelObject, ModelVolume, ModelInstance or ModelMaterial to provide a unique ID // to synchronize the front end (UI) with the back end (BackgroundSlicingProcess / Print / PrintObject). +// Also base for Print, PrintObject, SLAPrint, SLAPrintObject to provide a unique ID for matching Model / ModelObject +// with their corresponding Print / PrintObject objects by the notification center at the UI when processing back-end warnings. // Achtung! The s_last_id counter is not thread safe, so it is expected, that the ObjectBase derived instances // are only instantiated from the main thread. class ObjectBase diff --git a/src/libslic3r/OpenVDBUtils.cpp b/src/libslic3r/OpenVDBUtils.cpp index c30052036e..53a71f194f 100644 --- a/src/libslic3r/OpenVDBUtils.cpp +++ b/src/libslic3r/OpenVDBUtils.cpp @@ -2,6 +2,7 @@ #include "OpenVDBUtils.hpp" #include #include +#include #include //#include "MTUtils.hpp" @@ -57,17 +58,42 @@ void Contour3DDataAdapter::getIndexSpacePoint(size_t n, // TODO: Do I need to call initialize? Seems to work without it as well but the // docs say it should be called ones. It does a mutex lock-unlock sequence all // even if was called previously. - openvdb::FloatGrid::Ptr mesh_to_grid(const TriangleMesh &mesh, const openvdb::math::Transform &tr, float exteriorBandWidth, float interiorBandWidth, int flags) { +// openvdb::initialize(); +// return openvdb::tools::meshToVolume( +// TriangleMeshDataAdapter{mesh}, tr, exteriorBandWidth, +// interiorBandWidth, flags); + openvdb::initialize(); - return openvdb::tools::meshToVolume( - TriangleMeshDataAdapter{mesh}, tr, exteriorBandWidth, - interiorBandWidth, flags); + + TriangleMeshPtrs meshparts = mesh.split(); + + auto it = std::remove_if(meshparts.begin(), meshparts.end(), + [](TriangleMesh *m){ + m->require_shared_vertices(); + return !m->is_manifold() || m->volume() < EPSILON; + }); + + meshparts.erase(it, meshparts.end()); + + openvdb::FloatGrid::Ptr grid; + for (TriangleMesh *m : meshparts) { + auto gridptr = openvdb::tools::meshToVolume( + TriangleMeshDataAdapter{*m}, tr, exteriorBandWidth, + interiorBandWidth, flags); + + if (grid && gridptr) openvdb::tools::csgUnion(*grid, *gridptr); + else if (gridptr) grid = std::move(gridptr); + } + + grid = openvdb::tools::levelSetRebuild(*grid, 0., exteriorBandWidth, interiorBandWidth); + + return grid; } openvdb::FloatGrid::Ptr mesh_to_grid(const sla::Contour3D &mesh, diff --git a/src/libslic3r/OpenVDBUtils.hpp b/src/libslic3r/OpenVDBUtils.hpp index c493845a1c..e35231d35b 100644 --- a/src/libslic3r/OpenVDBUtils.hpp +++ b/src/libslic3r/OpenVDBUtils.hpp @@ -2,7 +2,6 @@ #define OPENVDBUTILS_HPP #include -#include #include #include diff --git a/src/libslic3r/Optimize/BruteforceOptimizer.hpp b/src/libslic3r/Optimize/BruteforceOptimizer.hpp new file mode 100644 index 0000000000..2daef538e7 --- /dev/null +++ b/src/libslic3r/Optimize/BruteforceOptimizer.hpp @@ -0,0 +1,140 @@ +#ifndef BRUTEFORCEOPTIMIZER_HPP +#define BRUTEFORCEOPTIMIZER_HPP + +#include + +namespace Slic3r { namespace opt { + +namespace detail { +// Implementing a bruteforce optimizer + +// Return the number of iterations needed to reach a specific grid position (idx) +template +long num_iter(const std::array &idx, size_t gridsz) +{ + long ret = 0; + for (size_t i = 0; i < N; ++i) ret += idx[i] * std::pow(gridsz, i); + return ret; +} + +// Implementation of a grid search where the search interval is sampled in +// equidistant points for each dimension. Grid size determines the number of +// samples for one dimension so the number of function calls is gridsize ^ dimension. +struct AlgBurteForce { + bool to_min; + StopCriteria stc; + size_t gridsz; + + AlgBurteForce(const StopCriteria &cr, size_t gs): stc{cr}, gridsz{gs} {} + + // This function is called recursively for each dimension and generates + // the grid values for the particular dimension. If D is less than zero, + // the object function input values are generated for each dimension and it + // can be evaluated. The current best score is compared with the newly + // returned score and changed appropriately. + template + bool run(std::array &idx, + Result &result, + const Bounds &bounds, + Fn &&fn, + Cmp &&cmp) + { + if (stc.stop_condition()) return false; + + if constexpr (D < 0) { // Let's evaluate fn + Input inp; + + auto max_iter = stc.max_iterations(); + if (max_iter && num_iter(idx, gridsz) >= max_iter) + return false; + + for (size_t d = 0; d < N; ++d) { + const Bound &b = bounds[d]; + double step = (b.max() - b.min()) / (gridsz - 1); + inp[d] = b.min() + idx[d] * step; + } + + auto score = fn(inp); + if (cmp(score, result.score)) { // Change current score to the new + double absdiff = std::abs(score - result.score); + + result.score = score; + result.optimum = inp; + + // Check if the required precision is reached. + if (absdiff < stc.abs_score_diff() || + absdiff < stc.rel_score_diff() * std::abs(score)) + return false; + } + + } else { + for (size_t i = 0; i < gridsz; ++i) { + idx[D] = i; // Mark the current grid position and dig down + if (!run(idx, result, bounds, std::forward(fn), + std::forward(cmp))) + return false; + } + } + + return true; + } + + template + Result optimize(Fn&& fn, + const Input &/*initvals*/, + const Bounds& bounds) + { + std::array idx = {}; + Result result; + + if (to_min) { + result.score = std::numeric_limits::max(); + run(idx, result, bounds, std::forward(fn), + std::less{}); + } + else { + result.score = std::numeric_limits::lowest(); + run(idx, result, bounds, std::forward(fn), + std::greater{}); + } + + return result; + } +}; + +} // namespace detail + +using AlgBruteForce = detail::AlgBurteForce; + +template<> +class Optimizer { + AlgBruteForce m_alg; + +public: + + Optimizer(const StopCriteria &cr = {}, size_t gridsz = 100) + : m_alg{cr, gridsz} + {} + + Optimizer& to_max() { m_alg.to_min = false; return *this; } + Optimizer& to_min() { m_alg.to_min = true; return *this; } + + template + Result optimize(Func&& func, + const Input &initvals, + const Bounds& bounds) + { + return m_alg.optimize(std::forward(func), initvals, bounds); + } + + Optimizer &set_criteria(const StopCriteria &cr) + { + m_alg.stc = cr; return *this; + } + + const StopCriteria &get_criteria() const { return m_alg.stc; } +}; + +}} // namespace Slic3r::opt + +#endif // BRUTEFORCEOPTIMIZER_HPP diff --git a/src/libslic3r/Optimize/NLoptOptimizer.hpp b/src/libslic3r/Optimize/NLoptOptimizer.hpp new file mode 100644 index 0000000000..826b1632ae --- /dev/null +++ b/src/libslic3r/Optimize/NLoptOptimizer.hpp @@ -0,0 +1,233 @@ +#ifndef NLOPTOPTIMIZER_HPP +#define NLOPTOPTIMIZER_HPP + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4244) +#pragma warning(disable: 4267) +#endif +#include +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#include + +#include + +namespace Slic3r { namespace opt { + +namespace detail { + +// Helper types for NLopt algorithm selection in template contexts +template struct NLoptAlg {}; + +// NLopt can combine multiple algorithms if one is global an other is a local +// method. This is how template specializations can be informed about this fact. +template +struct NLoptAlgComb {}; + +template struct IsNLoptAlg { + static const constexpr bool value = false; +}; + +template struct IsNLoptAlg> { + static const constexpr bool value = true; +}; + +template +struct IsNLoptAlg> { + static const constexpr bool value = true; +}; + +template +using NLoptOnly = std::enable_if_t::value, T>; + + +enum class OptDir { MIN, MAX }; // Where to optimize + +struct NLopt { // Helper RAII class for nlopt_opt + nlopt_opt ptr = nullptr; + + template explicit NLopt(A&&...a) + { + ptr = nlopt_create(std::forward(a)...); + } + + NLopt(const NLopt&) = delete; + NLopt(NLopt&&) = delete; + NLopt& operator=(const NLopt&) = delete; + NLopt& operator=(NLopt&&) = delete; + + ~NLopt() { nlopt_destroy(ptr); } +}; + +template class NLoptOpt {}; + +// Optimizers based on NLopt. +template class NLoptOpt> { +protected: + StopCriteria m_stopcr; + OptDir m_dir; + + template using TOptData = + std::tuple*, NLoptOpt*, nlopt_opt>; + + template + static double optfunc(unsigned n, const double *params, + double *gradient, + void *data) + { + assert(n >= N); + + auto tdata = static_cast*>(data); + + if (std::get<1>(*tdata)->m_stopcr.stop_condition()) + nlopt_force_stop(std::get<2>(*tdata)); + + auto fnptr = std::get<0>(*tdata); + auto funval = to_arr(params); + + double scoreval = 0.; + using RetT = decltype((*fnptr)(funval)); + if constexpr (std::is_convertible_v>) { + ScoreGradient score = (*fnptr)(funval); + for (size_t i = 0; i < n; ++i) gradient[i] = (*score.gradient)[i]; + scoreval = score.score; + } else { + scoreval = (*fnptr)(funval); + } + + return scoreval; + } + + template + void set_up(NLopt &nl, const Bounds& bounds) + { + std::array lb, ub; + + for (size_t i = 0; i < N; ++i) { + lb[i] = bounds[i].min(); + ub[i] = bounds[i].max(); + } + + nlopt_set_lower_bounds(nl.ptr, lb.data()); + nlopt_set_upper_bounds(nl.ptr, ub.data()); + + double abs_diff = m_stopcr.abs_score_diff(); + double rel_diff = m_stopcr.rel_score_diff(); + double stopval = m_stopcr.stop_score(); + if(!std::isnan(abs_diff)) nlopt_set_ftol_abs(nl.ptr, abs_diff); + if(!std::isnan(rel_diff)) nlopt_set_ftol_rel(nl.ptr, rel_diff); + if(!std::isnan(stopval)) nlopt_set_stopval(nl.ptr, stopval); + + if(this->m_stopcr.max_iterations() > 0) + nlopt_set_maxeval(nl.ptr, this->m_stopcr.max_iterations()); + } + + template + Result optimize(NLopt &nl, Fn &&fn, const Input &initvals) + { + Result r; + + TOptData data = std::make_tuple(&fn, this, nl.ptr); + + switch(m_dir) { + case OptDir::MIN: + nlopt_set_min_objective(nl.ptr, optfunc, &data); break; + case OptDir::MAX: + nlopt_set_max_objective(nl.ptr, optfunc, &data); break; + } + + r.optimum = initvals; + r.resultcode = nlopt_optimize(nl.ptr, r.optimum.data(), &r.score); + + return r; + } + +public: + + template + Result optimize(Func&& func, + const Input &initvals, + const Bounds& bounds) + { + NLopt nl{alg, N}; + set_up(nl, bounds); + + return optimize(nl, std::forward(func), initvals); + } + + explicit NLoptOpt(StopCriteria stopcr = {}) : m_stopcr(stopcr) {} + + void set_criteria(const StopCriteria &cr) { m_stopcr = cr; } + const StopCriteria &get_criteria() const noexcept { return m_stopcr; } + void set_dir(OptDir dir) noexcept { m_dir = dir; } + + void seed(long s) { nlopt_srand(s); } +}; + +template +class NLoptOpt>: public NLoptOpt> +{ + using Base = NLoptOpt>; +public: + + template + Result optimize(Fn&& f, + const Input &initvals, + const Bounds& bounds) + { + NLopt nl_glob{glob, N}, nl_loc{loc, N}; + + Base::set_up(nl_glob, bounds); + Base::set_up(nl_loc, bounds); + nlopt_set_local_optimizer(nl_glob.ptr, nl_loc.ptr); + + return Base::optimize(nl_glob, std::forward(f), initvals); + } + + explicit NLoptOpt(StopCriteria stopcr = {}) : Base{stopcr} {} +}; + +} // namespace detail; + +// Optimizers based on NLopt. +template class Optimizer> { + detail::NLoptOpt m_opt; + +public: + + Optimizer& to_max() { m_opt.set_dir(detail::OptDir::MAX); return *this; } + Optimizer& to_min() { m_opt.set_dir(detail::OptDir::MIN); return *this; } + + template + Result optimize(Func&& func, + const Input &initvals, + const Bounds& bounds) + { + return m_opt.optimize(std::forward(func), initvals, bounds); + } + + explicit Optimizer(StopCriteria stopcr = {}) : m_opt(stopcr) {} + + Optimizer &set_criteria(const StopCriteria &cr) + { + m_opt.set_criteria(cr); return *this; + } + + const StopCriteria &get_criteria() const { return m_opt.get_criteria(); } + + void seed(long s) { m_opt.seed(s); } +}; + +// Predefinded NLopt algorithms +using AlgNLoptGenetic = detail::NLoptAlgComb; +using AlgNLoptSubplex = detail::NLoptAlg; +using AlgNLoptSimplex = detail::NLoptAlg; +using AlgNLoptDIRECT = detail::NLoptAlg; +using AlgNLoptMLSL = detail::NLoptAlg; + +}} // namespace Slic3r::opt + +#endif // NLOPTOPTIMIZER_HPP diff --git a/src/libslic3r/Optimize/Optimizer.hpp b/src/libslic3r/Optimize/Optimizer.hpp new file mode 100644 index 0000000000..05191eba26 --- /dev/null +++ b/src/libslic3r/Optimize/Optimizer.hpp @@ -0,0 +1,182 @@ +#ifndef OPTIMIZER_HPP +#define OPTIMIZER_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace opt { + +// A type to hold the complete result of the optimization. +template struct Result { + int resultcode; // Method dependent + std::array optimum; + double score; +}; + +// An interval of possible input values for optimization +class Bound { + double m_min, m_max; + +public: + Bound(double min = std::numeric_limits::min(), + double max = std::numeric_limits::max()) + : m_min(min), m_max(max) + {} + + double min() const noexcept { return m_min; } + double max() const noexcept { return m_max; } +}; + +// Helper types for optimization function input and bounds +template using Input = std::array; +template using Bounds = std::array; + +// A type for specifying the stop criteria. Setter methods can be concatenated +class StopCriteria { + + // If the absolute value difference between two scores. + double m_abs_score_diff = std::nan(""); + + // If the relative value difference between two scores. + double m_rel_score_diff = std::nan(""); + + // Stop if this value or better is found. + double m_stop_score = std::nan(""); + + // A predicate that if evaluates to true, the optimization should terminate + // and the best result found prior to termination should be returned. + std::function m_stop_condition = [] { return false; }; + + // The max allowed number of iterations. + unsigned m_max_iterations = 0; + +public: + + StopCriteria & abs_score_diff(double val) + { + m_abs_score_diff = val; return *this; + } + + double abs_score_diff() const { return m_abs_score_diff; } + + StopCriteria & rel_score_diff(double val) + { + m_rel_score_diff = val; return *this; + } + + double rel_score_diff() const { return m_rel_score_diff; } + + StopCriteria & stop_score(double val) + { + m_stop_score = val; return *this; + } + + double stop_score() const { return m_stop_score; } + + StopCriteria & max_iterations(double val) + { + m_max_iterations = val; return *this; + } + + double max_iterations() const { return m_max_iterations; } + + template StopCriteria & stop_condition(Fn &&cond) + { + m_stop_condition = cond; return *this; + } + + bool stop_condition() { return m_stop_condition(); } +}; + +// Helper class to use optimization methods involving gradient. +template struct ScoreGradient { + double score; + std::optional> gradient; + + ScoreGradient(double s, const std::array &grad) + : score{s}, gradient{grad} + {} +}; + +// Helper to be used in static_assert. +template struct always_false { enum { value = false }; }; + +// Basic interface to optimizer object +template class Optimizer { +public: + + Optimizer(const StopCriteria &) + { + static_assert (always_false::value, + "Optimizer unimplemented for given method!"); + } + + // Switch optimization towards function minimum + Optimizer &to_min() { return *this; } + + // Switch optimization towards function maximum + Optimizer &to_max() { return *this; } + + // Set criteria for successive optimizations + Optimizer &set_criteria(const StopCriteria &) { return *this; } + + // Get current criteria + StopCriteria get_criteria() const { return {}; }; + + // Find function minimum or maximum for Func which has has signature: + // double(const Input &input) and input with dimension N + // + // Initial starting point can be given as the second parameter. + // + // For each dimension an interval (Bound) has to be given marking the bounds + // for that dimension. + // + // initvals have to be within the specified bounds, otherwise its undefined + // behavior. + // + // Func can return a score of type double or optionally a ScoreGradient + // class to indicate the function gradient for a optimization methods that + // make use of the gradient. + template + Result optimize(Func&& /*func*/, + const Input &/*initvals*/, + const Bounds& /*bounds*/) { return {}; } + + // optional for randomized methods: + void seed(long /*s*/) {} +}; + +namespace detail { + +// Helper to convert C style array to std::array. The copy should be optimized +// away with modern compilers. +template auto to_arr(const T *a) +{ + std::array r; + std::copy(a, a + N, std::begin(r)); + return r; +} + +template auto to_arr(const T (&a) [N]) +{ + return to_arr(static_cast(a)); +} + +} // namespace detail + +// Helper functions to create bounds, initial value +template Bounds bounds(const Bound (&b) [N]) { return detail::to_arr(b); } +template Input initvals(const double (&a) [N]) { return detail::to_arr(a); } +template auto score_gradient(double s, const double (&grad)[N]) +{ + return ScoreGradient(s, detail::to_arr(grad)); +} + +}} // namespace Slic3r::opt + +#endif // OPTIMIZER_HPP diff --git a/src/libslic3r/PNGRead.cpp b/src/libslic3r/PNGRead.cpp new file mode 100644 index 0000000000..e66143b845 --- /dev/null +++ b/src/libslic3r/PNGRead.cpp @@ -0,0 +1,100 @@ +#include "PNGRead.hpp" + +#include + +#include +#include + +namespace Slic3r { namespace png { + +struct PNGDescr { + png_struct *png = nullptr; png_info *info = nullptr; + + PNGDescr() = default; + PNGDescr(const PNGDescr&) = delete; + PNGDescr(PNGDescr&&) = delete; + PNGDescr& operator=(const PNGDescr&) = delete; + PNGDescr& operator=(PNGDescr&&) = delete; + + ~PNGDescr() + { + if (png && info) png_destroy_info_struct(png, &info); + if (png) png_destroy_read_struct( &png, nullptr, nullptr); + } +}; + +bool is_png(const ReadBuf &rb) +{ + static const constexpr int PNG_SIG_BYTES = 8; + +#if PNG_LIBPNG_VER_MINOR <= 2 + // Earlier libpng versions had png_sig_cmp(png_bytep, ...) which is not + // a const pointer. It is not possible to cast away the const qualifier from + // the input buffer so... yes... life is challenging... + png_byte buf[PNG_SIG_BYTES]; + auto inbuf = static_cast(rb.buf); + std::copy(inbuf, inbuf + PNG_SIG_BYTES, buf); +#else + auto buf = static_cast(rb.buf); +#endif + + return rb.sz >= PNG_SIG_BYTES && !png_sig_cmp(buf, 0, PNG_SIG_BYTES); +} + +// Buffer read callback for libpng. It provides an allocated output buffer and +// the amount of data it desires to read from the input. +void png_read_callback(png_struct *png_ptr, + png_bytep outBytes, + png_size_t byteCountToRead) +{ + // Retrieve our input buffer through the png_ptr + auto reader = static_cast(png_get_io_ptr(png_ptr)); + + if (!reader || !reader->is_ok()) return; + + reader->read(static_cast(outBytes), byteCountToRead); +} + +bool decode_png(IStream &in_buf, ImageGreyscale &out_img) +{ + static const constexpr int PNG_SIG_BYTES = 8; + + std::vector sig(PNG_SIG_BYTES, 0); + in_buf.read(sig.data(), PNG_SIG_BYTES); + if (!png_check_sig(sig.data(), PNG_SIG_BYTES)) + return false; + + PNGDescr dsc; + dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, + nullptr); + + if(!dsc.png) return false; + + dsc.info = png_create_info_struct(dsc.png); + if(!dsc.info) return false; + + png_set_read_fn(dsc.png, static_cast(&in_buf), png_read_callback); + + // Tell that we have already read the first bytes to check the signature + png_set_sig_bytes(dsc.png, PNG_SIG_BYTES); + + png_read_info(dsc.png, dsc.info); + + out_img.cols = png_get_image_width(dsc.png, dsc.info); + out_img.rows = png_get_image_height(dsc.png, dsc.info); + size_t color_type = png_get_color_type(dsc.png, dsc.info); + size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info); + + if (color_type != PNG_COLOR_TYPE_GRAY || bit_depth != 8) + return false; + + out_img.buf.resize(out_img.rows * out_img.cols); + + auto readbuf = static_cast(out_img.buf.data()); + for (size_t r = 0; r < out_img.rows; ++r) + png_read_row(dsc.png, readbuf + r * out_img.cols, nullptr); + + return true; +} + +}} // namespace Slic3r::png diff --git a/src/libslic3r/PNGRead.hpp b/src/libslic3r/PNGRead.hpp new file mode 100644 index 0000000000..082edd5691 --- /dev/null +++ b/src/libslic3r/PNGRead.hpp @@ -0,0 +1,70 @@ +#ifndef PNGREAD_HPP +#define PNGREAD_HPP + +#include +#include +#include + +namespace Slic3r { namespace png { + +// Interface for an input stream of encoded png image data. +struct IStream { + virtual ~IStream() = default; + virtual size_t read(std::uint8_t *outp, size_t amount) = 0; + virtual bool is_ok() const = 0; +}; + +// The output format of decode_png: a 2D pixel matrix stored continuously row +// after row (row major layout). +template struct Image { + std::vector buf; + size_t rows, cols; + PxT get(size_t row, size_t col) const { return buf[row * cols + col]; } +}; + +using ImageGreyscale = Image; + +// Only decodes true 8 bit grayscale png images. Returns false for other formats +// TODO (if needed): implement transformation of rgb images into grayscale... +bool decode_png(IStream &stream, ImageGreyscale &out_img); + +// TODO (if needed) +// struct RGB { uint8_t r, g, b; }; +// using ImageRGB = Image; +// bool decode_png(IStream &stream, ImageRGB &img); + + +// Encoded png data buffer: a simple read-only buffer and its size. +struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; }; + +bool is_png(const ReadBuf &pngbuf); + +template bool decode_png(const ReadBuf &in_buf, Img &out_img) +{ + struct ReadBufStream: public IStream { + const ReadBuf &rbuf_ref; size_t pos = 0; + + explicit ReadBufStream(const ReadBuf &buf): rbuf_ref{buf} {} + + size_t read(std::uint8_t *outp, size_t amount) override + { + if (amount > rbuf_ref.sz - pos) return 0; + + auto buf = static_cast(rbuf_ref.buf); + std::copy(buf + pos, buf + (pos + amount), outp); + pos += amount; + + return amount; + } + + bool is_ok() const override { return pos < rbuf_ref.sz; } + } stream{in_buf}; + + return decode_png(stream, out_img); +} + +// TODO: std::istream of FILE* could be similarly adapted in case its needed... + +}} // namespace Slic3r::png + +#endif // PNGREAD_HPP diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index 527d82b4cb..0434e3a0aa 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -1,4 +1,5 @@ #include "PlaceholderParser.hpp" +#include "Exception.hpp" #include "Flow.hpp" #include #include @@ -1303,7 +1304,7 @@ static std::string process_macro(const std::string &templ, client::MyContext &co if (!context.error_message.empty()) { if (context.error_message.back() != '\n' && context.error_message.back() != '\r') context.error_message += '\n'; - throw std::runtime_error(context.error_message); + throw Slic3r::RuntimeError(context.error_message); } return output; } @@ -1319,7 +1320,7 @@ std::string PlaceholderParser::process(const std::string &templ, unsigned int cu } // Evaluate a boolean expression using the full expressive power of the PlaceholderParser boolean expression syntax. -// Throws std::runtime_error on syntax or runtime error. +// Throws Slic3r::RuntimeError on syntax or runtime error. bool PlaceholderParser::evaluate_boolean_expression(const std::string &templ, const DynamicConfig &config, const DynamicConfig *config_override) { client::MyContext context; diff --git a/src/libslic3r/PlaceholderParser.hpp b/src/libslic3r/PlaceholderParser.hpp index d744dba220..14be020aca 100644 --- a/src/libslic3r/PlaceholderParser.hpp +++ b/src/libslic3r/PlaceholderParser.hpp @@ -40,11 +40,11 @@ public: const DynamicConfig* external_config() const { return m_external_config; } // Fill in the template using a macro processing language. - // Throws std::runtime_error on syntax or runtime error. + // Throws Slic3r::RuntimeError on syntax or runtime error. std::string process(const std::string &templ, unsigned int current_extruder_id = 0, const DynamicConfig *config_override = nullptr) const; // Evaluate a boolean expression using the full expressive power of the PlaceholderParser boolean expression syntax. - // Throws std::runtime_error on syntax or runtime error. + // Throws Slic3r::RuntimeError on syntax or runtime error. static bool evaluate_boolean_expression(const std::string &templ, const DynamicConfig &config, const DynamicConfig *config_override = nullptr); // Update timestamp, year, month, day, hour, minute, second variables at the provided config. diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index b818cd8bed..30a1a4942c 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -60,10 +60,13 @@ inline int64_t cross2(const Vec2i64 &v1, const Vec2i64 &v2) { return v1(0) * v2( inline float cross2(const Vec2f &v1, const Vec2f &v2) { return v1(0) * v2(1) - v1(1) * v2(0); } inline double cross2(const Vec2d &v1, const Vec2d &v2) { return v1(0) * v2(1) - v1(1) * v2(0); } -inline Vec2i32 to_2d(const Vec2i32 &pt3) { return Vec2i32(pt3(0), pt3(1)); } -inline Vec2i64 to_2d(const Vec3i64 &pt3) { return Vec2i64(pt3(0), pt3(1)); } -inline Vec2f to_2d(const Vec3f &pt3) { return Vec2f (pt3(0), pt3(1)); } -inline Vec2d to_2d(const Vec3d &pt3) { return Vec2d (pt3(0), pt3(1)); } +template Eigen::Matrix +to_2d(const Eigen::Matrix &ptN) { return {ptN(0), ptN(1)}; } + +//inline Vec2i32 to_2d(const Vec3i32 &pt3) { return Vec2i32(pt3(0), pt3(1)); } +//inline Vec2i64 to_2d(const Vec3i64 &pt3) { return Vec2i64(pt3(0), pt3(1)); } +//inline Vec2f to_2d(const Vec3f &pt3) { return Vec2f (pt3(0), pt3(1)); } +//inline Vec2d to_2d(const Vec3d &pt3) { return Vec2d (pt3(0), pt3(1)); } inline Vec3d to_3d(const Vec2d &v, double z) { return Vec3d(v(0), v(1), z); } inline Vec3f to_3d(const Vec2f &v, float z) { return Vec3f(v(0), v(1), z); } @@ -85,6 +88,8 @@ inline std::string to_string(const Vec3d &pt) { return std::string("[") + std: std::vector transform(const std::vector& points, const Transform3f& t); Pointf3s transform(const Pointf3s& points, const Transform3d& t); +template using Vec = Eigen::Matrix; + class Point : public Vec2crd { public: diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index 48e63dab31..fc83ae8d47 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -1,5 +1,6 @@ #include "BoundingBox.hpp" #include "ClipperUtils.hpp" +#include "Exception.hpp" #include "Polygon.hpp" #include "Polyline.hpp" @@ -16,7 +17,7 @@ Polyline Polygon::split_at_vertex(const Point &point) const for (const Point &pt : this->points) if (pt == point) return this->split_at_index(int(&pt - &this->points.front())); - throw std::invalid_argument("Point not found"); + throw Slic3r::InvalidArgument("Point not found"); return Polyline(); } diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index ab7c171e3c..48dd1b64dd 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -86,6 +86,14 @@ inline double total_length(const Polygons &polylines) { return total; } +inline double area(const Polygons &polys) +{ + double s = 0.; + for (auto &p : polys) s += p.area(); + + return s; +} + // Remove sticks (tentacles with zero area) from the polygon. extern bool remove_sticks(Polygon &poly); extern bool remove_sticks(Polygons &polys); diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp index 26aad83d2b..d24788c7bc 100644 --- a/src/libslic3r/Polyline.cpp +++ b/src/libslic3r/Polyline.cpp @@ -1,5 +1,6 @@ #include "BoundingBox.hpp" #include "Polyline.hpp" +#include "Exception.hpp" #include "ExPolygon.hpp" #include "ExPolygonCollection.hpp" #include "Line.hpp" @@ -19,7 +20,7 @@ Polyline::operator Polylines() const Polyline::operator Line() const { if (this->points.size() > 2) - throw std::invalid_argument("Can't convert polyline with more than two points to a line"); + throw Slic3r::InvalidArgument("Can't convert polyline with more than two points to a line"); return Line(this->points.front(), this->points.back()); } @@ -207,7 +208,7 @@ BoundingBox get_extents(const Polylines &polylines) const Point& leftmost_point(const Polylines &polylines) { if (polylines.empty()) - throw std::invalid_argument("leftmost_point() called on empty PolylineCollection"); + throw Slic3r::InvalidArgument("leftmost_point() called on empty PolylineCollection"); Polylines::const_iterator it = polylines.begin(); const Point *p = &it->leftmost_point(); for (++ it; it != polylines.end(); ++it) { diff --git a/src/slic3r/GUI/Preset.cpp b/src/libslic3r/Preset.cpp similarity index 76% rename from src/slic3r/GUI/Preset.cpp rename to src/libslic3r/Preset.cpp index d810c399d5..18a6e387cf 100644 --- a/src/slic3r/GUI/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1,10 +1,8 @@ #include +#include "Exception.hpp" #include "Preset.hpp" #include "AppConfig.hpp" -#include "BitmapCache.hpp" -#include "I18N.hpp" -#include "wxExtensions.hpp" #ifdef _MSC_VER #define WIN32_LEAN_AND_MEAN @@ -12,6 +10,16 @@ #include #endif /* _MSC_VER */ +// instead of #include "slic3r/GUI/I18N.hpp" : +#ifndef L +// !!! If you needed to translate some string, +// !!! please use _L(string) +// !!! _() - is a standard wxWidgets macro to translate +// !!! L() is used only for marking localizable string +// !!! It will be used in "xgettext" to create a Locating Message Catalog. +#define L(s) s +#endif /* L */ + #include #include #include @@ -19,6 +27,7 @@ #include #include #include +#include #include #include @@ -30,15 +39,9 @@ #include #include -#include -#include -#include -#include - -#include "libslic3r/libslic3r.h" -#include "libslic3r/Utils.hpp" -#include "libslic3r/PlaceholderParser.hpp" -#include "Plater.hpp" +#include "libslic3r.h" +#include "Utils.hpp" +#include "PlaceholderParser.hpp" using boost::property_tree::ptree; @@ -105,7 +108,7 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem const std::string id = path.stem().string(); if (! boost::filesystem::exists(path)) { - throw std::runtime_error((boost::format("Cannot load Vendor Config Bundle `%1%`: File not found: `%2%`.") % id % path).str()); + throw Slic3r::RuntimeError((boost::format("Cannot load Vendor Config Bundle `%1%`: File not found: `%2%`.") % id % path).str()); } VendorProfile res(id); @@ -115,7 +118,7 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem { auto res = tree.find(key); if (res == tree.not_found()) { - throw std::runtime_error((boost::format("Vendor Config Bundle `%1%` is not valid: Missing secion or key: `%2%`.") % id % key).str()); + throw Slic3r::RuntimeError((boost::format("Vendor Config Bundle `%1%` is not valid: Missing secion or key: `%2%`.") % id % key).str()); } return res; }; @@ -127,7 +130,7 @@ VendorProfile VendorProfile::from_ini(const ptree &tree, const boost::filesystem auto config_version_str = get_or_throw(vendor_section, "config_version")->second.data(); auto config_version = Semver::parse(config_version_str); if (! config_version) { - throw std::runtime_error((boost::format("Vendor Config Bundle `%1%` is not valid: Cannot parse config_version: `%2%`.") % id % config_version_str).str()); + throw Slic3r::RuntimeError((boost::format("Vendor Config Bundle `%1%` is not valid: Cannot parse config_version: `%2%`.") % id % config_version_str).str()); } else { res.config_version = std::move(*config_version); } @@ -245,9 +248,9 @@ const std::string& Preset::suffix_modified() return g_suffix_modified; } -void Preset::update_suffix_modified() +void Preset::update_suffix_modified(const std::string& new_suffix_modified) { - g_suffix_modified = (" (" + _(L("modified")) + ")").ToUTF8().data(); + g_suffix_modified = new_suffix_modified; } // Remove an optional "(modified)" suffix from a name. // This converts a UI name to a unique preset identifier. @@ -496,6 +499,7 @@ const std::vector& Preset::sla_print_options() "support_head_penetration", "support_head_width", "support_pillar_diameter", + "support_small_pillar_diameter_percent", "support_max_bridges_on_pillar", "support_pillar_connection_mode", "support_buildplate_only", @@ -590,10 +594,7 @@ const std::vector& Preset::sla_printer_options() PresetCollection::PresetCollection(Preset::Type type, const std::vector &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name) : m_type(type), m_edited_preset(type, "", false), - m_idx_selected(0), - m_bitmap_main_frame(new wxBitmap), - m_bitmap_add(new wxBitmap), - m_bitmap_cache(new GUI::BitmapCache) + m_idx_selected(0) { // Insert just the default preset. this->add_default_preset(keys, defaults, default_name); @@ -602,12 +603,6 @@ PresetCollection::PresetCollection(Preset::Type type, const std::vector &keys, // Throws an exception on error. void PresetCollection::load_presets(const std::string &dir_path, const std::string &subdir) { - boost::filesystem::path dir = boost::filesystem::canonical(boost::filesystem::path(dir_path) / subdir).make_preferred(); + // Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points, + // see https://github.com/prusa3d/PrusaSlicer/issues/732 + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(dir_path) / subdir).make_preferred(); m_dir_path = dir.string(); std::string errors_cummulative; // Store the loaded presets into a new vector, otherwise the binary search for already existing presets would be broken. @@ -676,9 +673,9 @@ void PresetCollection::load_presets(const std::string &dir_path, const std::stri preset.file << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; preset.loaded = true; } catch (const std::ifstream::failure &err) { - throw std::runtime_error(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what()); + throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what()); } catch (const std::runtime_error &err) { - throw std::runtime_error(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what()); + throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what()); } presets_loaded.emplace_back(preset); } catch (const std::runtime_error &err) { @@ -690,7 +687,7 @@ void PresetCollection::load_presets(const std::string &dir_path, const std::stri std::sort(m_presets.begin() + m_num_default_presets, m_presets.end()); this->select_preset(first_visible_idx()); if (! errors_cummulative.empty()) - throw std::runtime_error(errors_cummulative); + throw Slic3r::RuntimeError(errors_cummulative); } // Load a preset from an already parsed config file, insert it into the sorted sequence of presets @@ -951,16 +948,6 @@ bool PresetCollection::delete_preset(const std::string& name) return true; } -void PresetCollection::load_bitmap_default(const std::string &file_name) -{ - *m_bitmap_main_frame = create_scaled_bitmap(file_name); -} - -void PresetCollection::load_bitmap_add(const std::string &file_name) -{ - *m_bitmap_add = create_scaled_bitmap(file_name); -} - const Preset* PresetCollection::get_selected_preset_parent() const { if (this->get_selected_idx() == size_t(-1)) @@ -1119,279 +1106,15 @@ size_t PresetCollection::update_compatible_internal(const PresetWithVendorProfil // Delete the current preset, activate the first visible preset. //void PresetCollection::delete_current_preset(); -// Update the wxChoice UI component from this list of presets. -// Hide the -void PresetCollection::update_plater_ui(GUI::PresetComboBox *ui) -{ - if (ui == nullptr) - return; - - // Otherwise fill in the list from scratch. - ui->Freeze(); - ui->Clear(); - size_t selected_preset_item = INT_MAX; // some value meaning that no one item is selected - - const Preset &selected_preset = this->get_selected_preset(); - // Show wide icons if the currently selected preset is not compatible with the current printer, - // and draw a red flag in front of the selected preset. - bool wide_icons = ! selected_preset.is_compatible && m_bitmap_incompatible != nullptr; - - /* It's supposed that standard size of an icon is 16px*16px for 100% scaled display. - * So set sizes for solid_colored icons used for filament preset - * and scale them in respect to em_unit value - */ - const float scale_f = ui->em_unit() * 0.1f; - const int icon_height = 16 * scale_f + 0.5f; - const int icon_width = 16 * scale_f + 0.5f; - const int thin_space_icon_width = 4 * scale_f + 0.5f; - const int wide_space_icon_width = 6 * scale_f + 0.5f; - - std::map nonsys_presets; - wxString selected = ""; - wxString tooltip = ""; - if (!this->m_presets.front().is_visible) - ui->set_label_marker(ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap)); - for (size_t i = this->m_presets.front().is_visible ? 0 : m_num_default_presets; i < this->m_presets.size(); ++ i) { - const Preset &preset = this->m_presets[i]; - if (! preset.is_visible || (! preset.is_compatible && i != m_idx_selected)) - continue; - - std::string bitmap_key = ""; - // !!! Temporary solution, till refactoring: create and use "sla_printer" icon instead of m_bitmap_main_frame - wxBitmap main_bmp = m_bitmap_main_frame ? *m_bitmap_main_frame : wxNullBitmap; - if (m_type == Preset::TYPE_PRINTER && preset.printer_technology()==ptSLA ) { - bitmap_key = "sla_printer"; - main_bmp = create_scaled_bitmap("sla_printer"); - } - - // If the filament preset is not compatible and there is a "red flag" icon loaded, show it left - // to the filament color image. - if (wide_icons) - bitmap_key += preset.is_compatible ? ",cmpt" : ",ncmpt"; - bitmap_key += (preset.is_system || preset.is_default) ? ",syst" : ",nsyst"; - wxBitmap *bmp = m_bitmap_cache->find(bitmap_key); - if (bmp == nullptr) { - // Create the bitmap with color bars. - std::vector bmps; - if (wide_icons) - // Paint a red flag for incompatible presets. - bmps.emplace_back(preset.is_compatible ? m_bitmap_cache->mkclear(icon_width, icon_height) : *m_bitmap_incompatible); - // Paint the color bars. - bmps.emplace_back(m_bitmap_cache->mkclear(thin_space_icon_width, icon_height)); - bmps.emplace_back(main_bmp); - // Paint a lock at the system presets. - bmps.emplace_back(m_bitmap_cache->mkclear(wide_space_icon_width, icon_height)); - bmps.emplace_back((preset.is_system || preset.is_default) ? *m_bitmap_lock : m_bitmap_cache->mkclear(icon_width, icon_height)); - bmp = m_bitmap_cache->insert(bitmap_key, bmps); - } - - const std::string name = preset.alias.empty() ? preset.name : preset.alias; - if (preset.is_default || preset.is_system) { - ui->Append(wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), - (bmp == 0) ? main_bmp : *bmp); - if (i == m_idx_selected || - // just in case: mark selected_preset_item as a first added element - selected_preset_item == INT_MAX) { - selected_preset_item = ui->GetCount() - 1; - tooltip = wxString::FromUTF8(preset.name.c_str()); - } - } - else - { - nonsys_presets.emplace(wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), bmp/*preset.is_compatible*/); - if (i == m_idx_selected) { - selected = wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? g_suffix_modified : "")).c_str()); - tooltip = wxString::FromUTF8(preset.name.c_str()); - } - } - if (i + 1 == m_num_default_presets) - ui->set_label_marker(ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap)); - } - if (!nonsys_presets.empty()) - { - ui->set_label_marker(ui->Append(PresetCollection::separator(L("User presets")), wxNullBitmap)); - for (std::map::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { - ui->Append(it->first, *it->second); - if (it->first == selected || - // just in case: mark selected_preset_item as a first added element - selected_preset_item == INT_MAX) - selected_preset_item = ui->GetCount() - 1; - } - } - if (m_type == Preset::TYPE_PRINTER || m_type == Preset::TYPE_SLA_MATERIAL) { - std::string bitmap_key = ""; - // If the filament preset is not compatible and there is a "red flag" icon loaded, show it left - // to the filament color image. - if (wide_icons) - bitmap_key += "wide,"; - bitmap_key += "edit_preset_list"; - wxBitmap *bmp = m_bitmap_cache->find(bitmap_key); - if (bmp == nullptr) { - // Create the bitmap with color bars. - std::vector bmps; - if (wide_icons) - // Paint a red flag for incompatible presets. - bmps.emplace_back(m_bitmap_cache->mkclear(icon_width, icon_height)); - // Paint the color bars. - bmps.emplace_back(m_bitmap_cache->mkclear(thin_space_icon_width, icon_height)); - bmps.emplace_back(*m_bitmap_main_frame); - // Paint a lock at the system presets. - bmps.emplace_back(m_bitmap_cache->mkclear(wide_space_icon_width, icon_height)); -// bmps.emplace_back(m_bitmap_add ? *m_bitmap_add : wxNullBitmap); - bmps.emplace_back(create_scaled_bitmap("edit_uni")); - bmp = m_bitmap_cache->insert(bitmap_key, bmps); - } - if (m_type == Preset::TYPE_SLA_MATERIAL) - ui->set_label_marker(ui->Append(PresetCollection::separator(L("Add/Remove materials")), *bmp), GUI::PresetComboBox::LABEL_ITEM_WIZARD_MATERIALS); - else - ui->set_label_marker(ui->Append(PresetCollection::separator(L("Add/Remove printers")), *bmp), GUI::PresetComboBox::LABEL_ITEM_WIZARD_PRINTERS); - } - - /* But, if selected_preset_item is still equal to INT_MAX, it means that - * there is no presets added to the list. - * So, select last combobox item ("Add/Remove preset") - */ - if (selected_preset_item == INT_MAX) - selected_preset_item = ui->GetCount() - 1; - - ui->SetSelection(selected_preset_item); - ui->SetToolTip(tooltip.IsEmpty() ? ui->GetString(selected_preset_item) : tooltip); - ui->check_selection(selected_preset_item); - ui->Thaw(); - - // Update control min size after rescale (changed Display DPI under MSW) - if (ui->GetMinWidth() != 20 * ui->em_unit()) - ui->SetMinSize(wxSize(20 * ui->em_unit(), ui->GetSize().GetHeight())); -} - -size_t PresetCollection::update_tab_ui(wxBitmapComboBox *ui, bool show_incompatible, const int em/* = 10*/) -{ - if (ui == nullptr) - return 0; - ui->Freeze(); - ui->Clear(); - size_t selected_preset_item = INT_MAX; // some value meaning that no one item is selected - - /* It's supposed that standard size of an icon is 16px*16px for 100% scaled display. - * So set sizes for solid_colored(empty) icons used for preset - * and scale them in respect to em_unit value - */ - const float scale_f = em * 0.1f; - const int icon_height = 16 * scale_f + 0.5f; - const int icon_width = 16 * scale_f + 0.5f; - - std::map nonsys_presets; - wxString selected = ""; - if (!this->m_presets.front().is_visible) - ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap); - for (size_t i = this->m_presets.front().is_visible ? 0 : m_num_default_presets; i < this->m_presets.size(); ++i) { - const Preset &preset = this->m_presets[i]; - if (! preset.is_visible || (! show_incompatible && ! preset.is_compatible && i != m_idx_selected)) - continue; - std::string bitmap_key = "tab"; - - // !!! Temporary solution, till refactoring: create and use "sla_printer" icon instead of m_bitmap_main_frame - wxBitmap main_bmp = m_bitmap_main_frame ? *m_bitmap_main_frame : wxNullBitmap; - if (m_type == Preset::TYPE_PRINTER && preset.printer_technology() == ptSLA) { - bitmap_key = "sla_printer"; - main_bmp = create_scaled_bitmap("sla_printer"); - } - - bitmap_key += preset.is_compatible ? ",cmpt" : ",ncmpt"; - bitmap_key += (preset.is_system || preset.is_default) ? ",syst" : ",nsyst"; - wxBitmap *bmp = m_bitmap_cache->find(bitmap_key); - if (bmp == nullptr) { - // Create the bitmap with color bars. - std::vector bmps; - const wxBitmap* tmp_bmp = preset.is_compatible ? m_bitmap_compatible : m_bitmap_incompatible; - bmps.emplace_back((tmp_bmp == 0) ? main_bmp : *tmp_bmp); - // Paint a lock at the system presets. - bmps.emplace_back((preset.is_system || preset.is_default) ? *m_bitmap_lock : m_bitmap_cache->mkclear(icon_width, icon_height)); - bmp = m_bitmap_cache->insert(bitmap_key, bmps); - } - - if (preset.is_default || preset.is_system) { - ui->Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), - (bmp == 0) ? main_bmp : *bmp); - if (i == m_idx_selected || - // just in case: mark selected_preset_item as a first added element - selected_preset_item == INT_MAX) - selected_preset_item = ui->GetCount() - 1; - } - else - { - nonsys_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()), bmp/*preset.is_compatible*/); - if (i == m_idx_selected) - selected = wxString::FromUTF8((preset.name + (preset.is_dirty ? g_suffix_modified : "")).c_str()); - } - if (i + 1 == m_num_default_presets) - ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap); - } - if (!nonsys_presets.empty()) - { - ui->Append(PresetCollection::separator(L("User presets")), wxNullBitmap); - for (std::map::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { - ui->Append(it->first, *it->second); - if (it->first == selected || - // just in case: mark selected_preset_item as a first added element - selected_preset_item == INT_MAX) - selected_preset_item = ui->GetCount() - 1; - } - } - if (m_type == Preset::TYPE_PRINTER) { - wxBitmap *bmp = m_bitmap_cache->find("edit_printer_list"); - if (bmp == nullptr) { - // Create the bitmap with color bars. - std::vector bmps; - bmps.emplace_back(*m_bitmap_main_frame); -// bmps.emplace_back(m_bitmap_add ? *m_bitmap_add : wxNullBitmap); - bmps.emplace_back(create_scaled_bitmap("edit_uni")); - bmp = m_bitmap_cache->insert("add_printer_tab", bmps); - } - ui->Append(PresetCollection::separator("Add a new printer"), *bmp); - } - - /* But, if selected_preset_item is still equal to INT_MAX, it means that - * there is no presets added to the list. - * So, select last combobox item ("Add/Remove preset") - */ - if (selected_preset_item == INT_MAX) - selected_preset_item = ui->GetCount() - 1; - - ui->SetSelection(selected_preset_item); - ui->SetToolTip(ui->GetString(selected_preset_item)); - ui->Thaw(); - return selected_preset_item; -} - -// Update a dirty floag of the current preset, update the labels of the UI component accordingly. +// Update a dirty flag of the current preset // Return true if the dirty flag changed. -bool PresetCollection::update_dirty_ui(wxBitmapComboBox *ui) +bool PresetCollection::update_dirty() { - wxWindowUpdateLocker noUpdates(ui); - // 1) Update the dirty flag of the current preset. bool was_dirty = this->get_selected_preset().is_dirty; bool is_dirty = current_is_dirty(); this->get_selected_preset().is_dirty = is_dirty; this->get_edited_preset().is_dirty = is_dirty; - // 2) Update the labels. - for (unsigned int ui_id = 0; ui_id < ui->GetCount(); ++ ui_id) { - std::string old_label = ui->GetString(ui_id).utf8_str().data(); - std::string preset_name = Preset::remove_suffix_modified(old_label); - const Preset *preset = this->find_preset(preset_name, false); -// The old_label could be the "----- system presets ------" or the "------- user presets --------" separator. -// assert(preset != nullptr); - if (preset != nullptr) { - std::string new_label = preset->is_dirty ? preset->name + g_suffix_modified : preset->name; - if (old_label != new_label) - ui->SetString(ui_id, wxString::FromUTF8(new_label.c_str())); - } - } -#ifdef __APPLE__ - // wxWidgets on OSX do not upload the text of the combo box line automatically. - // Force it to update by re-selecting. - ui->SetSelection(ui->GetSelection()); -#endif /* __APPLE __ */ + return was_dirty != is_dirty; } @@ -1605,16 +1328,6 @@ std::string PresetCollection::path_from_name(const std::string &new_name) const return (boost::filesystem::path(m_dir_path) / file_name).make_preferred().string(); } -void PresetCollection::clear_bitmap_cache() -{ - m_bitmap_cache->clear(); -} - -wxString PresetCollection::separator(const std::string &label) -{ - return wxString::FromUTF8(PresetCollection::separator_head()) + _(label) + wxString::FromUTF8(PresetCollection::separator_tail()); -} - const Preset& PrinterPresetCollection::default_preset_for(const DynamicPrintConfig &config) const { const ConfigOptionEnumGeneric *opt_printer_technology = config.opt("printer_technology"); @@ -1632,6 +1345,472 @@ const Preset* PrinterPresetCollection::find_by_model_id(const std::string &model return it != cend() ? &*it : nullptr; } +// ------------------------- +// *** PhysicalPrinter *** +// ------------------------- + +std::string PhysicalPrinter::separator() +{ + return " * "; +} + +const std::vector& PhysicalPrinter::printer_options() +{ + static std::vector s_opts; + if (s_opts.empty()) { + s_opts = { + "preset_name", + "printer_technology", + "printer_model", + "host_type", + "print_host", + "printhost_apikey", + "printhost_cafile", + "printhost_authorization_type", + // HTTP digest authentization (RFC 2617) + "printhost_user", + "printhost_password" + }; + } + return s_opts; +} + +const std::vector& PhysicalPrinter::print_host_options() +{ + static std::vector s_opts; + if (s_opts.empty()) { + s_opts = { + "print_host", + "printhost_apikey", + "printhost_cafile" + }; + } + return s_opts; +} + +std::vector PhysicalPrinter::presets_with_print_host_information(const PrinterPresetCollection& printer_presets) +{ + std::vector presets; + for (const Preset& preset : printer_presets) + if (has_print_host_information(preset.config)) + presets.emplace_back(preset.name); + + return presets; +} + +bool PhysicalPrinter::has_print_host_information(const DynamicPrintConfig& config) +{ + for (const std::string& opt : print_host_options()) + if (!config.opt_string(opt).empty()) + return true; + + return false; +} + +const std::set& PhysicalPrinter::get_preset_names() const +{ + return preset_names; +} + +bool PhysicalPrinter::has_empty_config() const +{ + return config.opt_string("print_host" ).empty() && + config.opt_string("printhost_apikey" ).empty() && + config.opt_string("printhost_cafile" ).empty() && + config.opt_string("printhost_user" ).empty() && + config.opt_string("printhost_password").empty(); +} + +void PhysicalPrinter::update_preset_names_in_config() +{ + if (!preset_names.empty()) { + std::string name; + for (auto el : preset_names) + name += el + ";"; + name.pop_back(); + config.set_key_value("preset_name", new ConfigOptionString(name)); + } +} + +void PhysicalPrinter::save(const std::string& file_name_from, const std::string& file_name_to) +{ + // rename the file + boost::nowide::rename(file_name_from.data(), file_name_to.data()); + this->file = file_name_to; + // save configuration + this->config.save(this->file); +} + +void PhysicalPrinter::update_from_preset(const Preset& preset) +{ + config.apply_only(preset.config, printer_options(), true); + // add preset names to the options list + auto ret = preset_names.emplace(preset.name); + update_preset_names_in_config(); +} + +void PhysicalPrinter::update_from_config(const DynamicPrintConfig& new_config) +{ + config.apply_only(new_config, printer_options(), false); + + std::string str = config.opt_string("preset_name"); + std::set values{}; + if (!str.empty()) { + boost::split(values, str, boost::is_any_of(";")); + for (const std::string& val : values) + preset_names.emplace(val); + } + preset_names = values; +} + +void PhysicalPrinter::reset_presets() +{ + return preset_names.clear(); +} + +bool PhysicalPrinter::add_preset(const std::string& preset_name) +{ + return preset_names.emplace(preset_name).second; +} + +bool PhysicalPrinter::delete_preset(const std::string& preset_name) +{ + return preset_names.erase(preset_name) > 0; +} + +PhysicalPrinter::PhysicalPrinter(const std::string& name, const DynamicPrintConfig &default_config, const Preset& preset) : + name(name), config(default_config) +{ + update_from_preset(preset); +} + +void PhysicalPrinter::set_name(const std::string& name) +{ + this->name = name; +} + +std::string PhysicalPrinter::get_full_name(std::string preset_name) const +{ + return name + separator() + preset_name; +} + +std::string PhysicalPrinter::get_short_name(std::string full_name) +{ + int pos = full_name.find(separator()); + if (pos > 0) + boost::erase_tail(full_name, full_name.length() - pos); + return full_name; +} + +std::string PhysicalPrinter::get_preset_name(std::string name) +{ + int pos = name.find(separator()); + boost::erase_head(name, pos + 3); + return Preset::remove_suffix_modified(name); +} + + +// ----------------------------------- +// *** PhysicalPrinterCollection *** +// ----------------------------------- + +PhysicalPrinterCollection::PhysicalPrinterCollection( const std::vector& keys) +{ + // Default config for a physical printer containing all key/value pairs of PhysicalPrinter::printer_options(). + for (const std::string &key : keys) { + const ConfigOptionDef *opt = print_config_def.get(key); + assert(opt); + assert(opt->default_value); + m_default_config.set_key_value(key, opt->default_value->clone()); + } +} + +// Load all printers found in dir_path. +// Throws an exception on error. +void PhysicalPrinterCollection::load_printers(const std::string& dir_path, const std::string& subdir) +{ + // Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points, + // see https://github.com/prusa3d/PrusaSlicer/issues/732 + boost::filesystem::path dir = boost::filesystem::absolute(boost::filesystem::path(dir_path) / subdir).make_preferred(); + m_dir_path = dir.string(); + std::string errors_cummulative; + // Store the loaded printers into a new vector, otherwise the binary search for already existing presets would be broken. + std::deque printers_loaded; + for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) + if (Slic3r::is_ini_file(dir_entry)) { + std::string name = dir_entry.path().filename().string(); + // Remove the .ini suffix. + name.erase(name.size() - 4); + if (this->find_printer(name, false)) { + // This happens when there's is a preset (most likely legacy one) with the same name as a system preset + // that's already been loaded from a bundle. + BOOST_LOG_TRIVIAL(warning) << "Printer already present, not loading: " << name; + continue; + } + try { + PhysicalPrinter printer(name, this->default_config()); + printer.file = dir_entry.path().string(); + // Load the preset file, apply preset values on top of defaults. + try { + DynamicPrintConfig config; + config.load_from_ini(printer.file); + printer.update_from_config(config); + printer.loaded = true; + } + catch (const std::ifstream::failure& err) { + throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + printer.file + "\n\tReason: " + err.what()); + } + catch (const std::runtime_error& err) { + throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + printer.file + "\n\tReason: " + err.what()); + } + printers_loaded.emplace_back(printer); + } + catch (const std::runtime_error& err) { + errors_cummulative += err.what(); + errors_cummulative += "\n"; + } + } + m_printers.insert(m_printers.end(), std::make_move_iterator(printers_loaded.begin()), std::make_move_iterator(printers_loaded.end())); + std::sort(m_printers.begin(), m_printers.end()); + if (!errors_cummulative.empty()) + throw Slic3r::RuntimeError(errors_cummulative); +} + +// if there is saved user presets, contains information about "Print Host upload", +// Create default printers with this presets +// Note! "Print Host upload" options will be cleared after physical printer creations +void PhysicalPrinterCollection::load_printers_from_presets(PrinterPresetCollection& printer_presets) +{ + int cnt=0; + for (Preset& preset: printer_presets) { + DynamicPrintConfig& config = preset.config; + const std::vector& options = PhysicalPrinter::print_host_options(); + + for(const std::string& option : options) { + if (!config.opt_string(option).empty()) { + // check if printer with those "Print Host upload" options already exist + PhysicalPrinter* existed_printer = find_printer_with_same_config(config); + if (existed_printer) + // just add preset for this printer + existed_printer->add_preset(preset.name); + else { + std::string new_printer_name = (boost::format("Printer %1%") % ++cnt ).str(); + while (find_printer(new_printer_name)) + new_printer_name = (boost::format("Printer %1%") % ++cnt).str(); + + // create new printer from this preset + PhysicalPrinter printer(new_printer_name, this->default_config(), preset); + printer.loaded = true; + save_printer(printer); + } + + // erase "Print Host upload" information from the preset + for (const std::string& opt : options) + config.opt_string(opt).clear(); + // save changes for preset + preset.save(); + + // update those changes for edited preset if it's equal to the preset + Preset& edited = printer_presets.get_edited_preset(); + if (preset.name == edited.name) { + for (const std::string& opt : options) + edited.config.opt_string(opt).clear(); + } + + break; + } + } + } +} + +PhysicalPrinter* PhysicalPrinterCollection::find_printer( const std::string& name, bool first_visible_if_not_found) +{ + auto it = this->find_printer_internal(name); + // Ensure that a temporary copy is returned if the preset found is currently selected. + return (it != m_printers.end() && it->name == name) ? &this->printer(it - m_printers.begin()) : + first_visible_if_not_found ? &this->printer(0) : nullptr; +} + +PhysicalPrinter* PhysicalPrinterCollection::find_printer_with_same_config(const DynamicPrintConfig& config) +{ + for (const PhysicalPrinter& printer :*this) { + bool is_equal = true; + for (const std::string& opt : PhysicalPrinter::print_host_options()) + if (is_equal && printer.config.opt_string(opt) != config.opt_string(opt)) + is_equal = false; + + if (is_equal) + return find_printer(printer.name); + } + return nullptr; +} + +// Generate a file path from a profile name. Add the ".ini" suffix if it is missing. +std::string PhysicalPrinterCollection::path_from_name(const std::string& new_name) const +{ + std::string file_name = boost::iends_with(new_name, ".ini") ? new_name : (new_name + ".ini"); + return (boost::filesystem::path(m_dir_path) / file_name).make_preferred().string(); +} + +void PhysicalPrinterCollection::save_printer(PhysicalPrinter& edited_printer, const std::string& renamed_from/* = ""*/) +{ + // controll and update preset_names in edited_printer config + edited_printer.update_preset_names_in_config(); + + std::string name = renamed_from.empty() ? edited_printer.name : renamed_from; + // 1) Find the printer with a new_name or create a new one, + // initialize it with the edited config. + auto it = this->find_printer_internal(name); + if (it != m_printers.end() && it->name == name) { + // Printer with the same name found. + // Overwriting an existing preset. + it->config = std::move(edited_printer.config); + it->name = edited_printer.name; + it->preset_names = edited_printer.preset_names; + } + else { + // Creating a new printer. + it = m_printers.insert(it, edited_printer); + } + assert(it != m_printers.end()); + + // 2) Save printer + PhysicalPrinter& printer = *it; + if (printer.file.empty()) + printer.file = this->path_from_name(printer.name); + + if (printer.file == this->path_from_name(printer.name)) + printer.save(); + else + // if printer was renamed, we should rename a file and than save the config + printer.save(printer.file, this->path_from_name(printer.name)); + + // update idx_selected + m_idx_selected = it - m_printers.begin(); +} + +bool PhysicalPrinterCollection::delete_printer(const std::string& name) +{ + auto it = this->find_printer_internal(name); + if (it == m_printers.end()) + return false; + + const PhysicalPrinter& printer = *it; + // Erase the preset file. + boost::nowide::remove(printer.file.c_str()); + m_printers.erase(it); + return true; +} + +bool PhysicalPrinterCollection::delete_selected_printer() +{ + if (!has_selection()) + return false; + const PhysicalPrinter& printer = this->get_selected_printer(); + + // Erase the preset file. + boost::nowide::remove(printer.file.c_str()); + // Remove the preset from the list. + m_printers.erase(m_printers.begin() + m_idx_selected); + // unselect all printers + unselect_printer(); + + return true; +} + +bool PhysicalPrinterCollection::delete_preset_from_printers( const std::string& preset_name) +{ + std::vector printers_for_delete; + for (PhysicalPrinter& printer : m_printers) { + if (printer.preset_names.size() == 1 && *printer.preset_names.begin() == preset_name) + printers_for_delete.emplace_back(printer.name); + else if (printer.delete_preset(preset_name)) + save_printer(printer); + } + + if (!printers_for_delete.empty()) + for (const std::string& printer_name : printers_for_delete) + delete_printer(printer_name); + + unselect_printer(); + return true; +} + +// Get list of printers which have more than one preset and "preset_name" preset is one of them +std::vector PhysicalPrinterCollection::get_printers_with_preset(const std::string& preset_name) +{ + std::vector printers; + + for (auto printer : m_printers) { + if (printer.preset_names.size() == 1) + continue; + if (printer.preset_names.find(preset_name) != printer.preset_names.end()) + printers.emplace_back(printer.name); + } + + return printers; +} + +// Get list of printers which has only "preset_name" preset +std::vector PhysicalPrinterCollection::get_printers_with_only_preset(const std::string& preset_name) +{ + std::vector printers; + + for (auto printer : m_printers) + if (printer.preset_names.size() == 1 && *printer.preset_names.begin() == preset_name) + printers.emplace_back(printer.name); + + return printers; +} + +std::string PhysicalPrinterCollection::get_selected_full_printer_name() const +{ + return (m_idx_selected == size_t(-1)) ? std::string() : this->get_selected_printer().get_full_name(m_selected_preset); +} + +void PhysicalPrinterCollection::select_printer(const std::string& full_name) +{ + std::string printer_name = PhysicalPrinter::get_short_name(full_name); + auto it = this->find_printer_internal(printer_name); + if (it == m_printers.end()) { + unselect_printer(); + return; + } + + // update idx_selected + m_idx_selected = it - m_printers.begin(); + + // update name of the currently selected preset + if (printer_name == full_name) + // use first preset in the list + m_selected_preset = *it->preset_names.begin(); + else + m_selected_preset = it->get_preset_name(full_name); +} + +void PhysicalPrinterCollection::select_printer(const PhysicalPrinter& printer) +{ + return select_printer(printer.name); +} + +bool PhysicalPrinterCollection::has_selection() const +{ + return m_idx_selected != size_t(-1); +} + +void PhysicalPrinterCollection::unselect_printer() +{ + m_idx_selected = size_t(-1); + m_selected_preset.clear(); +} + +bool PhysicalPrinterCollection::is_selected(PhysicalPrinterCollection::ConstIterator it, const std::string& preset_name) const +{ + return m_idx_selected == it - m_printers.begin() && + m_selected_preset == preset_name; +} + + namespace PresetUtils { const VendorProfile::PrinterModel* system_printer_model(const Preset &preset) { @@ -1646,6 +1825,26 @@ namespace PresetUtils { } return out; } + +#if ENABLE_GCODE_VIEWER + std::string system_printer_bed_model(const Preset& preset) + { + std::string out; + const VendorProfile::PrinterModel* pm = PresetUtils::system_printer_model(preset); + if (pm != nullptr && !pm->bed_model.empty()) + out = Slic3r::resources_dir() + "/profiles/" + preset.vendor->id + "/" + pm->bed_model; + return out; + } + + std::string system_printer_bed_texture(const Preset& preset) + { + std::string out; + const VendorProfile::PrinterModel* pm = PresetUtils::system_printer_model(preset); + if (pm != nullptr && !pm->bed_texture.empty()) + out = Slic3r::resources_dir() + "/profiles/" + preset.vendor->id + "/" + pm->bed_texture; + return out; + } +#endif // ENABLE_GCODE_VIEWER } // namespace PresetUtils } // namespace Slic3r diff --git a/src/slic3r/GUI/Preset.hpp b/src/libslic3r/Preset.hpp similarity index 72% rename from src/slic3r/GUI/Preset.hpp rename to src/libslic3r/Preset.hpp index dc00780918..ec11d59fae 100644 --- a/src/slic3r/GUI/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -8,27 +8,14 @@ #include #include -#include "libslic3r/libslic3r.h" -#include "libslic3r/PrintConfig.hpp" -#include "libslic3r/Semver.hpp" - -class wxBitmap; -class wxBitmapComboBox; -class wxChoice; -class wxItemContainer; -class wxString; -class wxWindow; +#include "PrintConfig.hpp" +#include "Semver.hpp" namespace Slic3r { class AppConfig; class PresetBundle; -namespace GUI { - class BitmapCache; - class PresetComboBox; -} - enum ConfigFileType { CONFIG_FILE_TYPE_UNKNOWN, @@ -236,7 +223,7 @@ public: static const std::vector& sla_material_options(); static const std::vector& sla_print_options(); - static void update_suffix_modified(); + static void update_suffix_modified(const std::string& new_suffix_modified); static const std::string& suffix_modified(); static std::string remove_suffix_modified(const std::string& name); static void normalize(DynamicPrintConfig &config); @@ -322,18 +309,6 @@ public: // returns true if the preset was deleted successfully. bool delete_preset(const std::string& name); - // Load default bitmap to be placed at the wxBitmapComboBox of a MainFrame. - void load_bitmap_default(const std::string &file_name); - - // Load "add new printer" bitmap to be placed at the wxBitmapComboBox of a MainFrame. - void load_bitmap_add(const std::string &file_name); - - // Compatible & incompatible marks, to be placed at the wxBitmapComboBox items. - void set_bitmap_compatible (const wxBitmap *bmp) { m_bitmap_compatible = bmp; } - void set_bitmap_incompatible(const wxBitmap *bmp) { m_bitmap_incompatible = bmp; } - void set_bitmap_lock (const wxBitmap *bmp) { m_bitmap_lock = bmp; } - void set_bitmap_lock_open (const wxBitmap *bmp) { m_bitmap_lock_open = bmp; } - // Enable / disable the "- default -" preset. void set_default_suppressed(bool default_suppressed); bool is_default_suppressed() const { return m_default_suppressed; } @@ -446,18 +421,9 @@ public: // Return a sorted list of system preset names. std::vector system_preset_names() const; - // Update the choice UI from the list of presets. - // If show_incompatible, all presets are shown, otherwise only the compatible presets are shown. - // If an incompatible preset is selected, it is shown as well. - size_t update_tab_ui(wxBitmapComboBox *ui, bool show_incompatible, const int em = 10); - // Update the choice UI from the list of presets. - // Only the compatible presets are shown. - // If an incompatible preset is selected, it is shown as well. - void update_plater_ui(GUI::PresetComboBox *ui); - - // Update a dirty floag of the current preset, update the labels of the UI component accordingly. + // Update a dirty flag of the current preset // Return true if the dirty flag changed. - bool update_dirty_ui(wxBitmapComboBox *ui); + bool update_dirty(); // Select a profile by its name. Return true if the selection changed. // Without force, the selection is only updated if the index changes. @@ -467,16 +433,7 @@ public: // Generate a file path from a profile name. Add the ".ini" suffix if it is missing. std::string path_from_name(const std::string &new_name) const; - void clear_bitmap_cache(); - -#ifdef __linux__ - static const char* separator_head() { return "------- "; } - static const char* separator_tail() { return " -------"; } -#else /* __linux__ */ - static const char* separator_head() { return "————— "; } - static const char* separator_tail() { return " —————"; } -#endif /* __linux__ */ - static wxString separator(const std::string &label); + size_t num_default_presets() { return m_num_default_presets; } protected: // Select a preset, if it exists. If it does not exist, select an invalid (-1) index. @@ -503,8 +460,7 @@ private: // If a preset does not exist, an iterator is returned indicating where to insert a preset with the same name. std::deque::iterator find_preset_internal(const std::string &name) { - Preset key(m_type, name); - auto it = std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key); + auto it = Slic3r::lower_bound_by_predicate(m_presets.begin() + m_num_default_presets, m_presets.end(), [&name](const auto& l) { return l.name < name; }); if (it == m_presets.end() || it->name != name) { // Preset has not been not found in the sorted list of non-default presets. Try the defaults. for (size_t i = 0; i < m_num_default_presets; ++ i) @@ -547,23 +503,10 @@ private: // Is the "- default -" preset suppressed? bool m_default_suppressed = true; size_t m_num_default_presets = 0; - // Compatible & incompatible marks, to be placed at the wxBitmapComboBox items of a Plater. - // These bitmaps are not owned by PresetCollection, but by a PresetBundle. - const wxBitmap *m_bitmap_compatible = nullptr; - const wxBitmap *m_bitmap_incompatible = nullptr; - const wxBitmap *m_bitmap_lock = nullptr; - const wxBitmap *m_bitmap_lock_open = nullptr; - // Marks placed at the wxBitmapComboBox of a MainFrame. - // These bitmaps are owned by PresetCollection. - wxBitmap *m_bitmap_main_frame; - // "Add printer profile" icon, owned by PresetCollection. - wxBitmap *m_bitmap_add; + // Path to the directory to store the config files into. std::string m_dir_path; - // Caching color bitmaps for the filament combo box. - GUI::BitmapCache *m_bitmap_cache = nullptr; - // to access select_preset_by_name_strict() friend class PresetBundle; }; @@ -583,8 +526,214 @@ public: namespace PresetUtils { // PrinterModel of a system profile, from which this preset is derived, or null if it is not derived from a system profile. const VendorProfile::PrinterModel* system_printer_model(const Preset &preset); +#if ENABLE_GCODE_VIEWER + std::string system_printer_bed_model(const Preset& preset); + std::string system_printer_bed_texture(const Preset& preset); +#endif // ENABLE_GCODE_VIEWER } // namespace PresetUtils + +////////////////////////////////////////////////////////////////////// + +class PhysicalPrinter +{ +public: + PhysicalPrinter(const std::string& name, const DynamicPrintConfig &default_config) : name(name), config(default_config) {} + PhysicalPrinter(const std::string& name, const DynamicPrintConfig &default_config, const Preset& preset); + void set_name(const std::string &name); + + // Name of the Physical Printer, usually derived form the file name. + std::string name; + // File name of the Physical Printer. + std::string file; + // Configuration data, loaded from a file, or set from the defaults. + DynamicPrintConfig config; + // set of presets used with this physical printer + std::set preset_names; + + // Has this profile been loaded? + bool loaded = false; + + static std::string separator(); + static const std::vector& printer_options(); + static const std::vector& print_host_options(); + static std::vector presets_with_print_host_information(const PrinterPresetCollection& printer_presets); + static bool has_print_host_information(const DynamicPrintConfig& config); + + const std::set& get_preset_names() const; + + bool has_empty_config() const; + void update_preset_names_in_config(); + + void save() { this->config.save(this->file); } + void save(const std::string& file_name_from, const std::string& file_name_to); + + void update_from_preset(const Preset& preset); + void update_from_config(const DynamicPrintConfig &new_config); + + // add preset to the preset_names + // return false, if preset with this name is already exist in the set + bool add_preset(const std::string& preset_name); + bool delete_preset(const std::string& preset_name); + void reset_presets(); + + // Return a printer technology, return ptFFF if the printer technology is not set. + static PrinterTechnology printer_technology(const DynamicPrintConfig& cfg) { + auto* opt = cfg.option>("printer_technology"); + // The following assert may trigger when importing some legacy profile, + // but it is safer to keep it here to capture the cases where the "printer_technology" key is queried, where it should not. + return (opt == nullptr) ? ptFFF : opt->value; + } + PrinterTechnology printer_technology() const { return printer_technology(this->config); } + + // Sort lexicographically by a preset name. The preset name shall be unique across a single PresetCollection. + bool operator<(const PhysicalPrinter& other) const { return this->name < other.name; } + + // get full printer name included a name of the preset + std::string get_full_name(std::string preset_name) const; + + // get printer name from the full name uncluded preset name + static std::string get_short_name(std::string full_name); + + // get preset name from the full name uncluded printer name + static std::string get_preset_name(std::string full_name); + +protected: + friend class PhysicalPrinterCollection; +}; + + +// --------------------------------- +// *** PhysicalPrinterCollection *** +// --------------------------------- + +// Collections of physical printers +class PhysicalPrinterCollection +{ +public: + PhysicalPrinterCollection(const std::vector& keys); + ~PhysicalPrinterCollection() {} + + typedef std::deque::iterator Iterator; + typedef std::deque::const_iterator ConstIterator; + Iterator begin() { return m_printers.begin(); } + ConstIterator begin() const { return m_printers.cbegin(); } + ConstIterator cbegin() const { return m_printers.cbegin(); } + Iterator end() { return m_printers.end(); } + ConstIterator end() const { return m_printers.cend(); } + ConstIterator cend() const { return m_printers.cend(); } + + bool empty() const {return m_printers.empty(); } + + void reset(bool delete_files) {}; + + const std::deque& operator()() const { return m_printers; } + + // Load ini files of the particular type from the provided directory path. + void load_printers(const std::string& dir_path, const std::string& subdir); + void load_printers_from_presets(PrinterPresetCollection &printer_presets); + + // Save the printer under a new name. If the name is different from the old one, + // a new printer is stored into the list of printers. + // New printer is activated. + void save_printer(PhysicalPrinter& printer, const std::string& renamed_from = ""); + + // Delete the current preset, activate the first visible preset. + // returns true if the preset was deleted successfully. + bool delete_printer(const std::string& name); + // Delete the selected preset + // returns true if the preset was deleted successfully. + bool delete_selected_printer(); + // Delete preset_name preset from all printers: + // If there is last preset for the printer and first_check== false, then delete this printer + // returns true if all presets were deleted successfully. + bool delete_preset_from_printers(const std::string& preset_name); + + // Get list of printers which have more than one preset and "preset_name" preset is one of them + std::vector get_printers_with_preset( const std::string &preset_name); + // Get list of printers which has only "preset_name" preset + std::vector get_printers_with_only_preset( const std::string &preset_name); + + // Return the selected preset, without the user modifications applied. + PhysicalPrinter& get_selected_printer() { return m_printers[m_idx_selected]; } + const PhysicalPrinter& get_selected_printer() const { return m_printers[m_idx_selected]; } + + size_t get_selected_idx() const { return m_idx_selected; } + // Returns the name of the selected preset, or an empty string if no preset is selected. + std::string get_selected_printer_name() const { return (m_idx_selected == size_t(-1)) ? std::string() : this->get_selected_printer().name; } + // Returns the config of the selected printer, or nullptr if no printer is selected. + DynamicPrintConfig* get_selected_printer_config() { return (m_idx_selected == size_t(-1)) ? nullptr : &(this->get_selected_printer().config); } + // Returns the config of the selected printer, or nullptr if no printer is selected. + PrinterTechnology get_selected_printer_technology() { return (m_idx_selected == size_t(-1)) ? PrinterTechnology::ptAny : this->get_selected_printer().printer_technology(); } + + // Each physical printer can have a several related preset, + // so, use the next functions to get an exact names of selections in the list: + // Returns the full name of the selected printer, or an empty string if no preset is selected. + std::string get_selected_full_printer_name() const; + // Returns the printer model of the selected preset, or an empty string if no preset is selected. + std::string get_selected_printer_preset_name() const { return (m_idx_selected == size_t(-1)) ? std::string() : m_selected_preset; } + + // Select printer by the full printer name, which contains name of printer, separator and name of selected preset + // If full_name doesn't contain name of selected preset, then select first preset in the list for this printer + void select_printer(const std::string& full_name); + void select_printer(const PhysicalPrinter& printer); + bool has_selection() const; + void unselect_printer() ; + bool is_selected(ConstIterator it, const std::string &preset_name) const; + + // Return a printer by an index. If the printer is active, a temporary copy is returned. + PhysicalPrinter& printer(size_t idx) { return m_printers[idx]; } + const PhysicalPrinter& printer(size_t idx) const { return const_cast(this)->printer(idx); } + + // Return a preset by its name. If the preset is active, a temporary copy is returned. + // If a preset is not found by its name, null is returned. + PhysicalPrinter* find_printer(const std::string& name, bool first_visible_if_not_found = false); + const PhysicalPrinter* find_printer(const std::string& name, bool first_visible_if_not_found = false) const + { + return const_cast(this)->find_printer(name, first_visible_if_not_found); + } + + // Generate a file path from a profile name. Add the ".ini" suffix if it is missing. + std::string path_from_name(const std::string& new_name) const; + + const DynamicPrintConfig& default_config() const { return m_default_config; } + +private: + PhysicalPrinterCollection& operator=(const PhysicalPrinterCollection& other); + + // Find a preset position in the sorted list of presets. + // The "-- default -- " preset is always the first, so it needs + // to be handled differently. + // If a preset does not exist, an iterator is returned indicating where to insert a preset with the same name. + std::deque::iterator find_printer_internal(const std::string& name) + { + return Slic3r::lower_bound_by_predicate(m_printers.begin(), m_printers.end(), [&name](const auto& l) { return l.name < name; }); + } + std::deque::const_iterator find_printer_internal(const std::string& name) const + { + return const_cast(this)->find_printer_internal(name); + } + + PhysicalPrinter* find_printer_with_same_config( const DynamicPrintConfig &config); + + // List of printers + // Use deque to force the container to allocate an object per each entry, + // so that the addresses of the presets don't change during resizing of the container. + std::deque m_printers; + + // Default config for a physical printer containing all key/value pairs of PhysicalPrinter::printer_options(). + DynamicPrintConfig m_default_config; + + // Selected printer. + size_t m_idx_selected = size_t(-1); + // The name of the preset which is currently select for this printer + std::string m_selected_preset; + + // Path to the directory to store the config files into. + std::string m_dir_path; +}; + + } // namespace Slic3r #endif /* slic3r_Preset_hpp_ */ diff --git a/src/slic3r/GUI/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp similarity index 85% rename from src/slic3r/GUI/PresetBundle.cpp rename to src/libslic3r/PresetBundle.cpp index ba806a0b2d..c100e6971a 100644 --- a/src/slic3r/GUI/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1,12 +1,12 @@ #include #include "PresetBundle.hpp" -#include "BitmapCache.hpp" -#include "Plater.hpp" -#include "I18N.hpp" -#include "wxExtensions.hpp" +#include "libslic3r.h" +#include "Utils.hpp" +#include "Model.hpp" #include +#include #include #include #include @@ -21,17 +21,6 @@ #include #include -#include -#include -#include -#include -#include - -#include "libslic3r/libslic3r.h" -#include "libslic3r/Utils.hpp" -#include "libslic3r/Model.hpp" -#include "GUI_App.hpp" - // Store the print/filament/printer presets into a "presets" subdirectory of the Slic3rPE config dir. // This breaks compatibility with the upstream Slic3r if the --datadir is used to switch between the two versions. @@ -53,15 +42,8 @@ PresetBundle::PresetBundle() : sla_materials(Preset::TYPE_SLA_MATERIAL, Preset::sla_material_options(), static_cast(SLAFullPrintConfig::defaults())), sla_prints(Preset::TYPE_SLA_PRINT, Preset::sla_print_options(), static_cast(SLAFullPrintConfig::defaults())), printers(Preset::TYPE_PRINTER, Preset::printer_options(), static_cast(FullPrintConfig::defaults()), "- default FFF -"), - m_bitmapCompatible(new wxBitmap), - m_bitmapIncompatible(new wxBitmap), - m_bitmapLock(new wxBitmap), - m_bitmapLockOpen(new wxBitmap), - m_bitmapCache(new GUI::BitmapCache) + physical_printers(PhysicalPrinter::printer_options()) { - if (wxImage::FindHandler(wxBITMAP_TYPE_PNG) == nullptr) - wxImage::AddHandler(new wxPNGHandler); - // The following keys are handled by the UI, they do not have a counterpart in any StaticPrintConfig derived classes, // therefore they need to be handled differently. As they have no counterpart in StaticPrintConfig, they are not being // initialized based on PrintConfigDef(), but to empty values (zeros, empty vectors, empty strings). @@ -112,16 +94,6 @@ PresetBundle::PresetBundle() : preset.inherits(); } - // Load the default preset bitmaps. - // #ys_FIXME_to_delete we'll load them later, using em_unit() -// this->prints .load_bitmap_default("cog"); -// this->sla_prints .load_bitmap_default("package_green.png"); -// this->filaments .load_bitmap_default("spool.png"); -// this->sla_materials.load_bitmap_default("package_green.png"); -// this->printers .load_bitmap_default("printer_empty.png"); -// this->printers .load_bitmap_add("add.png"); -// this->load_compatible_bitmaps(); - // Re-activate the default presets, so their "edited" preset copies will be updated with the additional configuration values above. this->prints .select_preset(0); this->sla_prints .select_preset(0); @@ -134,20 +106,6 @@ PresetBundle::PresetBundle() : PresetBundle::~PresetBundle() { - assert(m_bitmapCompatible != nullptr); - assert(m_bitmapIncompatible != nullptr); - assert(m_bitmapLock != nullptr); - assert(m_bitmapLockOpen != nullptr); - delete m_bitmapCompatible; - m_bitmapCompatible = nullptr; - delete m_bitmapIncompatible; - m_bitmapIncompatible = nullptr; - delete m_bitmapLock; - m_bitmapLock = nullptr; - delete m_bitmapLockOpen; - m_bitmapLockOpen = nullptr; - delete m_bitmapCache; - m_bitmapCache = nullptr; } void PresetBundle::reset(bool delete_files) @@ -182,14 +140,16 @@ void PresetBundle::setup_directories() data_dir / "presets" / "filament", data_dir / "presets" / "sla_print", data_dir / "presets" / "sla_material", - data_dir / "presets" / "printer" + data_dir / "presets" / "printer", + data_dir / "presets" / "physical_printer" #else // Store the print/filament/printer presets at the same location as the upstream Slic3r. data_dir / "print", data_dir / "filament", data_dir / "sla_print", data_dir / "sla_material", - data_dir / "printer" + data_dir / "printer", + data_dir / "physical_printer" #endif }; for (const boost::filesystem::path &path : paths) { @@ -197,7 +157,7 @@ void PresetBundle::setup_directories() subdir.make_preferred(); if (! boost::filesystem::is_directory(subdir) && ! boost::filesystem::create_directory(subdir)) - throw std::runtime_error(std::string("Slic3r was unable to create its data directory at ") + subdir.string()); + throw Slic3r::RuntimeError(std::string("Slic3r was unable to create its data directory at ") + subdir.string()); } } @@ -239,10 +199,15 @@ void PresetBundle::load_presets(AppConfig &config, const std::string &preferred_ } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } + try { + this->physical_printers.load_printers(dir_user_presets, "physical_printer"); + } catch (const std::runtime_error &err) { + errors_cummulative += err.what(); + } this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); if (! errors_cummulative.empty()) - throw std::runtime_error(errors_cummulative); + throw Slic3r::RuntimeError(errors_cummulative); this->load_selections(config, preferred_model_id); } @@ -362,6 +327,32 @@ const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& p return presets.get_preset_name_by_alias(alias); } +void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset::Type type, + const std::vector& unselected_options) +{ + PresetCollection& presets = type == Preset::TYPE_PRINT ? prints : + type == Preset::TYPE_SLA_PRINT ? sla_prints : + type == Preset::TYPE_FILAMENT ? filaments : + type == Preset::TYPE_SLA_MATERIAL ? sla_materials : printers; + + // if we want to save just some from selected options + if (!unselected_options.empty()) { + // revert unselected options to the old values + presets.get_edited_preset().config.apply_only(presets.get_selected_preset().config, unselected_options); + } + + // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini + presets.save_current_preset(new_name); + // Mark the print & filament enabled if they are compatible with the currently selected preset. + // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible. + update_compatible(PresetSelectCompatibleType::Never); + + if (type == Preset::TYPE_FILAMENT) { + // synchronize the first filament presets. + set_filament_preset(0, filaments.get_selected_preset_name()); + } +} + void PresetBundle::load_installed_filaments(AppConfig &config) { if (! config.has_section(AppConfig::SECTION_FILAMENTS)) { @@ -465,6 +456,13 @@ void PresetBundle::load_selections(AppConfig &config, const std::string &preferr // exist. this->update_compatible(PresetSelectCompatibleType::Always); this->update_multi_material_filament_presets(); + + // Parse the initial physical printer name. + std::string initial_physical_printer_name = remove_ini_suffix(config.get("extras", "physical_printer")); + + // Activate physical printer from the config + if (!initial_physical_printer_name.empty()) + physical_printers.select_printer(initial_physical_printer_name); } // Export selections (current print, current filaments, current printer) into config.ini @@ -484,36 +482,8 @@ void PresetBundle::export_selections(AppConfig &config) config.set("presets", "sla_print", sla_prints.get_selected_preset_name()); config.set("presets", "sla_material", sla_materials.get_selected_preset_name()); config.set("presets", "printer", printers.get_selected_preset_name()); -} -void PresetBundle::load_compatible_bitmaps() -{ - *m_bitmapCompatible = create_scaled_bitmap("flag_green"); - *m_bitmapIncompatible = create_scaled_bitmap("flag_red"); - *m_bitmapLock = create_scaled_bitmap("lock_closed"); - *m_bitmapLockOpen = create_scaled_bitmap("lock_open"); - - prints .set_bitmap_compatible(m_bitmapCompatible); - filaments .set_bitmap_compatible(m_bitmapCompatible); - sla_prints .set_bitmap_compatible(m_bitmapCompatible); - sla_materials.set_bitmap_compatible(m_bitmapCompatible); - - prints .set_bitmap_incompatible(m_bitmapIncompatible); - filaments .set_bitmap_incompatible(m_bitmapIncompatible); - sla_prints .set_bitmap_incompatible(m_bitmapIncompatible); - sla_materials.set_bitmap_incompatible(m_bitmapIncompatible); - - prints .set_bitmap_lock(m_bitmapLock); - filaments .set_bitmap_lock(m_bitmapLock); - sla_prints .set_bitmap_lock(m_bitmapLock); - sla_materials.set_bitmap_lock(m_bitmapLock); - printers .set_bitmap_lock(m_bitmapLock); - - prints .set_bitmap_lock_open(m_bitmapLock); - filaments .set_bitmap_lock_open(m_bitmapLock); - sla_prints .set_bitmap_lock_open(m_bitmapLock); - sla_materials.set_bitmap_lock_open(m_bitmapLock); - printers .set_bitmap_lock_open(m_bitmapLock); + config.set("extras", "physical_printer", physical_printers.get_selected_full_printer_name()); } DynamicPrintConfig PresetBundle::full_config() const @@ -709,21 +679,21 @@ void PresetBundle::load_config_file(const std::string &path) boost::nowide::ifstream ifs(path); boost::property_tree::read_ini(ifs, tree); } catch (const std::ifstream::failure &err) { - throw std::runtime_error(std::string("The Config Bundle cannot be loaded: ") + path + "\n\tReason: " + err.what()); + throw Slic3r::RuntimeError(std::string("The Config Bundle cannot be loaded: ") + path + "\n\tReason: " + err.what()); } catch (const boost::property_tree::file_parser_error &err) { - throw std::runtime_error((boost::format("Failed loading the Config Bundle \"%1%\": %2% at line %3%") + throw Slic3r::RuntimeError((boost::format("Failed loading the Config Bundle \"%1%\": %2% at line %3%") % err.filename() % err.message() % err.line()).str()); } catch (const std::runtime_error &err) { - throw std::runtime_error(std::string("Failed loading the preset file: ") + path + "\n\tReason: " + err.what()); + throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + path + "\n\tReason: " + err.what()); } // 2) Continue based on the type of the configuration file. ConfigFileType config_file_type = guess_config_file_type(tree); switch (config_file_type) { case CONFIG_FILE_TYPE_UNKNOWN: - throw std::runtime_error(std::string("Unknown configuration file type: ") + path); + throw Slic3r::RuntimeError(std::string("Unknown configuration file type: ") + path); case CONFIG_FILE_TYPE_APP_CONFIG: - throw std::runtime_error(std::string("Invalid configuration file: ") + path + ". This is an application config file."); + throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + path + ". This is an application config file."); case CONFIG_FILE_TYPE_CONFIG: { // Initialize a config from full defaults. @@ -886,8 +856,6 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // 4) Load the project config values (the per extruder wipe matrix etc). this->project_config.apply_only(config, s_project_options); - update_custom_gcode_per_print_z_from_config(GUI::wxGetApp().plater()->model().custom_gcode_per_print_z, &this->project_config); - break; } case ptSLA: @@ -1544,207 +1512,11 @@ void PresetBundle::export_configbundle(const std::string &path, bool export_syst // an optional "(modified)" suffix will be removed from the filament name. void PresetBundle::set_filament_preset(size_t idx, const std::string &name) { - if (name.find_first_of(PresetCollection::separator_head()) == 0) - return; - - if (idx >= filament_presets.size()) + if (idx >= filament_presets.size()) filament_presets.resize(idx + 1, filaments.default_preset().name); filament_presets[idx] = Preset::remove_suffix_modified(name); } -void PresetBundle::load_default_preset_bitmaps() -{ - // Clear bitmap cache, before load new scaled default preset bitmaps - m_bitmapCache->clear(); - this->prints.clear_bitmap_cache(); - this->sla_prints.clear_bitmap_cache(); - this->filaments.clear_bitmap_cache(); - this->sla_materials.clear_bitmap_cache(); - this->printers.clear_bitmap_cache(); - - this->prints.load_bitmap_default("cog"); - this->sla_prints.load_bitmap_default("cog"); - this->filaments.load_bitmap_default("spool.png"); - this->sla_materials.load_bitmap_default("resin"); - this->printers.load_bitmap_default("printer"); - this->printers.load_bitmap_add("add.png"); - this->load_compatible_bitmaps(); -} - -void PresetBundle::update_plater_filament_ui(unsigned int idx_extruder, GUI::PresetComboBox *ui) -{ - if (ui == nullptr || this->printers.get_edited_preset().printer_technology() == ptSLA || - this->filament_presets.size() <= idx_extruder ) - return; - - unsigned char rgb[3]; - std::string extruder_color = this->printers.get_edited_preset().config.opt_string("extruder_colour", idx_extruder); - if (!m_bitmapCache->parse_color(extruder_color, rgb)) - // Extruder color is not defined. - extruder_color.clear(); - - // Fill in the list from scratch. - ui->Freeze(); - ui->Clear(); - size_t selected_preset_item = INT_MAX; // some value meaning that no one item is selected - - const Preset *selected_preset = this->filaments.find_preset(this->filament_presets[idx_extruder]); - // Show wide icons if the currently selected preset is not compatible with the current printer, - // and draw a red flag in front of the selected preset. - bool wide_icons = selected_preset != nullptr && ! selected_preset->is_compatible && m_bitmapIncompatible != nullptr; - assert(selected_preset != nullptr); - std::map nonsys_presets; - wxString selected_str = ""; - if (!this->filaments().front().is_visible) - ui->set_label_marker(ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap)); - - /* It's supposed that standard size of an icon is 16px*16px for 100% scaled display. - * So set sizes for solid_colored icons used for filament preset - * and scale them in respect to em_unit value - */ - const float scale_f = ui->em_unit() * 0.1f; - - // To avoid the errors of number rounding for different combination of monitor configuration, - // let use scaled 8px, as a smallest icon unit - const int icon_unit = 8 * scale_f + 0.5f; - const int normal_icon_width = 2 * icon_unit; //16 * scale_f + 0.5f; - const int thin_icon_width = icon_unit; //8 * scale_f + 0.5f; - const int wide_icon_width = 3 * icon_unit; //24 * scale_f + 0.5f; - - const int space_icon_width = 2 * scale_f + 0.5f; - - // To avoid asserts, each added bitmap to wxBitmapCombobox should be the same size, so - // set a bitmap height to m_bitmapLock->GetHeight() - // - // To avoid asserts, each added bitmap to wxBitmapCombobox should be the same size. - // But for some display scaling (for example 125% or 175%) normal_icon_width differs from icon width. - // So: - // for nonsystem presets set a width of empty bitmap to m_bitmapLock->GetWidth() - // for compatible presets set a width of empty bitmap to m_bitmapIncompatible->GetWidth() - // - // Note, under OSX we should use a Scaled Height/Width because of Retina scale -#ifdef __APPLE__ - const int icon_height = m_bitmapLock->GetScaledHeight(); - const int lock_icon_width = m_bitmapLock->GetScaledWidth(); - const int flag_icon_width = m_bitmapIncompatible->GetScaledWidth(); -#else - const int icon_height = m_bitmapLock->GetHeight(); - const int lock_icon_width = m_bitmapLock->GetWidth(); - const int flag_icon_width = m_bitmapIncompatible->GetWidth(); -#endif - - wxString tooltip = ""; - - for (int i = this->filaments().front().is_visible ? 0 : 1; i < int(this->filaments().size()); ++i) { - const Preset &preset = this->filaments.preset(i); - bool selected = this->filament_presets[idx_extruder] == preset.name; - if (! preset.is_visible || (! preset.is_compatible && ! selected)) - continue; - // Assign an extruder color to the selected item if the extruder color is defined. - std::string filament_rgb = preset.config.opt_string("filament_colour", 0); - std::string extruder_rgb = (selected && !extruder_color.empty()) ? extruder_color : filament_rgb; - bool single_bar = filament_rgb == extruder_rgb; - std::string bitmap_key = single_bar ? filament_rgb : filament_rgb + extruder_rgb; - // If the filament preset is not compatible and there is a "red flag" icon loaded, show it left - // to the filament color image. - if (wide_icons) - bitmap_key += preset.is_compatible ? ",cmpt" : ",ncmpt"; - bitmap_key += (preset.is_system || preset.is_default) ? ",syst" : ",nsyst"; - if (preset.is_dirty) - bitmap_key += ",drty"; - wxBitmap *bitmap = m_bitmapCache->find(bitmap_key); - if (bitmap == nullptr) { - // Create the bitmap with color bars. - std::vector bmps; - if (wide_icons) - // Paint a red flag for incompatible presets. - bmps.emplace_back(preset.is_compatible ? m_bitmapCache->mkclear(flag_icon_width, icon_height) : *m_bitmapIncompatible); - // Paint the color bars. - m_bitmapCache->parse_color(filament_rgb, rgb); - bmps.emplace_back(m_bitmapCache->mksolid(single_bar ? wide_icon_width : normal_icon_width, icon_height, rgb)); - if (! single_bar) { - m_bitmapCache->parse_color(extruder_rgb, rgb); - bmps.emplace_back(m_bitmapCache->mksolid(thin_icon_width, icon_height, rgb)); - } - // Paint a lock at the system presets. - bmps.emplace_back(m_bitmapCache->mkclear(space_icon_width, icon_height)); - bmps.emplace_back((preset.is_system || preset.is_default) ? *m_bitmapLock : m_bitmapCache->mkclear(lock_icon_width, icon_height)); -// (preset.is_dirty ? *m_bitmapLockOpen : *m_bitmapLock) : m_bitmapCache->mkclear(16, 16)); - bitmap = m_bitmapCache->insert(bitmap_key, bmps); - } - - const std::string name = preset.alias.empty() ? preset.name : preset.alias; - if (preset.is_default || preset.is_system) { - ui->Append(wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), - (bitmap == 0) ? wxNullBitmap : *bitmap); - if (selected || - // just in case: mark selected_preset_item as a first added element - selected_preset_item == INT_MAX ) { - selected_preset_item = ui->GetCount() - 1; - tooltip = wxString::FromUTF8(preset.name.c_str()); - } - } - else - { - nonsys_presets.emplace(wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), - (bitmap == 0) ? &wxNullBitmap : bitmap); - if (selected) { - selected_str = wxString::FromUTF8((/*preset.*/name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()); - tooltip = wxString::FromUTF8(preset.name.c_str()); - } - } - if (preset.is_default) - ui->set_label_marker(ui->Append(PresetCollection::separator(L("System presets")), wxNullBitmap)); - } - - if (!nonsys_presets.empty()) - { - ui->set_label_marker(ui->Append(PresetCollection::separator(L("User presets")), wxNullBitmap)); - for (std::map::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { - ui->Append(it->first, *it->second); - if (it->first == selected_str || - // just in case: mark selected_preset_item as a first added element - selected_preset_item == INT_MAX) { - selected_preset_item = ui->GetCount() - 1; - } - } - } - - std::string bitmap_key = ""; - if (wide_icons) - bitmap_key += "wide,"; - bitmap_key += "edit_preset_list"; - wxBitmap* bmp = m_bitmapCache->find(bitmap_key); - if (bmp == nullptr) { - // Create the bitmap with color bars. - std::vector bmps; - if (wide_icons) - // Paint a red flag for incompatible presets. - bmps.emplace_back(m_bitmapCache->mkclear(flag_icon_width, icon_height)); - // Paint the color bars + a lock at the system presets. - bmps.emplace_back(m_bitmapCache->mkclear(wide_icon_width+space_icon_width, icon_height)); - bmps.emplace_back(create_scaled_bitmap("edit_uni")); - bmp = m_bitmapCache->insert(bitmap_key, bmps); - } - ui->set_label_marker(ui->Append(PresetCollection::separator(L("Add/Remove filaments")), *bmp), GUI::PresetComboBox::LABEL_ITEM_WIZARD_FILAMENTS); - - /* But, if selected_preset_item is still equal to INT_MAX, it means that - * there is no presets added to the list. - * So, select last combobox item ("Add/Remove filaments") - */ - if (selected_preset_item == INT_MAX) - selected_preset_item = ui->GetCount() - 1; - - ui->SetSelection(selected_preset_item); - ui->SetToolTip(tooltip.IsEmpty() ? ui->GetString(selected_preset_item) : tooltip); - ui->check_selection(selected_preset_item); - ui->Thaw(); - - // Update control min size after rescale (changed Display DPI under MSW) - if (ui->GetMinWidth() != 20 * ui->em_unit()) - ui->SetMinSize(wxSize(20 * ui->em_unit(), ui->GetSize().GetHeight())); -} - void PresetBundle::set_default_suppressed(bool default_suppressed) { prints.set_default_suppressed(default_suppressed); diff --git a/src/slic3r/GUI/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp similarity index 89% rename from src/slic3r/GUI/PresetBundle.hpp rename to src/libslic3r/PresetBundle.hpp index bf1bba21db..ff02bbeae3 100644 --- a/src/slic3r/GUI/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -1,22 +1,15 @@ #ifndef slic3r_PresetBundle_hpp_ #define slic3r_PresetBundle_hpp_ -#include "AppConfig.hpp" #include "Preset.hpp" +#include "AppConfig.hpp" #include -#include #include #include -class wxWindow; - namespace Slic3r { -namespace GUI { - class BitmapCache; -}; - // Bundle of Print + Filament + Printer presets. class PresetBundle { @@ -45,6 +38,7 @@ public: PresetCollection& materials(PrinterTechnology pt) { return pt == ptFFF ? this->filaments : this->sla_materials; } const PresetCollection& materials(PrinterTechnology pt) const { return pt == ptFFF ? this->filaments : this->sla_materials; } PrinterPresetCollection printers; + PhysicalPrinterCollection physical_printers; // Filament preset names for a multi-extruder or multi-material print. // extruders.size() should be the same as printers.get_edited_preset().config.nozzle_diameter.size() std::vector filament_presets; @@ -110,9 +104,6 @@ public: // Export a config bundle file containing all the presets and the names of the active presets. void export_configbundle(const std::string &path, bool export_system_settings = false); - // Update a filament selection combo box on the plater for an idx_extruder. - void update_plater_filament_ui(unsigned int idx_extruder, GUI::PresetComboBox *ui); - // Enable / disable the "- default -" preset. void set_default_suppressed(bool default_suppressed); @@ -132,8 +123,6 @@ public: void update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible); void update_compatible(PresetSelectCompatibleType select_other_if_incompatible) { this->update_compatible(select_other_if_incompatible, select_other_if_incompatible); } - void load_default_preset_bitmaps(); - // Set the is_visible flag for printer vendors, printer models and printer variants // based on the user configuration. // If the "vendor" section is missing, enable all models and variants of the particular vendor. @@ -141,6 +130,10 @@ public: const std::string& get_preset_name_by_alias(const Preset::Type& preset_type, const std::string& alias) const; + // Save current preset of a required type under a new name. If the name is different from the old one, + // Unselected option would be reverted to the beginning values + void save_changes_for_preset(const std::string& new_name, Preset::Type type, const std::vector& unselected_options); + static const char *PRUSA_BUNDLE; private: std::string load_system_presets(); @@ -163,21 +156,9 @@ private: // If it is not an external config, then the config will be stored into the user profile directory. void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config); void load_config_file_config_bundle(const std::string &path, const boost::property_tree::ptree &tree); - void load_compatible_bitmaps(); DynamicPrintConfig full_fff_config() const; DynamicPrintConfig full_sla_config() const; - - // Indicator, that the preset is compatible with the selected printer. - wxBitmap *m_bitmapCompatible; - // Indicator, that the preset is NOT compatible with the selected printer. - wxBitmap *m_bitmapIncompatible; - // Indicator, that the preset is system and not modified. - wxBitmap *m_bitmapLock; - // Indicator, that the preset is system and user modified. - wxBitmap *m_bitmapLockOpen; - // Caching color bitmaps for the filament combo box. - GUI::BitmapCache *m_bitmapCache; }; } // namespace Slic3r diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 878380955a..0fe8d4e4b1 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1,5 +1,6 @@ #include "clipper/clipper_z.hpp" +#include "Exception.hpp" #include "Print.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" @@ -404,6 +405,7 @@ static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, mv_dst.name = mv_src.name; static_cast(mv_dst.config) = static_cast(mv_src.config); mv_dst.m_supported_facets = mv_src.m_supported_facets; + mv_dst.m_seam_facets = mv_src.m_seam_facets; //FIXME what to do with the materials? // mv_dst.m_material_id = mv_src.m_material_id; ++ i_src; @@ -867,6 +869,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ model_volume_list_update_supports(model_object, model_object_new); } } + if (model_custom_seam_data_changed(model_object, model_object_new)) { + update_apply_status(this->invalidate_step(psGCodeExport)); + } if (! model_parts_differ && ! modifiers_differ) { // Synchronize Object's config. bool object_config_changed = model_object.config != model_object_new.config; @@ -1504,7 +1509,7 @@ BoundingBox Print::total_bounding_box() const double Print::skirt_first_layer_height() const { if (m_objects.empty()) - throw std::invalid_argument("skirt_first_layer_height() can't be called without PrintObjects"); + throw Slic3r::InvalidArgument("skirt_first_layer_height() can't be called without PrintObjects"); return m_objects.front()->config().get_abs_value("first_layer_height"); } @@ -1580,7 +1585,7 @@ void Print::auto_assign_extruders(ModelObject* model_object) const // Slicing process, running at a background thread. void Print::process() { - BOOST_LOG_TRIVIAL(info) << "Staring the slicing process." << log_memory_info(); + BOOST_LOG_TRIVIAL(info) << "Starting the slicing process." << log_memory_info(); for (PrintObject *obj : m_objects) obj->make_perimeters(); this->set_status(70, L("Infilling layers")); @@ -1600,7 +1605,7 @@ void Print::process() // Initialize the tool ordering, so it could be used by the G-code preview slider for planning tool changes and filament switches. m_tool_ordering = ToolOrdering(*this, -1, false); if (m_tool_ordering.empty() || m_tool_ordering.last_extruder() == unsigned(-1)) - throw std::runtime_error("The print is empty. The model is not printable with current print settings."); + throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); } this->set_done(psWipeTower); } @@ -1633,13 +1638,21 @@ void Print::process() // The export_gcode may die for various reasons (fails to process output_filename_format, // write error into the G-code, cannot execute post-processing scripts). // It is up to the caller to show an error message. +#if ENABLE_GCODE_VIEWER +std::string Print::export_gcode(const std::string& path_template, GCodeProcessor::Result* result, ThumbnailsGeneratorCallback thumbnail_cb) +#else std::string Print::export_gcode(const std::string& path_template, GCodePreviewData* preview_data, ThumbnailsGeneratorCallback thumbnail_cb) +#endif // ENABLE_GCODE_VIEWER { // output everything to a G-code file // The following call may die if the output_filename_format template substitution fails. std::string path = this->output_filepath(path_template); std::string message; +#if ENABLE_GCODE_VIEWER + if (!path.empty() && result == nullptr) { +#else if (! path.empty() && preview_data == nullptr) { +#endif // ENABLE_GCODE_VIEWER // Only show the path if preview_data is not set -> running from command line. message = L("Exporting G-code"); message += " to "; @@ -1650,7 +1663,11 @@ std::string Print::export_gcode(const std::string& path_template, GCodePreviewDa // The following line may die for multiple reasons. GCode gcode; +#if ENABLE_GCODE_VIEWER + gcode.do_export(this, path.c_str(), result, thumbnail_cb); +#else gcode.do_export(this, path.c_str(), preview_data, thumbnail_cb); +#endif // ENABLE_GCODE_VIEWER return path.c_str(); } @@ -2181,16 +2198,16 @@ DynamicConfig PrintStatistics::config() const DynamicConfig config; std::string normal_print_time = short_time(this->estimated_normal_print_time); std::string silent_print_time = short_time(this->estimated_silent_print_time); - config.set_key_value("print_time", new ConfigOptionString(normal_print_time)); - config.set_key_value("normal_print_time", new ConfigOptionString(normal_print_time)); - config.set_key_value("silent_print_time", new ConfigOptionString(silent_print_time)); - config.set_key_value("used_filament", new ConfigOptionFloat (this->total_used_filament / 1000.)); - config.set_key_value("extruded_volume", new ConfigOptionFloat (this->total_extruded_volume)); - config.set_key_value("total_cost", new ConfigOptionFloat (this->total_cost)); + config.set_key_value("print_time", new ConfigOptionString(normal_print_time)); + config.set_key_value("normal_print_time", new ConfigOptionString(normal_print_time)); + config.set_key_value("silent_print_time", new ConfigOptionString(silent_print_time)); + config.set_key_value("used_filament", new ConfigOptionFloat(this->total_used_filament / 1000.)); + config.set_key_value("extruded_volume", new ConfigOptionFloat(this->total_extruded_volume)); + config.set_key_value("total_cost", new ConfigOptionFloat(this->total_cost)); config.set_key_value("total_toolchanges", new ConfigOptionInt(this->total_toolchanges)); - config.set_key_value("total_weight", new ConfigOptionFloat (this->total_weight)); - config.set_key_value("total_wipe_tower_cost", new ConfigOptionFloat (this->total_wipe_tower_cost)); - config.set_key_value("total_wipe_tower_filament", new ConfigOptionFloat (this->total_wipe_tower_filament)); + config.set_key_value("total_weight", new ConfigOptionFloat(this->total_weight)); + config.set_key_value("total_wipe_tower_cost", new ConfigOptionFloat(this->total_wipe_tower_cost)); + config.set_key_value("total_wipe_tower_filament", new ConfigOptionFloat(this->total_wipe_tower_filament)); return config; } diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index bf541e1222..98a1314112 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -11,6 +11,9 @@ #include "GCode/ToolOrdering.hpp" #include "GCode/WipeTower.hpp" #include "GCode/ThumbnailData.hpp" +#if ENABLE_GCODE_VIEWER +#include "GCode/GCodeProcessor.hpp" +#endif // ENABLE_GCODE_VIEWER #include "libslic3r.h" @@ -20,11 +23,17 @@ class Print; class PrintObject; class ModelObject; class GCode; +#if !ENABLE_GCODE_VIEWER class GCodePreviewData; +#endif // !ENABLE_GCODE_VIEWER enum class SlicingMode : uint32_t; class Layer; class SupportLayer; +namespace FillAdaptive_Internal { + struct Octree; +}; + // Print step IDs for keeping track of the print state. enum PrintStep { psSkirt, @@ -186,10 +195,8 @@ public: std::vector slice_support_blockers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_BLOCKER); } std::vector slice_support_enforcers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_ENFORCER); } - // Helpers to project custom supports on slices - void project_and_append_custom_supports(FacetSupportType type, std::vector& expolys) const; - void project_and_append_custom_enforcers(std::vector& enforcers) const { project_and_append_custom_supports(FacetSupportType::ENFORCER, enforcers); } - void project_and_append_custom_blockers(std::vector& blockers) const { project_and_append_custom_supports(FacetSupportType::BLOCKER, blockers); } + // Helpers to project custom facets on slices + void project_and_append_custom_facets(bool seam, EnforcerBlockerType type, std::vector& expolys) const; private: // to be called from Print only. @@ -232,6 +239,7 @@ private: void discover_horizontal_shells(); void combine_infill(); void _generate_support_material(); + std::pair, std::unique_ptr> prepare_adaptive_infill_data(); // XYZ in scaled coordinates Vec3crd m_size; @@ -300,8 +308,10 @@ struct PrintStatistics PrintStatistics() { clear(); } std::string estimated_normal_print_time; std::string estimated_silent_print_time; +#if !ENABLE_GCODE_VIEWER std::vector> estimated_normal_custom_gcode_print_times; std::vector> estimated_silent_custom_gcode_print_times; +#endif // !ENABLE_GCODE_VIEWER double total_used_filament; double total_extruded_volume; double total_cost; @@ -319,10 +329,12 @@ struct PrintStatistics std::string finalize_output_path(const std::string &path_in) const; void clear() { +#if !ENABLE_GCODE_VIEWER estimated_normal_print_time.clear(); estimated_silent_print_time.clear(); estimated_normal_custom_gcode_print_times.clear(); estimated_silent_custom_gcode_print_times.clear(); +#endif // !ENABLE_GCODE_VIEWER total_used_filament = 0.; total_extruded_volume = 0.; total_cost = 0.; @@ -362,7 +374,11 @@ public: void process() override; // Exports G-code into a file name based on the path_template, returns the file path of the generated G-code file. // If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r). +#if ENABLE_GCODE_VIEWER + std::string export_gcode(const std::string& path_template, GCodeProcessor::Result* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); +#else std::string export_gcode(const std::string& path_template, GCodePreviewData* preview_data, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); +#endif // ENABLE_GCODE_VIEWER // methods for handling state bool is_step_done(PrintStep step) const { return Inherited::is_step_done(step); } @@ -399,7 +415,7 @@ public: // in the notification center. const PrintObject* get_object(ObjectID object_id) const { auto it = std::find_if(m_objects.begin(), m_objects.end(), - [object_id](const PrintObject *obj) { return *static_cast(obj) == object_id; }); + [object_id](const PrintObject *obj) { return obj->id() == object_id; }); return (it == m_objects.end()) ? nullptr : *it; } const PrintRegionPtrs& regions() const { return m_regions; } @@ -417,6 +433,7 @@ public: const Polygon& first_layer_convex_hull() const { return m_first_layer_convex_hull; } const PrintStatistics& print_statistics() const { return m_print_statistics; } + PrintStatistics& print_statistics() { return m_print_statistics; } // Wipe tower support. bool has_wipe_tower() const; diff --git a/src/libslic3r/PrintBase.cpp b/src/libslic3r/PrintBase.cpp index 14339f3c62..7cdf6448c3 100644 --- a/src/libslic3r/PrintBase.cpp +++ b/src/libslic3r/PrintBase.cpp @@ -1,3 +1,4 @@ +#include "Exception.hpp" #include "PrintBase.hpp" #include @@ -68,7 +69,7 @@ std::string PrintBase::output_filename(const std::string &format, const std::str filename = boost::filesystem::change_extension(filename, default_ext); return filename.string(); } catch (std::runtime_error &err) { - throw std::runtime_error(L("Failed processing of the output_filename_format template.") + "\n" + err.what()); + throw Slic3r::RuntimeError(L("Failed processing of the output_filename_format template.") + "\n" + err.what()); } } @@ -93,7 +94,7 @@ void PrintBase::status_update_warnings(ObjectID object_id, int step, PrintStateB if (this->m_status_callback) m_status_callback(SlicingStatus(*this, step)); else if (! message.empty()) - printf("%s warning: %s\n", (object_id == ObjectID(*this)) ? "print" : "print object", message.c_str()); + printf("%s warning: %s\n", (object_id == this->id()) ? "print" : "print object", message.c_str()); } tbb::mutex& PrintObjectBase::state_mutex(PrintBase *print) @@ -108,7 +109,7 @@ std::function PrintObjectBase::cancel_callback(PrintBase *print) void PrintObjectBase::status_update_warnings(PrintBase *print, int step, PrintStateBase::WarningLevel warning_level, const std::string &message) { - print->status_update_warnings(*this, step, warning_level, message); + print->status_update_warnings(this->id(), step, warning_level, message); } } // namespace Slic3r diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index d7f3483e87..647c24c1ce 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -304,7 +304,7 @@ private: class PrintBase; -class PrintObjectBase : public ObjectID +class PrintObjectBase : public ObjectBase { public: const ModelObject* model_object() const { return m_model_object; } @@ -335,7 +335,7 @@ protected: * The PrintBase class will abstract this flow for different technologies. * */ -class PrintBase : public ObjectID +class PrintBase : public ObjectBase { public: PrintBase() : m_placeholder_parser(&m_full_print_config) { this->restart(); } @@ -386,9 +386,9 @@ public: struct SlicingStatus { SlicingStatus(int percent, const std::string &text, unsigned int flags = 0) : percent(percent), text(text), flags(flags) {} SlicingStatus(const PrintBase &print, int warning_step) : - flags(UPDATE_PRINT_STEP_WARNINGS), warning_object_id(print), warning_step(warning_step) {} + flags(UPDATE_PRINT_STEP_WARNINGS), warning_object_id(print.id()), warning_step(warning_step) {} SlicingStatus(const PrintObjectBase &print_object, int warning_step) : - flags(UPDATE_PRINT_OBJECT_STEP_WARNINGS), warning_object_id(print_object), warning_step(warning_step) {} + flags(UPDATE_PRINT_OBJECT_STEP_WARNINGS), warning_object_id(print_object.id()), warning_step(warning_step) {} int percent { -1 }; std::string text; // Bitmap of flags. @@ -507,9 +507,9 @@ protected: bool set_started(PrintStepEnum step) { return m_state.set_started(step, this->state_mutex(), [this](){ this->throw_if_canceled(); }); } PrintStateBase::TimeStamp set_done(PrintStepEnum step) { std::pair status = m_state.set_done(step, this->state_mutex(), [this](){ this->throw_if_canceled(); }); - if (status.second) - this->status_update_warnings(*this, static_cast(step), PrintStateBase::WarningLevel::NON_CRITICAL, std::string()); - return status.first; + if (status.second) + this->status_update_warnings(this->id(), static_cast(step), PrintStateBase::WarningLevel::NON_CRITICAL, std::string()); + return status.first; } bool invalidate_step(PrintStepEnum step) { return m_state.invalidate(step, this->cancel_callback()); } @@ -530,7 +530,7 @@ protected: std::pair active_step = m_state.active_step_add_warning(warning_level, message, message_id, this->state_mutex()); if (active_step.second) // Update UI. - this->status_update_warnings(*this, static_cast(active_step.first), warning_level, message); + this->status_update_warnings(this->id(), static_cast(active_step.first), warning_level, message); } private: @@ -556,9 +556,9 @@ protected: { return m_state.set_started(step, PrintObjectBase::state_mutex(m_print), [this](){ this->throw_if_canceled(); }); } PrintStateBase::TimeStamp set_done(PrintObjectStepEnum step) { std::pair status = m_state.set_done(step, PrintObjectBase::state_mutex(m_print), [this](){ this->throw_if_canceled(); }); - if (status.second) - this->status_update_warnings(m_print, static_cast(step), PrintStateBase::WarningLevel::NON_CRITICAL, std::string()); - return status.first; + if (status.second) + this->status_update_warnings(m_print, static_cast(step), PrintStateBase::WarningLevel::NON_CRITICAL, std::string()); + return status.first; } bool invalidate_step(PrintObjectStepEnum step) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 0d7ddeecdf..830076cefc 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -130,6 +130,37 @@ void PrintConfigDef::init_common_params() def->min = 0; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.2)); + + // Options used by physical printers + + def = this->add("printhost_user", coString); + def->label = L("User"); +// def->tooltip = L(""); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("")); + + def = this->add("printhost_password", coString); + def->label = L("Password"); +// def->tooltip = L(""); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("")); + + def = this->add("preset_name", coString); + def->label = L("Printer preset name"); + def->tooltip = L("Related printer preset name"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("")); + + def = this->add("printhost_authorization_type", coEnum); + def->label = L("Authorization Type"); +// def->tooltip = L(""); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("key"); + def->enum_values.push_back("user"); + def->enum_labels.push_back("KeyPassword"); + def->enum_labels.push_back("UserPassword"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(atKeyPassword)); } void PrintConfigDef::init_fff_params() @@ -850,6 +881,8 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("hilbertcurve"); def->enum_values.push_back("archimedeanchords"); def->enum_values.push_back("octagramspiral"); + def->enum_values.push_back("adaptivecubic"); + def->enum_values.push_back("supportcubic"); def->enum_labels.push_back(L("Rectilinear")); def->enum_labels.push_back(L("Grid")); def->enum_labels.push_back(L("Triangles")); @@ -863,6 +896,8 @@ void PrintConfigDef::init_fff_params() def->enum_labels.push_back(L("Hilbert Curve")); def->enum_labels.push_back(L("Archimedean Chords")); def->enum_labels.push_back(L("Octagram Spiral")); + def->enum_labels.push_back(L("Adaptive Cubic")); + def->enum_labels.push_back(L("Support Cubic")); def->set_default_value(new ConfigOptionEnum(ipStars)); def = this->add("first_layer_acceleration", coFloat); @@ -1101,6 +1136,7 @@ void PrintConfigDef::init_fff_params() def = this->add("ironing_type", coEnum); def->label = L("Ironing Type"); + def->category = L("Ironing"); def->tooltip = L("Ironing Type"); def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values.push_back("top"); @@ -1124,7 +1160,8 @@ void PrintConfigDef::init_fff_params() def = this->add("ironing_spacing", coFloat); def->label = L("Spacing between ironing passes"); - def->tooltip = L("Distance between ironing lins"); + def->category = L("Ironing"); + def->tooltip = L("Distance between ironing lines"); def->sidetext = L("mm"); def->min = 0; def->mode = comExpert; @@ -2715,7 +2752,7 @@ void PrintConfigDef::init_sla_params() def->set_default_value(new ConfigOptionBool(true)); def = this->add("support_head_front_diameter", coFloat); - def->label = L("Support head front diameter"); + def->label = L("Pinhead front diameter"); def->category = L("Supports"); def->tooltip = L("Diameter of the pointing side of the head"); def->sidetext = L("mm"); @@ -2724,7 +2761,7 @@ void PrintConfigDef::init_sla_params() def->set_default_value(new ConfigOptionFloat(0.4)); def = this->add("support_head_penetration", coFloat); - def->label = L("Support head penetration"); + def->label = L("Head penetration"); def->category = L("Supports"); def->tooltip = L("How much the pinhead has to penetrate the model surface"); def->sidetext = L("mm"); @@ -2733,7 +2770,7 @@ void PrintConfigDef::init_sla_params() def->set_default_value(new ConfigOptionFloat(0.2)); def = this->add("support_head_width", coFloat); - def->label = L("Support head width"); + def->label = L("Pinhead width"); def->category = L("Supports"); def->tooltip = L("Width from the back sphere center to the front sphere center"); def->sidetext = L("mm"); @@ -2743,7 +2780,7 @@ void PrintConfigDef::init_sla_params() def->set_default_value(new ConfigOptionFloat(1.0)); def = this->add("support_pillar_diameter", coFloat); - def->label = L("Support pillar diameter"); + def->label = L("Pillar diameter"); def->category = L("Supports"); def->tooltip = L("Diameter in mm of the support pillars"); def->sidetext = L("mm"); @@ -2751,6 +2788,17 @@ void PrintConfigDef::init_sla_params() def->max = 15; def->mode = comSimple; def->set_default_value(new ConfigOptionFloat(1.0)); + + def = this->add("support_small_pillar_diameter_percent", coPercent); + def->label = L("Small pillar diameter percent"); + def->category = L("Supports"); + def->tooltip = L("The percentage of smaller pillars compared to the normal pillar diameter " + "which are used in problematic areas where a normal pilla cannot fit."); + def->sidetext = L("%"); + def->min = 1; + def->max = 100; + def->mode = comExpert; + def->set_default_value(new ConfigOptionPercent(50)); def = this->add("support_max_bridges_on_pillar", coInt); def->label = L("Max bridges on a pillar"); @@ -2763,7 +2811,7 @@ void PrintConfigDef::init_sla_params() def->set_default_value(new ConfigOptionInt(3)); def = this->add("support_pillar_connection_mode", coEnum); - def->label = L("Support pillar connection mode"); + def->label = L("Pillar connection mode"); def->tooltip = L("Controls the bridge type between two neighboring pillars." " Can be zig-zag, cross (double zig-zag) or dynamic which" " will automatically switch between the first two depending" @@ -3489,6 +3537,12 @@ CLIActionsConfigDef::CLIActionsConfigDef() def->cli = "export-gcode|gcode|g"; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("gcodeviewer", coBool); + def->label = L("G-code viewer"); + def->tooltip = L("Visualize an already sliced and saved G-code"); + def->cli = "gcodeviewer"; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("slice", coBool); def->label = L("Slice"); def->tooltip = L("Slice the model as FFF or SLA based on the printer_technology configuration value."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 0ab51718bd..a1bd20a1c1 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -33,9 +33,13 @@ enum PrintHostType { htOctoPrint, htDuet, htFlashAir, htAstroBox }; +enum AuthorizationType { + atKeyPassword, atUserPassword +}; + enum InfillPattern : int { ipRectilinear, ipMonotonous, ipGrid, ipTriangles, ipStars, ipCubic, ipLine, ipConcentric, ipHoneycomb, ip3DHoneycomb, - ipGyroid, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, ipCount, + ipGyroid, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, ipAdaptiveCubic, ipSupportCubic, ipCount, }; enum class IroningType { @@ -110,6 +114,15 @@ template<> inline const t_config_enum_values& ConfigOptionEnum::g return keys_map; } +template<> inline const t_config_enum_values& ConfigOptionEnum::get_enum_values() { + static t_config_enum_values keys_map; + if (keys_map.empty()) { + keys_map["key"] = atKeyPassword; + keys_map["user"] = atUserPassword; + } + return keys_map; +} + template<> inline const t_config_enum_values& ConfigOptionEnum::get_enum_values() { static t_config_enum_values keys_map; if (keys_map.empty()) { @@ -127,6 +140,8 @@ template<> inline const t_config_enum_values& ConfigOptionEnum::g keys_map["hilbertcurve"] = ipHilbertCurve; keys_map["archimedeanchords"] = ipArchimedeanChords; keys_map["octagramspiral"] = ipOctagramSpiral; + keys_map["adaptivecubic"] = ipAdaptiveCubic; + keys_map["supportcubic"] = ipSupportCubic; } return keys_map; } @@ -227,9 +242,13 @@ class DynamicPrintConfig : public DynamicConfig public: DynamicPrintConfig() {} DynamicPrintConfig(const DynamicPrintConfig &rhs) : DynamicConfig(rhs) {} + DynamicPrintConfig(DynamicPrintConfig &&rhs) noexcept : DynamicConfig(std::move(rhs)) {} explicit DynamicPrintConfig(const StaticPrintConfig &rhs); explicit DynamicPrintConfig(const ConfigBase &rhs) : DynamicConfig(rhs) {} + DynamicPrintConfig& operator=(const DynamicPrintConfig &rhs) { DynamicConfig::operator=(rhs); return *this; } + DynamicPrintConfig& operator=(DynamicPrintConfig &&rhs) noexcept { DynamicConfig::operator=(std::move(rhs)); return *this; } + static DynamicPrintConfig full_print_config(); static DynamicPrintConfig* new_from_defaults_keys(const std::vector &keys); @@ -1019,6 +1038,10 @@ public: // Radius in mm of the support pillars. ConfigOptionFloat support_pillar_diameter /*= 0.8*/; + + // The percentage of smaller pillars compared to the normal pillar diameter + // which are used in problematic areas where a normal pilla cannot fit. + ConfigOptionPercent support_small_pillar_diameter_percent; // How much bridge (supporting another pinhead) can be placed on a pillar. ConfigOptionInt support_max_bridges_on_pillar; @@ -1143,6 +1166,7 @@ protected: OPT_PTR(support_head_penetration); OPT_PTR(support_head_width); OPT_PTR(support_pillar_diameter); + OPT_PTR(support_small_pillar_diameter_percent); OPT_PTR(support_max_bridges_on_pillar); OPT_PTR(support_pillar_connection_mode); OPT_PTR(support_buildplate_only); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index d2bdb6d531..ddd41af012 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1,3 +1,4 @@ +#include "Exception.hpp" #include "Print.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" @@ -9,6 +10,9 @@ #include "Surface.hpp" #include "Slicing.hpp" #include "Utils.hpp" +#include "AABBTreeIndirect.hpp" +#include "Fill/FillAdaptive.hpp" +#include "Format/STL.hpp" #include #include @@ -135,7 +139,7 @@ void PrintObject::slice() } }); if (m_layers.empty()) - throw std::runtime_error("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n"); + throw Slic3r::SlicingError("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n"); this->set_done(posSlice); } @@ -369,13 +373,15 @@ void PrintObject::infill() this->prepare_infill(); if (this->set_started(posInfill)) { + auto [adaptive_fill_octree, support_fill_octree] = this->prepare_adaptive_infill_data(); + BOOST_LOG_TRIVIAL(debug) << "Filling layers in parallel - start"; tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), - [this](const tbb::blocked_range& range) { + [this, &adaptive_fill_octree = adaptive_fill_octree, &support_fill_octree = support_fill_octree](const tbb::blocked_range& range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); - m_layers[layer_idx]->make_fills(); + m_layers[layer_idx]->make_fills(adaptive_fill_octree.get(), support_fill_octree.get()); } } ); @@ -421,13 +427,83 @@ void PrintObject::generate_support_material() // therefore they cannot be printed without supports. for (const Layer *layer : m_layers) if (layer->empty()) - throw std::runtime_error("Levitating objects cannot be printed without supports."); + throw Slic3r::SlicingError("Levitating objects cannot be printed without supports."); #endif } this->set_done(posSupportMaterial); } } +//#define ADAPTIVE_SUPPORT_SIMPLE + +std::pair, std::unique_ptr> PrintObject::prepare_adaptive_infill_data() +{ + using namespace FillAdaptive_Internal; + + auto [adaptive_line_spacing, support_line_spacing] = adaptive_fill_line_spacing(*this); + + std::unique_ptr adaptive_fill_octree = {}, support_fill_octree = {}; + + if (adaptive_line_spacing == 0. && support_line_spacing == 0.) + return std::make_pair(std::move(adaptive_fill_octree), std::move(support_fill_octree)); + + TriangleMesh mesh = this->model_object()->raw_mesh(); + mesh.transform(m_trafo, true); + // Apply XY shift + mesh.translate(- unscale(m_center_offset.x()), - unscale(m_center_offset.y()), 0); + // Center of the first cube in octree + Vec3d mesh_origin = mesh.bounding_box().center(); + +#ifdef ADAPTIVE_SUPPORT_SIMPLE + if (mesh.its.vertices.empty()) + { + mesh.require_shared_vertices(); + } + + Vec3f vertical(0, 0, 1); + + indexed_triangle_set its_set; + its_set.vertices = mesh.its.vertices; + + // Filter out non overhanging faces + for (size_t i = 0; i < mesh.its.indices.size(); ++i) { + stl_triangle_vertex_indices vertex_idx = mesh.its.indices[i]; + + auto its_calculate_normal = [](const stl_triangle_vertex_indices &index, const std::vector &vertices) { + stl_normal normal = (vertices[index.y()] - vertices[index.x()]).cross(vertices[index.z()] - vertices[index.x()]); + return normal; + }; + + stl_normal normal = its_calculate_normal(vertex_idx, mesh.its.vertices); + stl_normalize_vector(normal); + + if(normal.dot(vertical) >= 0.707) { + its_set.indices.push_back(vertex_idx); + } + } + + mesh = TriangleMesh(its_set); + +#ifdef SLIC3R_DEBUG_SLICE_PROCESSING + Slic3r::store_stl(debug_out_path("overhangs.stl").c_str(), &mesh, false); +#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ +#endif /* ADAPTIVE_SUPPORT_SIMPLE */ + + Vec3d rotation = Vec3d((5.0 * M_PI) / 4.0, Geometry::deg2rad(215.264), M_PI / 6.0); + Transform3d rotation_matrix = Geometry::assemble_transform(Vec3d::Zero(), rotation, Vec3d::Ones(), Vec3d::Ones()).inverse(); + + if (adaptive_line_spacing != 0.) { + // Rotate mesh and build octree on it with axis-aligned (standart base) cubes + mesh.transform(rotation_matrix); + adaptive_fill_octree = FillAdaptive::build_octree(mesh, adaptive_line_spacing, rotation_matrix * mesh_origin); + } + + if (support_line_spacing != 0.) + support_fill_octree = FillSupportCubic::build_octree(mesh, support_line_spacing, rotation_matrix * mesh_origin, rotation_matrix); + + return std::make_pair(std::move(adaptive_fill_octree), std::move(support_fill_octree)); +} + void PrintObject::clear_layers() { for (Layer *l : m_layers) @@ -2669,18 +2745,20 @@ void PrintObject::_generate_support_material() } -void PrintObject::project_and_append_custom_supports( - FacetSupportType type, std::vector& expolys) const +void PrintObject::project_and_append_custom_facets( + bool seam, EnforcerBlockerType type, std::vector& expolys) const { for (const ModelVolume* mv : this->model_object()->volumes) { - const std::vector custom_facets = mv->m_supported_facets.get_facets(type); - if (custom_facets.empty()) + const indexed_triangle_set custom_facets = seam + ? mv->m_seam_facets.get_facets(*mv, type) + : mv->m_supported_facets.get_facets(*mv, type); + if (! mv->is_model_part() || custom_facets.indices.empty()) continue; - const TriangleMesh& mesh = mv->mesh(); const Transform3f& tr1 = mv->get_matrix().cast(); const Transform3f& tr2 = this->trafo().cast(); const Transform3f tr = tr2 * tr1; + const float tr_det_sign = (tr.matrix().determinant() > 0. ? 1.f : -1.f); // The projection will be at most a pentagon. Let's minimize heap @@ -2705,11 +2783,11 @@ void PrintObject::project_and_append_custom_supports( }; // Vector to collect resulting projections from each triangle. - std::vector projections_of_triangles(custom_facets.size()); + std::vector projections_of_triangles(custom_facets.indices.size()); // Iterate over all triangles. tbb::parallel_for( - tbb::blocked_range(0, custom_facets.size()), + tbb::blocked_range(0, custom_facets.indices.size()), [&](const tbb::blocked_range& range) { for (size_t idx = range.begin(); idx < range.end(); ++ idx) { @@ -2717,10 +2795,11 @@ void PrintObject::project_and_append_custom_supports( // Transform the triangle into worlds coords. for (int i=0; i<3; ++i) - facet[i] = tr * mesh.its.vertices[mesh.its.indices[custom_facets[idx]](i)]; + facet[i] = tr * custom_facets.vertices[custom_facets.indices[idx](i)]; - // Ignore triangles with upward-pointing normal. - if ((facet[1]-facet[0]).cross(facet[2]-facet[0]).z() > 0.) + // Ignore triangles with upward-pointing normal. Don't forget about mirroring. + float z_comp = (facet[1]-facet[0]).cross(facet[2]-facet[0]).z(); + if (! seam && tr_det_sign * z_comp > 0.) continue; // Sort the three vertices according to z-coordinate. @@ -2732,7 +2811,7 @@ void PrintObject::project_and_append_custom_supports( std::array trianglef; for (int i=0; i<3; ++i) { trianglef[i] = Vec2f(facet[i].x(), facet[i].y()); - trianglef[i] += Vec2f(unscale(this->center_offset().x()), + trianglef[i] -= Vec2f(unscale(this->center_offset().x()), unscale(this->center_offset().y())); } @@ -2822,8 +2901,9 @@ void PrintObject::project_and_append_custom_supports( // Now append the collected polygons to respective layers. for (auto& trg : projections_of_triangles) { int layer_id = trg.first_layer_id; - for (const LightPolygon& poly : trg.polygons) { + if (layer_id >= int(expolys.size())) + break; // part of triangle could be projected above top layer expolys[layer_id].emplace_back(std::move(poly.pts)); ++layer_id; } diff --git a/src/libslic3r/PrintRegion.cpp b/src/libslic3r/PrintRegion.cpp index b3ac6a4a5b..2a75cd621d 100644 --- a/src/libslic3r/PrintRegion.cpp +++ b/src/libslic3r/PrintRegion.cpp @@ -1,3 +1,4 @@ +#include "Exception.hpp" #include "Print.hpp" namespace Slic3r { @@ -13,7 +14,7 @@ unsigned int PrintRegion::extruder(FlowRole role) const else if (role == frSolidInfill || role == frTopSolidInfill) extruder = m_config.solid_infill_extruder; else - throw std::invalid_argument("Unknown role"); + throw Slic3r::InvalidArgument("Unknown role"); return extruder; } @@ -40,7 +41,7 @@ Flow PrintRegion::flow(FlowRole role, double layer_height, bool bridge, bool fir } else if (role == frTopSolidInfill) { config_width = m_config.top_infill_extrusion_width; } else { - throw std::invalid_argument("Unknown role"); + throw Slic3r::InvalidArgument("Unknown role"); } } diff --git a/src/libslic3r/SLA/AGGRaster.hpp b/src/libslic3r/SLA/AGGRaster.hpp index 37baed9e88..917f718e98 100644 --- a/src/libslic3r/SLA/AGGRaster.hpp +++ b/src/libslic3r/SLA/AGGRaster.hpp @@ -128,12 +128,13 @@ protected: } public: - template AGGRaster(const Resolution &res, + template + AGGRaster(const Resolution &res, const PixelDim & pd, const Trafo & trafo, - const TColor & foreground, - const TColor & background, - GammaFn && gammafn) + const TColor & foreground, + const TColor & background, + GammaFn && gammafn) : m_resolution(res) , m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm) , m_buf(res.pixels()) diff --git a/src/libslic3r/SLA/BoostAdapter.hpp b/src/libslic3r/SLA/BoostAdapter.hpp index b7b3c63a6c..13e0465b14 100644 --- a/src/libslic3r/SLA/BoostAdapter.hpp +++ b/src/libslic3r/SLA/BoostAdapter.hpp @@ -1,7 +1,9 @@ #ifndef SLA_BOOSTADAPTER_HPP #define SLA_BOOSTADAPTER_HPP -#include +#include +#include + #include namespace boost { diff --git a/src/libslic3r/SLA/Clustering.cpp b/src/libslic3r/SLA/Clustering.cpp new file mode 100644 index 0000000000..41ff1d4f09 --- /dev/null +++ b/src/libslic3r/SLA/Clustering.cpp @@ -0,0 +1,152 @@ +#include "Clustering.hpp" +#include "boost/geometry/index/rtree.hpp" + +#include +#include + +namespace Slic3r { namespace sla { + +namespace bgi = boost::geometry::index; +using Index3D = bgi::rtree< PointIndexEl, bgi::rstar<16, 4> /* ? */ >; + +namespace { + +bool cmp_ptidx_elements(const PointIndexEl& e1, const PointIndexEl& e2) +{ + return e1.second < e2.second; +}; + +ClusteredPoints cluster(Index3D &sindex, + unsigned max_points, + std::function( + const Index3D &, const PointIndexEl &)> qfn) +{ + using Elems = std::vector; + + // Recursive function for visiting all the points in a given distance to + // each other + std::function group = + [&sindex, &group, max_points, qfn](Elems& pts, Elems& cluster) + { + for(auto& p : pts) { + std::vector tmp = qfn(sindex, p); + + std::sort(tmp.begin(), tmp.end(), cmp_ptidx_elements); + + Elems newpts; + std::set_difference(tmp.begin(), tmp.end(), + cluster.begin(), cluster.end(), + std::back_inserter(newpts), cmp_ptidx_elements); + + int c = max_points && newpts.size() + cluster.size() > max_points? + int(max_points - cluster.size()) : int(newpts.size()); + + cluster.insert(cluster.end(), newpts.begin(), newpts.begin() + c); + std::sort(cluster.begin(), cluster.end(), cmp_ptidx_elements); + + if(!newpts.empty() && (!max_points || cluster.size() < max_points)) + group(newpts, cluster); + } + }; + + std::vector clusters; + for(auto it = sindex.begin(); it != sindex.end();) { + Elems cluster = {}; + Elems pts = {*it}; + group(pts, cluster); + + for(auto& c : cluster) sindex.remove(c); + it = sindex.begin(); + + clusters.emplace_back(cluster); + } + + ClusteredPoints result; + for(auto& cluster : clusters) { + result.emplace_back(); + for(auto c : cluster) result.back().emplace_back(c.second); + } + + return result; +} + +std::vector distance_queryfn(const Index3D& sindex, + const PointIndexEl& p, + double dist, + unsigned max_points) +{ + std::vector tmp; tmp.reserve(max_points); + sindex.query( + bgi::nearest(p.first, max_points), + std::back_inserter(tmp) + ); + + for(auto it = tmp.begin(); it < tmp.end(); ++it) + if((p.first - it->first).norm() > dist) it = tmp.erase(it); + + return tmp; +} + +} // namespace + +// Clustering a set of points by the given criteria +ClusteredPoints cluster( + const std::vector& indices, + std::function pointfn, + double dist, + unsigned max_points) +{ + // A spatial index for querying the nearest points + Index3D sindex; + + // Build the index + for(auto idx : indices) sindex.insert( std::make_pair(pointfn(idx), idx)); + + return cluster(sindex, max_points, + [dist, max_points](const Index3D& sidx, const PointIndexEl& p) + { + return distance_queryfn(sidx, p, dist, max_points); + }); +} + +// Clustering a set of points by the given criteria +ClusteredPoints cluster( + const std::vector& indices, + std::function pointfn, + std::function predicate, + unsigned max_points) +{ + // A spatial index for querying the nearest points + Index3D sindex; + + // Build the index + for(auto idx : indices) sindex.insert( std::make_pair(pointfn(idx), idx)); + + return cluster(sindex, max_points, + [max_points, predicate](const Index3D& sidx, const PointIndexEl& p) + { + std::vector tmp; tmp.reserve(max_points); + sidx.query(bgi::satisfies([p, predicate](const PointIndexEl& e){ + return predicate(p, e); + }), std::back_inserter(tmp)); + return tmp; + }); +} + +ClusteredPoints cluster(const Eigen::MatrixXd& pts, double dist, unsigned max_points) +{ + // A spatial index for querying the nearest points + Index3D sindex; + + // Build the index + for(Eigen::Index i = 0; i < pts.rows(); i++) + sindex.insert(std::make_pair(Vec3d(pts.row(i)), unsigned(i))); + + return cluster(sindex, max_points, + [dist, max_points](const Index3D& sidx, const PointIndexEl& p) + { + return distance_queryfn(sidx, p, dist, max_points); + }); +} + +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/Clustering.hpp b/src/libslic3r/SLA/Clustering.hpp index 1b0d47d953..269ec28822 100644 --- a/src/libslic3r/SLA/Clustering.hpp +++ b/src/libslic3r/SLA/Clustering.hpp @@ -2,7 +2,8 @@ #define SLA_CLUSTERING_HPP #include -#include + +#include #include namespace Slic3r { namespace sla { @@ -16,7 +17,7 @@ ClusteredPoints cluster(const std::vector& indices, double dist, unsigned max_points); -ClusteredPoints cluster(const PointSet& points, +ClusteredPoints cluster(const Eigen::MatrixXd& points, double dist, unsigned max_points); @@ -26,5 +27,56 @@ ClusteredPoints cluster( std::function predicate, unsigned max_points); -}} +// This function returns the position of the centroid in the input 'clust' +// vector of point indices. +template +long cluster_centroid(const ClusterEl &clust, PointFn pointfn, DistFn df) +{ + switch(clust.size()) { + case 0: /* empty cluster */ return -1; + case 1: /* only one element */ return 0; + case 2: /* if two elements, there is no center */ return 0; + default: ; + } + + // The function works by calculating for each point the average distance + // from all the other points in the cluster. We create a selector bitmask of + // the same size as the cluster. The bitmask will have two true bits and + // false bits for the rest of items and we will loop through all the + // permutations of the bitmask (combinations of two points). Get the + // distance for the two points and add the distance to the averages. + // The point with the smallest average than wins. + + // The complexity should be O(n^2) but we will mostly apply this function + // for small clusters only (cca 3 elements) + + std::vector sel(clust.size(), false); // create full zero bitmask + std::fill(sel.end() - 2, sel.end(), true); // insert the two ones + std::vector avgs(clust.size(), 0.0); // store the average distances + + do { + std::array idx; + for(size_t i = 0, j = 0; i < clust.size(); i++) + if(sel[i]) idx[j++] = i; + + double d = df(pointfn(clust[idx[0]]), + pointfn(clust[idx[1]])); + + // add the distance to the sums for both associated points + for(auto i : idx) avgs[i] += d; + + // now continue with the next permutation of the bitmask with two 1s + } while(std::next_permutation(sel.begin(), sel.end())); + + // Divide by point size in the cluster to get the average (may be redundant) + for(auto& a : avgs) a /= clust.size(); + + // get the lowest average distance and return the index + auto minit = std::min_element(avgs.begin(), avgs.end()); + return long(minit - avgs.begin()); +} + + +}} // namespace Slic3r::sla + #endif // CLUSTERING_HPP diff --git a/src/libslic3r/SLA/Common.hpp b/src/libslic3r/SLA/Common.hpp deleted file mode 100644 index ca616cabce..0000000000 --- a/src/libslic3r/SLA/Common.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef SLA_COMMON_HPP -#define SLA_COMMON_HPP - -#include -#include -#include -#include -#include - - -namespace Slic3r { - -// Typedefs from Point.hpp -typedef Eigen::Matrix Vec3f; -typedef Eigen::Matrix Vec3d; -typedef Eigen::Matrix Vec3i; -typedef Eigen::Matrix Vec4i; - -namespace sla { - -using PointSet = Eigen::MatrixXd; - -} // namespace sla -} // namespace Slic3r - - -#endif // SLASUPPORTTREE_HPP diff --git a/src/libslic3r/SLA/Concurrency.hpp b/src/libslic3r/SLA/Concurrency.hpp index 8620c67b19..300024c76d 100644 --- a/src/libslic3r/SLA/Concurrency.hpp +++ b/src/libslic3r/SLA/Concurrency.hpp @@ -4,6 +4,12 @@ #include #include #include +#include + +#include +#include + +#include namespace Slic3r { namespace sla { @@ -17,18 +23,59 @@ template struct _ccr {}; template<> struct _ccr { using SpinningMutex = tbb::spin_mutex; - using BlockingMutex = tbb::mutex; - - template - static inline void enumerate(It from, It to, Fn fn) + using BlockingMutex = tbb::mutex; + + template + static IteratorOnly loop_(const tbb::blocked_range &range, Fn &&fn) { - auto iN = to - from; - size_t N = iN < 0 ? 0 : size_t(iN); - - tbb::parallel_for(size_t(0), N, [from, fn](size_t n) { - fn(*(from + decltype(iN)(n)), n); + for (auto &el : range) fn(el); + } + + template + static IntegerOnly loop_(const tbb::blocked_range &range, Fn &&fn) + { + for (I i = range.begin(); i < range.end(); ++i) fn(i); + } + + template + static void for_each(It from, It to, Fn &&fn, size_t granularity = 1) + { + tbb::parallel_for(tbb::blocked_range{from, to, granularity}, + [&fn, from](const auto &range) { + loop_(range, std::forward(fn)); }); } + + template + static T reduce(I from, + I to, + const T &init, + MergeFn &&mergefn, + AccessFn &&access, + size_t granularity = 1 + ) + { + return tbb::parallel_reduce( + tbb::blocked_range{from, to, granularity}, init, + [&](const auto &range, T subinit) { + T acc = subinit; + loop_(range, [&](auto &i) { acc = mergefn(acc, access(i)); }); + return acc; + }, + std::forward(mergefn)); + } + + template + static IteratorOnly reduce(I from, + I to, + const T & init, + MergeFn &&mergefn, + size_t granularity = 1) + { + return reduce( + from, to, init, std::forward(mergefn), + [](typename I::value_type &i) { return i; }, granularity); + } }; template<> struct _ccr @@ -39,11 +86,52 @@ private: public: using SpinningMutex = _Mtx; using BlockingMutex = _Mtx; - - template - static inline void enumerate(It from, It to, Fn fn) + + template + static IteratorOnly loop_(It from, It to, Fn &&fn) { - for (auto it = from; it != to; ++it) fn(*it, size_t(it - from)); + for (auto it = from; it != to; ++it) fn(*it); + } + + template + static IntegerOnly loop_(I from, I to, Fn &&fn) + { + for (I i = from; i < to; ++i) fn(i); + } + + template + static void for_each(It from, + It to, + Fn &&fn, + size_t /* ignore granularity */ = 1) + { + loop_(from, to, std::forward(fn)); + } + + template + static T reduce(I from, + I to, + const T & init, + MergeFn &&mergefn, + AccessFn &&access, + size_t /*granularity*/ = 1 + ) + { + T acc = init; + loop_(from, to, [&](auto &i) { acc = mergefn(acc, access(i)); }); + return acc; + } + + template + static IteratorOnly reduce(I from, + I to, + const T &init, + MergeFn &&mergefn, + size_t /*granularity*/ = 1 + ) + { + return reduce(from, to, init, std::forward(mergefn), + [](typename I::value_type &i) { return i; }); } }; diff --git a/src/libslic3r/SLA/Contour3D.cpp b/src/libslic3r/SLA/Contour3D.cpp index 408465d43e..96d10af208 100644 --- a/src/libslic3r/SLA/Contour3D.cpp +++ b/src/libslic3r/SLA/Contour3D.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include @@ -27,7 +27,7 @@ Contour3D::Contour3D(TriangleMesh &&trmesh) faces3.swap(trmesh.its.indices); } -Contour3D::Contour3D(const EigenMesh3D &emesh) { +Contour3D::Contour3D(const IndexedMesh &emesh) { points.reserve(emesh.vertices().size()); faces3.reserve(emesh.indices().size()); diff --git a/src/libslic3r/SLA/Contour3D.hpp b/src/libslic3r/SLA/Contour3D.hpp index 295612f19b..3380cd6ab0 100644 --- a/src/libslic3r/SLA/Contour3D.hpp +++ b/src/libslic3r/SLA/Contour3D.hpp @@ -1,13 +1,16 @@ #ifndef SLA_CONTOUR3D_HPP #define SLA_CONTOUR3D_HPP -#include - #include -namespace Slic3r { namespace sla { +namespace Slic3r { -class EigenMesh3D; +// Used for quads (TODO: remove this, and convert quads to triangles in OpenVDBUtils) +using Vec4i = Eigen::Matrix; + +namespace sla { + +class IndexedMesh; /// Dumb vertex mesh consisting of triangles (or) quads. Capable of merging with /// other meshes of this type and converting to and from other mesh formats. @@ -19,7 +22,7 @@ struct Contour3D { Contour3D() = default; Contour3D(const TriangleMesh &trmesh); Contour3D(TriangleMesh &&trmesh); - Contour3D(const EigenMesh3D &emesh); + Contour3D(const IndexedMesh &emesh); Contour3D& merge(const Contour3D& ctr); Contour3D& merge(const Pointf3s& triangles); diff --git a/src/libslic3r/SLA/Hollowing.cpp b/src/libslic3r/SLA/Hollowing.cpp index 0dd9436a1d..6df752fd36 100644 --- a/src/libslic3r/SLA/Hollowing.cpp +++ b/src/libslic3r/SLA/Hollowing.cpp @@ -3,11 +3,10 @@ #include #include #include -#include -#include -#include +#include #include #include +#include #include @@ -160,7 +159,7 @@ bool DrainHole::get_intersections(const Vec3f& s, const Vec3f& dir, const Eigen::ParametrizedLine ray(s, dir.normalized()); for (size_t i=0; i<2; ++i) - out[i] = std::make_pair(sla::EigenMesh3D::hit_result::infty(), Vec3d::Zero()); + out[i] = std::make_pair(sla::IndexedMesh::hit_result::infty(), Vec3d::Zero()); const float sqr_radius = pow(radius, 2.f); @@ -274,4 +273,13 @@ void cut_drainholes(std::vector & obj_slices, obj_slices[i] = diff_ex(obj_slices[i], hole_slices[i]); } +void hollow_mesh(TriangleMesh &mesh, const HollowingConfig &cfg) +{ + std::unique_ptr inter_ptr = + Slic3r::sla::generate_interior(mesh); + + if (inter_ptr) mesh.merge(*inter_ptr); + mesh.require_shared_vertices(); +} + }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/Hollowing.hpp b/src/libslic3r/SLA/Hollowing.hpp index cc7d310eae..949cc23935 100644 --- a/src/libslic3r/SLA/Hollowing.hpp +++ b/src/libslic3r/SLA/Hollowing.hpp @@ -2,7 +2,6 @@ #define SLA_HOLLOWING_HPP #include -#include #include #include @@ -59,10 +58,14 @@ struct DrainHole using DrainHoles = std::vector; +constexpr float HoleStickOutLength = 1.f; + std::unique_ptr generate_interior(const TriangleMesh &mesh, const HollowingConfig & = {}, const JobController &ctl = {}); +void hollow_mesh(TriangleMesh &mesh, const HollowingConfig &cfg); + void cut_drainholes(std::vector & obj_slices, const std::vector &slicegrid, float closing_radius, diff --git a/src/libslic3r/SLA/Common.cpp b/src/libslic3r/SLA/IndexedMesh.cpp similarity index 53% rename from src/libslic3r/SLA/Common.cpp rename to src/libslic3r/SLA/IndexedMesh.cpp index a7420a7fb8..efcf09873e 100644 --- a/src/libslic3r/SLA/Common.cpp +++ b/src/libslic3r/SLA/IndexedMesh.cpp @@ -1,187 +1,18 @@ -#include -#include -#include -#include -#include -#include -#include +#include "IndexedMesh.hpp" +#include "Concurrency.hpp" + #include +#include -// for concave hull merging decisions -#include -#include "boost/geometry/index/rtree.hpp" - -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable: 4244) -#pragma warning(disable: 4267) -#endif - - -#include +#include #ifdef SLIC3R_HOLE_RAYCASTER - #include +#include #endif +namespace Slic3r { namespace sla { -#ifdef _MSC_VER -#pragma warning(pop) -#endif - - -namespace Slic3r { -namespace sla { - - -/* ************************************************************************** - * PointIndex implementation - * ************************************************************************** */ - -class PointIndex::Impl { -public: - using BoostIndex = boost::geometry::index::rtree< PointIndexEl, - boost::geometry::index::rstar<16, 4> /* ? */ >; - - BoostIndex m_store; -}; - -PointIndex::PointIndex(): m_impl(new Impl()) {} -PointIndex::~PointIndex() {} - -PointIndex::PointIndex(const PointIndex &cpy): m_impl(new Impl(*cpy.m_impl)) {} -PointIndex::PointIndex(PointIndex&& cpy): m_impl(std::move(cpy.m_impl)) {} - -PointIndex& PointIndex::operator=(const PointIndex &cpy) -{ - m_impl.reset(new Impl(*cpy.m_impl)); - return *this; -} - -PointIndex& PointIndex::operator=(PointIndex &&cpy) -{ - m_impl.swap(cpy.m_impl); - return *this; -} - -void PointIndex::insert(const PointIndexEl &el) -{ - m_impl->m_store.insert(el); -} - -bool PointIndex::remove(const PointIndexEl& el) -{ - return m_impl->m_store.remove(el) == 1; -} - -std::vector -PointIndex::query(std::function fn) const -{ - namespace bgi = boost::geometry::index; - - std::vector ret; - m_impl->m_store.query(bgi::satisfies(fn), std::back_inserter(ret)); - return ret; -} - -std::vector PointIndex::nearest(const Vec3d &el, unsigned k = 1) const -{ - namespace bgi = boost::geometry::index; - std::vector ret; ret.reserve(k); - m_impl->m_store.query(bgi::nearest(el, k), std::back_inserter(ret)); - return ret; -} - -size_t PointIndex::size() const -{ - return m_impl->m_store.size(); -} - -void PointIndex::foreach(std::function fn) -{ - for(auto& el : m_impl->m_store) fn(el); -} - -void PointIndex::foreach(std::function fn) const -{ - for(const auto &el : m_impl->m_store) fn(el); -} - -/* ************************************************************************** - * BoxIndex implementation - * ************************************************************************** */ - -class BoxIndex::Impl { -public: - using BoostIndex = boost::geometry::index:: - rtree /* ? */>; - - BoostIndex m_store; -}; - -BoxIndex::BoxIndex(): m_impl(new Impl()) {} -BoxIndex::~BoxIndex() {} - -BoxIndex::BoxIndex(const BoxIndex &cpy): m_impl(new Impl(*cpy.m_impl)) {} -BoxIndex::BoxIndex(BoxIndex&& cpy): m_impl(std::move(cpy.m_impl)) {} - -BoxIndex& BoxIndex::operator=(const BoxIndex &cpy) -{ - m_impl.reset(new Impl(*cpy.m_impl)); - return *this; -} - -BoxIndex& BoxIndex::operator=(BoxIndex &&cpy) -{ - m_impl.swap(cpy.m_impl); - return *this; -} - -void BoxIndex::insert(const BoxIndexEl &el) -{ - m_impl->m_store.insert(el); -} - -bool BoxIndex::remove(const BoxIndexEl& el) -{ - return m_impl->m_store.remove(el) == 1; -} - -std::vector BoxIndex::query(const BoundingBox &qrbb, - BoxIndex::QueryType qt) -{ - namespace bgi = boost::geometry::index; - - std::vector ret; ret.reserve(m_impl->m_store.size()); - - switch (qt) { - case qtIntersects: - m_impl->m_store.query(bgi::intersects(qrbb), std::back_inserter(ret)); - break; - case qtWithin: - m_impl->m_store.query(bgi::within(qrbb), std::back_inserter(ret)); - } - - return ret; -} - -size_t BoxIndex::size() const -{ - return m_impl->m_store.size(); -} - -void BoxIndex::foreach(std::function fn) -{ - for(auto& el : m_impl->m_store) fn(el); -} - - -/* **************************************************************************** - * EigenMesh3D implementation - * ****************************************************************************/ - - -class EigenMesh3D::AABBImpl { +class IndexedMesh::AABBImpl { private: AABBTreeIndirect::Tree3f m_tree; @@ -189,7 +20,7 @@ public: void init(const TriangleMesh& tm) { m_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( - tm.its.vertices, tm.its.indices); + tm.its.vertices, tm.its.indices); } void intersect_ray(const TriangleMesh& tm, @@ -215,9 +46,9 @@ public: size_t idx_unsigned = 0; Vec3d closest_vec3d(closest); double dist = AABBTreeIndirect::squared_distance_to_indexed_triangle_set( - tm.its.vertices, - tm.its.indices, - m_tree, point, idx_unsigned, closest_vec3d); + tm.its.vertices, + tm.its.indices, + m_tree, point, idx_unsigned, closest_vec3d); i = int(idx_unsigned); closest = closest_vec3d; return dist; @@ -226,72 +57,71 @@ public: static const constexpr double MESH_EPS = 1e-6; -EigenMesh3D::EigenMesh3D(const TriangleMesh& tmesh) +IndexedMesh::IndexedMesh(const TriangleMesh& tmesh) : m_aabb(new AABBImpl()), m_tm(&tmesh) { auto&& bb = tmesh.bounding_box(); m_ground_level += bb.min(Z); - + // Build the AABB accelaration tree m_aabb->init(tmesh); } -EigenMesh3D::~EigenMesh3D() {} +IndexedMesh::~IndexedMesh() {} -EigenMesh3D::EigenMesh3D(const EigenMesh3D &other): +IndexedMesh::IndexedMesh(const IndexedMesh &other): m_tm(other.m_tm), m_ground_level(other.m_ground_level), m_aabb( new AABBImpl(*other.m_aabb) ) {} -EigenMesh3D &EigenMesh3D::operator=(const EigenMesh3D &other) +IndexedMesh &IndexedMesh::operator=(const IndexedMesh &other) { m_tm = other.m_tm; m_ground_level = other.m_ground_level; m_aabb.reset(new AABBImpl(*other.m_aabb)); return *this; } -EigenMesh3D &EigenMesh3D::operator=(EigenMesh3D &&other) = default; +IndexedMesh &IndexedMesh::operator=(IndexedMesh &&other) = default; -EigenMesh3D::EigenMesh3D(EigenMesh3D &&other) = default; +IndexedMesh::IndexedMesh(IndexedMesh &&other) = default; -const std::vector& EigenMesh3D::vertices() const +const std::vector& IndexedMesh::vertices() const { return m_tm->its.vertices; } -const std::vector& EigenMesh3D::indices() const +const std::vector& IndexedMesh::indices() const { return m_tm->its.indices; } -const Vec3f& EigenMesh3D::vertices(size_t idx) const +const Vec3f& IndexedMesh::vertices(size_t idx) const { return m_tm->its.vertices[idx]; } -const Vec3i& EigenMesh3D::indices(size_t idx) const +const Vec3i& IndexedMesh::indices(size_t idx) const { return m_tm->its.indices[idx]; } -Vec3d EigenMesh3D::normal_by_face_id(int face_id) const { +Vec3d IndexedMesh::normal_by_face_id(int face_id) const { return m_tm->stl.facet_start[face_id].normal.cast(); } - -EigenMesh3D::hit_result -EigenMesh3D::query_ray_hit(const Vec3d &s, const Vec3d &dir) const +IndexedMesh::hit_result +IndexedMesh::query_ray_hit(const Vec3d &s, const Vec3d &dir) const { assert(is_approx(dir.norm(), 1.)); igl::Hit hit; @@ -319,13 +149,13 @@ EigenMesh3D::query_ray_hit(const Vec3d &s, const Vec3d &dir) const return ret; } -std::vector -EigenMesh3D::query_ray_hits(const Vec3d &s, const Vec3d &dir) const +std::vector +IndexedMesh::query_ray_hits(const Vec3d &s, const Vec3d &dir) const { - std::vector outs; + std::vector outs; std::vector hits; m_aabb->intersect_ray(*m_tm, s, dir, hits); - + // The sort is necessary, the hits are not always sorted. std::sort(hits.begin(), hits.end(), [](const igl::Hit& a, const igl::Hit& b) { return a.t < b.t; }); @@ -334,13 +164,13 @@ EigenMesh3D::query_ray_hits(const Vec3d &s, const Vec3d &dir) const // along an axis of a cube due to floating-point approximations in igl (?) hits.erase(std::unique(hits.begin(), hits.end(), [](const igl::Hit& a, const igl::Hit& b) - { return a.t == b.t; }), + { return a.t == b.t; }), hits.end()); // Convert the igl::Hit into hit_result outs.reserve(hits.size()); for (const igl::Hit& hit : hits) { - outs.emplace_back(EigenMesh3D::hit_result(*this)); + outs.emplace_back(IndexedMesh::hit_result(*this)); outs.back().m_t = double(hit.t); outs.back().m_dir = dir; outs.back().m_source = s; @@ -355,8 +185,8 @@ EigenMesh3D::query_ray_hits(const Vec3d &s, const Vec3d &dir) const #ifdef SLIC3R_HOLE_RAYCASTER -EigenMesh3D::hit_result EigenMesh3D::filter_hits( - const std::vector& object_hits) const +IndexedMesh::hit_result IndexedMesh::filter_hits( + const std::vector& object_hits) const { assert(! m_holes.empty()); hit_result out(*this); @@ -377,7 +207,7 @@ EigenMesh3D::hit_result EigenMesh3D::filter_hits( }; std::vector hole_isects; hole_isects.reserve(m_holes.size()); - + auto sf = s.cast(); auto dirf = dir.cast(); @@ -452,7 +282,7 @@ EigenMesh3D::hit_result EigenMesh3D::filter_hits( #endif -double EigenMesh3D::squared_distance(const Vec3d &p, int& i, Vec3d& c) const { +double IndexedMesh::squared_distance(const Vec3d &p, int& i, Vec3d& c) const { double sqdst = 0; Eigen::Matrix pp = p; Eigen::Matrix cc; @@ -461,31 +291,19 @@ double EigenMesh3D::squared_distance(const Vec3d &p, int& i, Vec3d& c) const { return sqdst; } -/* **************************************************************************** - * Misc functions - * ****************************************************************************/ -namespace { - -bool point_on_edge(const Vec3d& p, const Vec3d& e1, const Vec3d& e2, - double eps = 0.05) +static bool point_on_edge(const Vec3d& p, const Vec3d& e1, const Vec3d& e2, + double eps = 0.05) { using Line3D = Eigen::ParametrizedLine; - + auto line = Line3D::Through(e1, e2); double d = line.distance(p); return std::abs(d) < eps; } -template double distance(const Vec& pp1, const Vec& pp2) { - auto p = pp2 - pp1; - return std::sqrt(p.transpose() * p); -} - -} - PointSet normals(const PointSet& points, - const EigenMesh3D& mesh, + const IndexedMesh& mesh, double eps, std::function thr, // throw on cancel const std::vector& pt_indices) @@ -502,10 +320,10 @@ PointSet normals(const PointSet& points, PointSet ret(range.size(), 3); // for (size_t ridx = 0; ridx < range.size(); ++ridx) - ccr::enumerate( - range.begin(), range.end(), - [&ret, &mesh, &points, thr, eps](unsigned el, size_t ridx) { + ccr::for_each(size_t(0), range.size(), + [&ret, &mesh, &points, thr, eps, &range](size_t ridx) { thr(); + unsigned el = range[ridx]; auto eidx = Eigen::Index(el); int faceid = 0; Vec3d p; @@ -531,11 +349,11 @@ PointSet normals(const PointSet& points, // ic will mark a single vertex. int ia = -1, ib = -1, ic = -1; - if (std::abs(distance(p, p1)) < eps) { + if (std::abs((p - p1).norm()) < eps) { ic = trindex(0); - } else if (std::abs(distance(p, p2)) < eps) { + } else if (std::abs((p - p2).norm()) < eps) { ic = trindex(1); - } else if (std::abs(distance(p, p3)) < eps) { + } else if (std::abs((p - p3).norm()) < eps) { ic = trindex(2); } else if (point_on_edge(p, p1, p2, eps)) { ia = trindex(0); @@ -612,148 +430,4 @@ PointSet normals(const PointSet& points, return ret; } -namespace bgi = boost::geometry::index; -using Index3D = bgi::rtree< PointIndexEl, bgi::rstar<16, 4> /* ? */ >; - -namespace { - -bool cmp_ptidx_elements(const PointIndexEl& e1, const PointIndexEl& e2) -{ - return e1.second < e2.second; -}; - -ClusteredPoints cluster(Index3D &sindex, - unsigned max_points, - std::function( - const Index3D &, const PointIndexEl &)> qfn) -{ - using Elems = std::vector; - - // Recursive function for visiting all the points in a given distance to - // each other - std::function group = - [&sindex, &group, max_points, qfn](Elems& pts, Elems& cluster) - { - for(auto& p : pts) { - std::vector tmp = qfn(sindex, p); - - std::sort(tmp.begin(), tmp.end(), cmp_ptidx_elements); - - Elems newpts; - std::set_difference(tmp.begin(), tmp.end(), - cluster.begin(), cluster.end(), - std::back_inserter(newpts), cmp_ptidx_elements); - - int c = max_points && newpts.size() + cluster.size() > max_points? - int(max_points - cluster.size()) : int(newpts.size()); - - cluster.insert(cluster.end(), newpts.begin(), newpts.begin() + c); - std::sort(cluster.begin(), cluster.end(), cmp_ptidx_elements); - - if(!newpts.empty() && (!max_points || cluster.size() < max_points)) - group(newpts, cluster); - } - }; - - std::vector clusters; - for(auto it = sindex.begin(); it != sindex.end();) { - Elems cluster = {}; - Elems pts = {*it}; - group(pts, cluster); - - for(auto& c : cluster) sindex.remove(c); - it = sindex.begin(); - - clusters.emplace_back(cluster); - } - - ClusteredPoints result; - for(auto& cluster : clusters) { - result.emplace_back(); - for(auto c : cluster) result.back().emplace_back(c.second); - } - - return result; -} - -std::vector distance_queryfn(const Index3D& sindex, - const PointIndexEl& p, - double dist, - unsigned max_points) -{ - std::vector tmp; tmp.reserve(max_points); - sindex.query( - bgi::nearest(p.first, max_points), - std::back_inserter(tmp) - ); - - for(auto it = tmp.begin(); it < tmp.end(); ++it) - if(distance(p.first, it->first) > dist) it = tmp.erase(it); - - return tmp; -} - -} // namespace - -// Clustering a set of points by the given criteria -ClusteredPoints cluster( - const std::vector& indices, - std::function pointfn, - double dist, - unsigned max_points) -{ - // A spatial index for querying the nearest points - Index3D sindex; - - // Build the index - for(auto idx : indices) sindex.insert( std::make_pair(pointfn(idx), idx)); - - return cluster(sindex, max_points, - [dist, max_points](const Index3D& sidx, const PointIndexEl& p) - { - return distance_queryfn(sidx, p, dist, max_points); - }); -} - -// Clustering a set of points by the given criteria -ClusteredPoints cluster( - const std::vector& indices, - std::function pointfn, - std::function predicate, - unsigned max_points) -{ - // A spatial index for querying the nearest points - Index3D sindex; - - // Build the index - for(auto idx : indices) sindex.insert( std::make_pair(pointfn(idx), idx)); - - return cluster(sindex, max_points, - [max_points, predicate](const Index3D& sidx, const PointIndexEl& p) - { - std::vector tmp; tmp.reserve(max_points); - sidx.query(bgi::satisfies([p, predicate](const PointIndexEl& e){ - return predicate(p, e); - }), std::back_inserter(tmp)); - return tmp; - }); -} - -ClusteredPoints cluster(const PointSet& pts, double dist, unsigned max_points) -{ - // A spatial index for querying the nearest points - Index3D sindex; - - // Build the index - for(Eigen::Index i = 0; i < pts.rows(); i++) - sindex.insert(std::make_pair(Vec3d(pts.row(i)), unsigned(i))); - - return cluster(sindex, max_points, - [dist, max_points](const Index3D& sidx, const PointIndexEl& p) - { - return distance_queryfn(sidx, p, dist, max_points); - }); -} - -} // namespace sla -} // namespace Slic3r +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/EigenMesh3D.hpp b/src/libslic3r/SLA/IndexedMesh.hpp similarity index 81% rename from src/libslic3r/SLA/EigenMesh3D.hpp rename to src/libslic3r/SLA/IndexedMesh.hpp index b932c0c18e..a72492b344 100644 --- a/src/libslic3r/SLA/EigenMesh3D.hpp +++ b/src/libslic3r/SLA/IndexedMesh.hpp @@ -1,8 +1,10 @@ -#ifndef SLA_EIGENMESH3D_H -#define SLA_EIGENMESH3D_H +#ifndef SLA_INDEXEDMESH_H +#define SLA_INDEXEDMESH_H -#include +#include +#include +#include // There is an implementation of a hole-aware raycaster that was eventually // not used in production version. It is now hidden under following define @@ -19,10 +21,12 @@ class TriangleMesh; namespace sla { +using PointSet = Eigen::MatrixXd; + /// An index-triangle structure for libIGL functions. Also serves as an /// alternative (raw) input format for the SLASupportTree. // Implemented in libslic3r/SLA/Common.cpp -class EigenMesh3D { +class IndexedMesh { class AABBImpl; const TriangleMesh* m_tm; @@ -38,15 +42,15 @@ class EigenMesh3D { public: - explicit EigenMesh3D(const TriangleMesh&); + explicit IndexedMesh(const TriangleMesh&); - EigenMesh3D(const EigenMesh3D& other); - EigenMesh3D& operator=(const EigenMesh3D&); + IndexedMesh(const IndexedMesh& other); + IndexedMesh& operator=(const IndexedMesh&); - EigenMesh3D(EigenMesh3D &&other); - EigenMesh3D& operator=(EigenMesh3D &&other); + IndexedMesh(IndexedMesh &&other); + IndexedMesh& operator=(IndexedMesh &&other); - ~EigenMesh3D(); + ~IndexedMesh(); inline double ground_level() const { return m_ground_level + m_gnd_offset; } inline void ground_level_offset(double o) { m_gnd_offset = o; } @@ -62,15 +66,15 @@ public: // m_t holds a distance from m_source to the intersection. double m_t = infty(); int m_face_id = -1; - const EigenMesh3D *m_mesh = nullptr; + const IndexedMesh *m_mesh = nullptr; Vec3d m_dir; Vec3d m_source; Vec3d m_normal; - friend class EigenMesh3D; + friend class IndexedMesh; // A valid object of this class can only be obtained from - // EigenMesh3D::query_ray_hit method. - explicit inline hit_result(const EigenMesh3D& em): m_mesh(&em) {} + // IndexedMesh::query_ray_hit method. + explicit inline hit_result(const IndexedMesh& em): m_mesh(&em) {} public: // This denotes no hit on the mesh. static inline constexpr double infty() { return std::numeric_limits::infinity(); } @@ -83,7 +87,7 @@ public: inline Vec3d position() const { return m_source + m_dir * m_t; } inline int face() const { return m_face_id; } inline bool is_valid() const { return m_mesh != nullptr; } - inline bool is_hit() const { return !std::isinf(m_t); } + inline bool is_hit() const { return m_face_id >= 0 && !std::isinf(m_t); } inline const Vec3d& normal() const { assert(is_valid()); @@ -107,7 +111,7 @@ public: // This function is currently not used anywhere, it was written when the // holes were subtracted on slices, that is, before we started using CGAL // to actually cut the holes into the mesh. - hit_result filter_hits(const std::vector& obj_hits) const; + hit_result filter_hits(const std::vector& obj_hits) const; #endif // Casting a ray on the mesh, returns the distance where the hit occures. @@ -125,16 +129,18 @@ public: } Vec3d normal_by_face_id(int face_id) const; + + const TriangleMesh * get_triangle_mesh() const { return m_tm; } }; // Calculate the normals for the selected points (from 'points' set) on the // mesh. This will call squared distance for each point. PointSet normals(const PointSet& points, - const EigenMesh3D& convert_mesh, + const IndexedMesh& convert_mesh, double eps = 0.05, // min distance from edges std::function throw_on_cancel = [](){}, const std::vector& selected_points = {}); }} // namespace Slic3r::sla -#endif // EIGENMESH3D_H +#endif // INDEXEDMESH_H diff --git a/src/libslic3r/SLA/JobController.hpp b/src/libslic3r/SLA/JobController.hpp index 3baa3d12d1..b815e4d6fc 100644 --- a/src/libslic3r/SLA/JobController.hpp +++ b/src/libslic3r/SLA/JobController.hpp @@ -2,6 +2,7 @@ #define SLA_JOBCONTROLLER_HPP #include +#include namespace Slic3r { namespace sla { diff --git a/src/libslic3r/SLA/Pad.cpp b/src/libslic3r/SLA/Pad.cpp index d933ef5ed7..f2b189cd11 100644 --- a/src/libslic3r/SLA/Pad.cpp +++ b/src/libslic3r/SLA/Pad.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include diff --git a/src/libslic3r/SLA/ReprojectPointsOnMesh.hpp b/src/libslic3r/SLA/ReprojectPointsOnMesh.hpp index 702d1bce18..20804193e2 100644 --- a/src/libslic3r/SLA/ReprojectPointsOnMesh.hpp +++ b/src/libslic3r/SLA/ReprojectPointsOnMesh.hpp @@ -4,7 +4,7 @@ #include "libslic3r/Point.hpp" #include "SupportPoint.hpp" #include "Hollowing.hpp" -#include "EigenMesh3D.hpp" +#include "IndexedMesh.hpp" #include "libslic3r/Model.hpp" #include @@ -15,7 +15,7 @@ template Vec3d pos(const Pt &p) { return p.pos.template cast() template void pos(Pt &p, const Vec3d &pp) { p.pos = pp.cast(); } template -void reproject_support_points(const EigenMesh3D &mesh, std::vector &pts) +void reproject_support_points(const IndexedMesh &mesh, std::vector &pts) { tbb::parallel_for(size_t(0), pts.size(), [&mesh, &pts](size_t idx) { int junk; @@ -28,25 +28,19 @@ void reproject_support_points(const EigenMesh3D &mesh, std::vector &p inline void reproject_points_and_holes(ModelObject *object) { bool has_sppoints = !object->sla_support_points.empty(); + bool has_holes = !object->sla_drain_holes.empty(); - // Disabling reprojection of holes as they have a significant offset away - // from the model body which tolerates minor geometrical changes. - // - // TODO: uncomment and ensure the right offset of the hole points if - // reprojection would still be necessary. - // bool has_holes = !object->sla_drain_holes.empty(); - - if (!object || (/*!has_holes &&*/ !has_sppoints)) return; + if (!object || (!has_holes && !has_sppoints)) return; TriangleMesh rmsh = object->raw_mesh(); rmsh.require_shared_vertices(); - EigenMesh3D emesh{rmsh}; + IndexedMesh emesh{rmsh}; if (has_sppoints) reproject_support_points(emesh, object->sla_support_points); -// if (has_holes) -// reproject_support_points(emesh, object->sla_drain_holes); + if (has_holes) + reproject_support_points(emesh, object->sla_drain_holes); } }} diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index fda8383b11..9378977663 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -1,36 +1,259 @@ #include -#include -#include -#include #include -#include +#include + +#include + +#include "libslic3r/SLAPrint.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include #include "Model.hpp" -namespace Slic3r { -namespace sla { +#include -std::array find_best_rotation(const ModelObject& modelobj, - float accuracy, - std::function statuscb, - std::function stopcond) +namespace Slic3r { namespace sla { + +inline bool is_on_floor(const SLAPrintObject &mo) { - using libnest2d::opt::Method; - using libnest2d::opt::bound; - using libnest2d::opt::Optimizer; - using libnest2d::opt::TOptimizer; - using libnest2d::opt::StopCriteria; + auto opt_elevation = mo.config().support_object_elevation.getFloat(); + auto opt_padaround = mo.config().pad_around_object.getBool(); - static const unsigned MAX_TRIES = 100000; + return opt_elevation < EPSILON || opt_padaround; +} + +// Find transformed mesh ground level without copy and with parallel reduce. +double find_ground_level(const TriangleMesh &mesh, + const Transform3d & tr, + size_t threads) +{ + size_t vsize = mesh.its.vertices.size(); + + auto minfn = [](double a, double b) { return std::min(a, b); }; + + auto accessfn = [&mesh, &tr] (size_t vi) { + return (tr * mesh.its.vertices[vi].template cast()).z(); + }; + + double zmin = std::numeric_limits::max(); + size_t granularity = vsize / threads; + return ccr_par::reduce(size_t(0), vsize, zmin, minfn, accessfn, granularity); +} + +// Get the vertices of a triangle directly in an array of 3 points +std::array get_triangle_vertices(const TriangleMesh &mesh, + size_t faceidx) +{ + const auto &face = mesh.its.indices[faceidx]; + return {Vec3d{mesh.its.vertices[face(0)].cast()}, + Vec3d{mesh.its.vertices[face(1)].cast()}, + Vec3d{mesh.its.vertices[face(2)].cast()}}; +} + +std::array get_transformed_triangle(const TriangleMesh &mesh, + const Transform3d & tr, + size_t faceidx) +{ + const auto &tri = get_triangle_vertices(mesh, faceidx); + return {tr * tri[0], tr * tri[1], tr * tri[2]}; +} + +// Get area and normal of a triangle +struct Facestats { + Vec3d normal; + double area; + + explicit Facestats(const std::array &triangle) + { + Vec3d U = triangle[1] - triangle[0]; + Vec3d V = triangle[2] - triangle[0]; + Vec3d C = U.cross(V); + normal = C.normalized(); + area = 0.5 * C.norm(); + } +}; + +inline const Vec3d DOWN = {0., 0., -1.}; +constexpr double POINTS_PER_UNIT_AREA = 1.; + +// The score function for a particular face +inline double get_score(const Facestats &fc) +{ + // Simply get the angle (acos of dot product) between the face normal and + // the DOWN vector. + double phi = 1. - std::acos(fc.normal.dot(DOWN)) / PI; + + // Only consider faces that have have slopes below 90 deg: + phi = phi * (phi > 0.5); + + // Make the huge slopes more significant than the smaller slopes + phi = phi * phi * phi; + + // Multiply with the area of the current face + return fc.area * POINTS_PER_UNIT_AREA * phi; +} + +template +double sum_score(AccessFn &&accessfn, size_t facecount, size_t Nthreads) +{ + double initv = 0.; + auto mergefn = std::plus{}; + size_t grainsize = facecount / Nthreads; + size_t from = 0, to = facecount; + + return ccr_par::reduce(from, to, initv, mergefn, accessfn, grainsize); +} + +// Try to guess the number of support points needed to support a mesh +double get_model_supportedness(const TriangleMesh &mesh, const Transform3d &tr) +{ + if (mesh.its.vertices.empty()) return std::nan(""); + + auto accessfn = [&mesh, &tr](size_t fi) { + Facestats fc{get_transformed_triangle(mesh, tr, fi)}; + return get_score(fc); + }; + + size_t facecount = mesh.its.indices.size(); + size_t Nthreads = std::thread::hardware_concurrency(); + return sum_score(accessfn, facecount, Nthreads) / facecount; +} + +double get_model_supportedness_onfloor(const TriangleMesh &mesh, + const Transform3d & tr) +{ + if (mesh.its.vertices.empty()) return std::nan(""); + + size_t Nthreads = std::thread::hardware_concurrency(); + + double zmin = find_ground_level(mesh, tr, Nthreads); + double zlvl = zmin + 0.1; // Set up a slight tolerance from z level + + auto accessfn = [&mesh, &tr, zlvl](size_t fi) { + std::array tri = get_transformed_triangle(mesh, tr, fi); + Facestats fc{tri}; + + if (tri[0].z() <= zlvl && tri[1].z() <= zlvl && tri[2].z() <= zlvl) + return -fc.area * POINTS_PER_UNIT_AREA; + + return get_score(fc); + }; + + size_t facecount = mesh.its.indices.size(); + return sum_score(accessfn, facecount, Nthreads) / facecount; +} + +using XYRotation = std::array; + +// prepare the rotation transformation +Transform3d to_transform3d(const XYRotation &rot) +{ + Transform3d rt = Transform3d::Identity(); + rt.rotate(Eigen::AngleAxisd(rot[1], Vec3d::UnitY())); + rt.rotate(Eigen::AngleAxisd(rot[0], Vec3d::UnitX())); + return rt; +} + +XYRotation from_transform3d(const Transform3d &tr) +{ + Vec3d rot3d = Geometry::Transformation {tr}.get_rotation(); + return {rot3d.x(), rot3d.y()}; +} + +// Find the best score from a set of function inputs. Evaluate for every point. +template +std::array find_min_score(Fn &&fn, It from, It to, StopCond &&stopfn) +{ + std::array ret; + + double score = std::numeric_limits::max(); + + size_t Nthreads = std::thread::hardware_concurrency(); + size_t dist = std::distance(from, to); + std::vector scores(dist, score); + + ccr_par::for_each(size_t(0), dist, [&stopfn, &scores, &fn, &from](size_t i) { + if (stopfn()) return; + + scores[i] = fn(*(from + i)); + }, dist / Nthreads); + + auto it = std::min_element(scores.begin(), scores.end()); + + if (it != scores.end()) ret = *(from + std::distance(scores.begin(), it)); + + return ret; +} + +// collect the rotations for each face of the convex hull +std::vector get_chull_rotations(const TriangleMesh &mesh, size_t max_count) +{ + TriangleMesh chull = mesh.convex_hull_3d(); + chull.require_shared_vertices(); + double chull2d_area = chull.convex_hull().area(); + double area_threshold = chull2d_area / (scaled(1e3) * scaled(1.)); + + size_t facecount = chull.its.indices.size(); + + struct RotArea { XYRotation rot; double area; }; + + auto inputs = reserve_vector(facecount); + + auto rotcmp = [](const RotArea &r1, const RotArea &r2) { + double xdiff = r1.rot[X] - r2.rot[X], ydiff = r1.rot[Y] - r2.rot[Y]; + return std::abs(xdiff) < EPSILON ? ydiff < 0. : xdiff < 0.; + }; + + auto eqcmp = [](const XYRotation &r1, const XYRotation &r2) { + double xdiff = r1[X] - r2[X], ydiff = r1[Y] - r2[Y]; + return std::abs(xdiff) < EPSILON && std::abs(ydiff) < EPSILON; + }; + + for (size_t fi = 0; fi < facecount; ++fi) { + Facestats fc{get_triangle_vertices(chull, fi)}; + + if (fc.area > area_threshold) { + auto q = Eigen::Quaterniond{}.FromTwoVectors(fc.normal, DOWN); + XYRotation rot = from_transform3d(Transform3d::Identity() * q); + RotArea ra = {rot, fc.area}; + + auto it = std::lower_bound(inputs.begin(), inputs.end(), ra, rotcmp); + + if (it == inputs.end() || !eqcmp(it->rot, rot)) + inputs.insert(it, ra); + } + } + + inputs.shrink_to_fit(); + if (!max_count) max_count = inputs.size(); + std::sort(inputs.begin(), inputs.end(), + [](const RotArea &ra, const RotArea &rb) { + return ra.area > rb.area; + }); + + auto ret = reserve_vector(std::min(max_count, inputs.size())); + for (const RotArea &ra : inputs) ret.emplace_back(ra.rot); + + return ret; +} + +Vec2d find_best_rotation(const SLAPrintObject & po, + float accuracy, + std::function statuscb, + std::function stopcond) +{ + static const unsigned MAX_TRIES = 1000; // return value - std::array rot; + XYRotation rot; // We will use only one instance of this converted mesh to examine different // rotations - const TriangleMesh& mesh = modelobj.raw_mesh(); + TriangleMesh mesh = po.model_object()->raw_mesh(); + mesh.require_shared_vertices(); - // For current iteration number + // To keep track of the number of iterations unsigned status = 0; // The maximum number of iterations @@ -39,77 +262,61 @@ std::array find_best_rotation(const ModelObject& modelobj, // call status callback with zero, because we are at the start statuscb(status); - // So this is the object function which is called by the solver many times - // It has to yield a single value representing the current score. We will - // call the status callback in each iteration but the actual value may be - // the same for subsequent iterations (status goes from 0 to 100 but - // iterations can be many more) - auto objfunc = [&mesh, &status, &statuscb, &stopcond, max_tries] - (double rx, double ry, double rz) - { - const TriangleMesh& m = mesh; - - // prepare the rotation transformation - Transform3d rt = Transform3d::Identity(); - - rt.rotate(Eigen::AngleAxisd(rz, Vec3d::UnitZ())); - rt.rotate(Eigen::AngleAxisd(ry, Vec3d::UnitY())); - rt.rotate(Eigen::AngleAxisd(rx, Vec3d::UnitX())); - - double score = 0; - - // For all triangles we calculate the normal and sum up the dot product - // (a scalar indicating how much are two vectors aligned) with each axis - // this will result in a value that is greater if a normal is aligned - // with all axes. If the normal is aligned than the triangle itself is - // orthogonal to the axes and that is good for print quality. - - // TODO: some applications optimize for minimum z-axis cross section - // area. The current function is only an example of how to optimize. - - // Later we can add more criteria like the number of overhangs, etc... - for(size_t i = 0; i < m.stl.facet_start.size(); i++) { - Vec3d n = m.stl.facet_start[i].normal.cast(); - - // rotate the normal with the current rotation given by the solver - n = rt * n; - - // We should score against the alignment with the reference planes - score += std::abs(n.dot(Vec3d::UnitX())); - score += std::abs(n.dot(Vec3d::UnitY())); - score += std::abs(n.dot(Vec3d::UnitZ())); - } - + auto statusfn = [&statuscb, &status, &max_tries] { // report status - if(!stopcond()) statuscb( unsigned(++status * 100.0/max_tries) ); - - return score; + statuscb(unsigned(++status * 100.0/max_tries) ); }; - // Firing up the genetic optimizer. For now it uses the nlopt library. - StopCriteria stc; - stc.max_iterations = max_tries; - stc.relative_score_difference = 1e-3; - stc.stop_condition = stopcond; // stop when stopcond returns true - TOptimizer solver(stc); + // Different search methods have to be used depending on the model elevation + if (is_on_floor(po)) { - // We are searching rotations around the three axes x, y, z. Thus the - // problem becomes a 3 dimensional optimization task. - // We can specify the bounds for a dimension in the following way: - auto b = bound(-PI/2, PI/2); + std::vector inputs = get_chull_rotations(mesh, max_tries); + max_tries = inputs.size(); - // Now we start the optimization process with initial angles (0, 0, 0) - auto result = solver.optimize_max(objfunc, - libnest2d::opt::initvals(0.0, 0.0, 0.0), - b, b, b); + // If the model can be placed on the bed directly, we only need to + // check the 3D convex hull face rotations. - // Save the result and fck off - rot[0] = std::get<0>(result.optimum); - rot[1] = std::get<1>(result.optimum); - rot[2] = std::get<2>(result.optimum); + auto objfn = [&mesh, &statusfn](const XYRotation &rot) { + statusfn(); + Transform3d tr = to_transform3d(rot); + return get_model_supportedness_onfloor(mesh, tr); + }; - return rot; + rot = find_min_score<2>(objfn, inputs.begin(), inputs.end(), stopcond); + } else { + // Preparing the optimizer. + size_t gridsize = std::sqrt(max_tries); // 2D grid has gridsize^2 calls + opt::Optimizer solver(opt::StopCriteria{} + .max_iterations(max_tries) + .stop_condition(stopcond), + gridsize); + + // We are searching rotations around only two axes x, y. Thus the + // problem becomes a 2 dimensional optimization task. + // We can specify the bounds for a dimension in the following way: + auto bounds = opt::bounds({ {-PI, PI}, {-PI, PI} }); + + auto result = solver.to_min().optimize( + [&mesh, &statusfn] (const XYRotation &rot) + { + statusfn(); + return get_model_supportedness(mesh, to_transform3d(rot)); + }, opt::initvals({0., 0.}), bounds); + + // Save the result and fck off + rot = result.optimum; + } + + return {rot[0], rot[1]}; } +double get_model_supportedness(const SLAPrintObject &po, const Transform3d &tr) +{ + TriangleMesh mesh = po.model_object()->raw_mesh(); + mesh.require_shared_vertices(); + + return is_on_floor(po) ? get_model_supportedness_onfloor(mesh, tr) : + get_model_supportedness(mesh, tr); } -} + +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/Rotfinder.hpp b/src/libslic3r/SLA/Rotfinder.hpp index 4469f9731d..96561a890f 100644 --- a/src/libslic3r/SLA/Rotfinder.hpp +++ b/src/libslic3r/SLA/Rotfinder.hpp @@ -4,9 +4,11 @@ #include #include +#include + namespace Slic3r { -class ModelObject; +class SLAPrintObject; namespace sla { @@ -25,14 +27,17 @@ namespace sla { * * @return Returns the rotations around each axis (x, y, z) */ -std::array find_best_rotation( - const ModelObject& modelobj, +Vec2d find_best_rotation( + const SLAPrintObject& modelobj, float accuracy = 1.0f, std::function statuscb = [] (unsigned) {}, std::function stopcond = [] () { return false; } ); -} -} +double get_model_supportedness(const SLAPrintObject &mesh, + const Transform3d & tr); + +} // namespace sla +} // namespace Slic3r #endif // SLAROTFINDER_HPP diff --git a/src/libslic3r/SLA/SpatIndex.cpp b/src/libslic3r/SLA/SpatIndex.cpp new file mode 100644 index 0000000000..d95ba55bee --- /dev/null +++ b/src/libslic3r/SLA/SpatIndex.cpp @@ -0,0 +1,161 @@ +#include "SpatIndex.hpp" + +// for concave hull merging decisions +#include + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4244) +#pragma warning(disable: 4267) +#endif + +#include "boost/geometry/index/rtree.hpp" + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +namespace Slic3r { namespace sla { + +/* ************************************************************************** + * PointIndex implementation + * ************************************************************************** */ + +class PointIndex::Impl { +public: + using BoostIndex = boost::geometry::index::rtree< PointIndexEl, + boost::geometry::index::rstar<16, 4> /* ? */ >; + + BoostIndex m_store; +}; + +PointIndex::PointIndex(): m_impl(new Impl()) {} +PointIndex::~PointIndex() {} + +PointIndex::PointIndex(const PointIndex &cpy): m_impl(new Impl(*cpy.m_impl)) {} +PointIndex::PointIndex(PointIndex&& cpy): m_impl(std::move(cpy.m_impl)) {} + +PointIndex& PointIndex::operator=(const PointIndex &cpy) +{ + m_impl.reset(new Impl(*cpy.m_impl)); + return *this; +} + +PointIndex& PointIndex::operator=(PointIndex &&cpy) +{ + m_impl.swap(cpy.m_impl); + return *this; +} + +void PointIndex::insert(const PointIndexEl &el) +{ + m_impl->m_store.insert(el); +} + +bool PointIndex::remove(const PointIndexEl& el) +{ + return m_impl->m_store.remove(el) == 1; +} + +std::vector +PointIndex::query(std::function fn) const +{ + namespace bgi = boost::geometry::index; + + std::vector ret; + m_impl->m_store.query(bgi::satisfies(fn), std::back_inserter(ret)); + return ret; +} + +std::vector PointIndex::nearest(const Vec3d &el, unsigned k = 1) const +{ + namespace bgi = boost::geometry::index; + std::vector ret; ret.reserve(k); + m_impl->m_store.query(bgi::nearest(el, k), std::back_inserter(ret)); + return ret; +} + +size_t PointIndex::size() const +{ + return m_impl->m_store.size(); +} + +void PointIndex::foreach(std::function fn) +{ + for(auto& el : m_impl->m_store) fn(el); +} + +void PointIndex::foreach(std::function fn) const +{ + for(const auto &el : m_impl->m_store) fn(el); +} + +/* ************************************************************************** + * BoxIndex implementation + * ************************************************************************** */ + +class BoxIndex::Impl { +public: + using BoostIndex = boost::geometry::index:: + rtree /* ? */>; + + BoostIndex m_store; +}; + +BoxIndex::BoxIndex(): m_impl(new Impl()) {} +BoxIndex::~BoxIndex() {} + +BoxIndex::BoxIndex(const BoxIndex &cpy): m_impl(new Impl(*cpy.m_impl)) {} +BoxIndex::BoxIndex(BoxIndex&& cpy): m_impl(std::move(cpy.m_impl)) {} + +BoxIndex& BoxIndex::operator=(const BoxIndex &cpy) +{ + m_impl.reset(new Impl(*cpy.m_impl)); + return *this; +} + +BoxIndex& BoxIndex::operator=(BoxIndex &&cpy) +{ + m_impl.swap(cpy.m_impl); + return *this; +} + +void BoxIndex::insert(const BoxIndexEl &el) +{ + m_impl->m_store.insert(el); +} + +bool BoxIndex::remove(const BoxIndexEl& el) +{ + return m_impl->m_store.remove(el) == 1; +} + +std::vector BoxIndex::query(const BoundingBox &qrbb, + BoxIndex::QueryType qt) +{ + namespace bgi = boost::geometry::index; + + std::vector ret; ret.reserve(m_impl->m_store.size()); + + switch (qt) { + case qtIntersects: + m_impl->m_store.query(bgi::intersects(qrbb), std::back_inserter(ret)); + break; + case qtWithin: + m_impl->m_store.query(bgi::within(qrbb), std::back_inserter(ret)); + } + + return ret; +} + +size_t BoxIndex::size() const +{ + return m_impl->m_store.size(); +} + +void BoxIndex::foreach(std::function fn) +{ + for(auto& el : m_impl->m_store) fn(el); +} + +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SpatIndex.hpp b/src/libslic3r/SLA/SpatIndex.hpp index 2955cdcdf6..ef059d3ae6 100644 --- a/src/libslic3r/SLA/SpatIndex.hpp +++ b/src/libslic3r/SLA/SpatIndex.hpp @@ -73,7 +73,7 @@ public: BoxIndex& operator=(BoxIndex&&); void insert(const BoxIndexEl&); - inline void insert(const BoundingBox& bb, unsigned idx) + void insert(const BoundingBox& bb, unsigned idx) { insert(std::make_pair(bb, unsigned(idx))); } diff --git a/src/libslic3r/SLA/SupportPoint.hpp b/src/libslic3r/SLA/SupportPoint.hpp index 202a614c32..2b973697bb 100644 --- a/src/libslic3r/SLA/SupportPoint.hpp +++ b/src/libslic3r/SLA/SupportPoint.hpp @@ -2,7 +2,6 @@ #define SLA_SUPPORTPOINT_HPP #include -#include #include namespace Slic3r { namespace sla { @@ -29,13 +28,13 @@ struct SupportPoint float pos_y, float pos_z, float head_radius, - bool new_island) + bool new_island = false) : pos(pos_x, pos_y, pos_z) , head_front_radius(head_radius) , is_new_island(new_island) {} - SupportPoint(Vec3f position, float head_radius, bool new_island) + SupportPoint(Vec3f position, float head_radius, bool new_island = false) : pos(position) , head_front_radius(head_radius) , is_new_island(new_island) diff --git a/src/libslic3r/SLA/SupportPointGenerator.cpp b/src/libslic3r/SLA/SupportPointGenerator.cpp index 78c2ced356..7e884b6e3c 100644 --- a/src/libslic3r/SLA/SupportPointGenerator.cpp +++ b/src/libslic3r/SLA/SupportPointGenerator.cpp @@ -4,6 +4,7 @@ #include #include "SupportPointGenerator.hpp" +#include "Concurrency.hpp" #include "Model.hpp" #include "ExPolygon.hpp" #include "SVG.hpp" @@ -50,7 +51,7 @@ float SupportPointGenerator::distance_limit(float angle) const }*/ SupportPointGenerator::SupportPointGenerator( - const sla::EigenMesh3D &emesh, + const sla::IndexedMesh &emesh, const std::vector &slices, const std::vector & heights, const Config & config, @@ -64,7 +65,7 @@ SupportPointGenerator::SupportPointGenerator( } SupportPointGenerator::SupportPointGenerator( - const EigenMesh3D &emesh, + const IndexedMesh &emesh, const SupportPointGenerator::Config &config, std::function throw_on_cancel, std::function statusfn) @@ -87,27 +88,28 @@ void SupportPointGenerator::project_onto_mesh(std::vector& po // The function makes sure that all the points are really exactly placed on the mesh. // Use a reasonable granularity to account for the worker thread synchronization cost. - tbb::parallel_for(tbb::blocked_range(0, points.size(), 64), - [this, &points](const tbb::blocked_range& range) { - for (size_t point_id = range.begin(); point_id < range.end(); ++ point_id) { - if ((point_id % 16) == 0) - // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. - m_throw_on_cancel(); - Vec3f& p = points[point_id].pos; - // Project the point upward and downward and choose the closer intersection with the mesh. - sla::EigenMesh3D::hit_result hit_up = m_emesh.query_ray_hit(p.cast(), Vec3d(0., 0., 1.)); - sla::EigenMesh3D::hit_result hit_down = m_emesh.query_ray_hit(p.cast(), Vec3d(0., 0., -1.)); + static constexpr size_t gransize = 64; - bool up = hit_up.is_hit(); - bool down = hit_down.is_hit(); + ccr_par::for_each(size_t(0), points.size(), [this, &points](size_t idx) + { + if ((idx % 16) == 0) + // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. + m_throw_on_cancel(); - if (!up && !down) - continue; + Vec3f& p = points[idx].pos; + // Project the point upward and downward and choose the closer intersection with the mesh. + sla::IndexedMesh::hit_result hit_up = m_emesh.query_ray_hit(p.cast(), Vec3d(0., 0., 1.)); + sla::IndexedMesh::hit_result hit_down = m_emesh.query_ray_hit(p.cast(), Vec3d(0., 0., -1.)); - sla::EigenMesh3D::hit_result& hit = (!down || (hit_up.distance() < hit_down.distance())) ? hit_up : hit_down; - p = p + (hit.distance() * hit.direction()).cast(); - } - }); + bool up = hit_up.is_hit(); + bool down = hit_down.is_hit(); + + if (!up && !down) + return; + + sla::IndexedMesh::hit_result& hit = (!down || (hit_up.distance() < hit_down.distance())) ? hit_up : hit_down; + p = p + (hit.distance() * hit.direction()).cast(); + }, gransize); } static std::vector make_layers( @@ -126,78 +128,97 @@ static std::vector make_layers( //const float pixel_area = pow(wxGetApp().preset_bundle->project_config.option("display_width") / wxGetApp().preset_bundle->project_config.option("display_pixels_x"), 2.f); // const float pixel_area = pow(0.047f, 2.f); - // Use a reasonable granularity to account for the worker thread synchronization cost. - tbb::parallel_for(tbb::blocked_range(0, layers.size(), 32), - [&layers, &slices, &heights, pixel_area, throw_on_cancel](const tbb::blocked_range& range) { - for (size_t layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { - if ((layer_id % 8) == 0) - // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. - throw_on_cancel(); - SupportPointGenerator::MyLayer &layer = layers[layer_id]; - const ExPolygons &islands = slices[layer_id]; - //FIXME WTF? - const float height = (layer_id>2 ? heights[layer_id-3] : heights[0]-(heights[1]-heights[0])); - layer.islands.reserve(islands.size()); - for (const ExPolygon &island : islands) { - float area = float(island.area() * SCALING_FACTOR * SCALING_FACTOR); - if (area >= pixel_area) - //FIXME this is not a correct centroid of a polygon with holes. - layer.islands.emplace_back(layer, island, get_extents(island.contour), Slic3r::unscale(island.contour.centroid()).cast(), area, height); - } - } - }); + ccr_par::for_each(size_t(0), layers.size(), + [&layers, &slices, &heights, pixel_area, throw_on_cancel](size_t layer_id) + { + if ((layer_id % 8) == 0) + // Don't call the following function too often as it flushes + // CPU write caches due to synchronization primitves. + throw_on_cancel(); + + SupportPointGenerator::MyLayer &layer = layers[layer_id]; + const ExPolygons & islands = slices[layer_id]; + // FIXME WTF? + const float height = (layer_id > 2 ? + heights[layer_id - 3] : + heights[0] - (heights[1] - heights[0])); + layer.islands.reserve(islands.size()); + for (const ExPolygon &island : islands) { + float area = float(island.area() * SCALING_FACTOR * SCALING_FACTOR); + if (area >= pixel_area) + // FIXME this is not a correct centroid of a polygon with holes. + layer.islands.emplace_back(layer, island, get_extents(island.contour), + unscaled(island.contour.centroid()), area, height); + } + }, 32 /*gransize*/); // Calculate overlap of successive layers. Link overlapping islands. - tbb::parallel_for(tbb::blocked_range(1, layers.size(), 8), - [&layers, &heights, throw_on_cancel](const tbb::blocked_range& range) { - for (size_t layer_id = range.begin(); layer_id < range.end(); ++layer_id) { - if ((layer_id % 2) == 0) - // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. - throw_on_cancel(); - SupportPointGenerator::MyLayer &layer_above = layers[layer_id]; - SupportPointGenerator::MyLayer &layer_below = layers[layer_id - 1]; - //FIXME WTF? - const float layer_height = (layer_id!=0 ? heights[layer_id]-heights[layer_id-1] : heights[0]); - const float safe_angle = 5.f * (float(M_PI)/180.f); // smaller number - less supports - const float between_layers_offset = float(scale_(layer_height / std::tan(safe_angle))); - const float slope_angle = 75.f * (float(M_PI)/180.f); // smaller number - less supports - const float slope_offset = float(scale_(layer_height / std::tan(slope_angle))); - //FIXME This has a quadratic time complexity, it will be excessively slow for many tiny islands. - for (SupportPointGenerator::Structure &top : layer_above.islands) { - for (SupportPointGenerator::Structure &bottom : layer_below.islands) { - float overlap_area = top.overlap_area(bottom); - if (overlap_area > 0) { - top.islands_below.emplace_back(&bottom, overlap_area); - bottom.islands_above.emplace_back(&top, overlap_area); - } - } - if (! top.islands_below.empty()) { - Polygons top_polygons = to_polygons(*top.polygon); - Polygons bottom_polygons = top.polygons_below(); - top.overhangs = diff_ex(top_polygons, bottom_polygons); - if (! top.overhangs.empty()) { - top.overhangs_area = 0.f; - std::vector> expolys_with_areas; - for (ExPolygon &ex : top.overhangs) { - float area = float(ex.area()); - expolys_with_areas.emplace_back(&ex, area); - top.overhangs_area += area; - } - std::sort(expolys_with_areas.begin(), expolys_with_areas.end(), + ccr_par::for_each(size_t(1), layers.size(), + [&layers, &heights, throw_on_cancel] (size_t layer_id) + { + if ((layer_id % 2) == 0) + // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. + throw_on_cancel(); + SupportPointGenerator::MyLayer &layer_above = layers[layer_id]; + SupportPointGenerator::MyLayer &layer_below = layers[layer_id - 1]; + //FIXME WTF? + const float layer_height = (layer_id!=0 ? heights[layer_id]-heights[layer_id-1] : heights[0]); + const float safe_angle = 35.f * (float(M_PI)/180.f); // smaller number - less supports + const float between_layers_offset = scaled(layer_height * std::tan(safe_angle)); + const float slope_angle = 75.f * (float(M_PI)/180.f); // smaller number - less supports + const float slope_offset = scaled(layer_height * std::tan(slope_angle)); + //FIXME This has a quadratic time complexity, it will be excessively slow for many tiny islands. + for (SupportPointGenerator::Structure &top : layer_above.islands) { + for (SupportPointGenerator::Structure &bottom : layer_below.islands) { + float overlap_area = top.overlap_area(bottom); + if (overlap_area > 0) { + top.islands_below.emplace_back(&bottom, overlap_area); + bottom.islands_above.emplace_back(&top, overlap_area); + } + } + if (! top.islands_below.empty()) { + Polygons top_polygons = to_polygons(*top.polygon); + Polygons bottom_polygons = top.polygons_below(); + top.overhangs = diff_ex(top_polygons, bottom_polygons); + if (! top.overhangs.empty()) { + + // Produce 2 bands around the island, a safe band for dangling overhangs + // and an unsafe band for sloped overhangs. + // These masks include the original island + auto dangl_mask = offset(bottom_polygons, between_layers_offset, ClipperLib::jtSquare); + auto overh_mask = offset(bottom_polygons, slope_offset, ClipperLib::jtSquare); + + // Absolutely hopeless overhangs are those outside the unsafe band + top.overhangs = diff_ex(top_polygons, overh_mask); + + // Now cut out the supported core from the safe band + // and cut the safe band from the unsafe band to get distinct + // zones. + overh_mask = diff(overh_mask, dangl_mask); + dangl_mask = diff(dangl_mask, bottom_polygons); + + top.dangling_areas = intersection_ex(top_polygons, dangl_mask); + top.overhangs_slopes = intersection_ex(top_polygons, overh_mask); + + top.overhangs_area = 0.f; + std::vector> expolys_with_areas; + for (ExPolygon &ex : top.overhangs) { + float area = float(ex.area()); + expolys_with_areas.emplace_back(&ex, area); + top.overhangs_area += area; + } + std::sort(expolys_with_areas.begin(), expolys_with_areas.end(), [](const std::pair &p1, const std::pair &p2) - { return p1.second > p2.second; }); - ExPolygons overhangs_sorted; - for (auto &p : expolys_with_areas) - overhangs_sorted.emplace_back(std::move(*p.first)); - top.overhangs = std::move(overhangs_sorted); - top.overhangs_area *= float(SCALING_FACTOR * SCALING_FACTOR); - top.overhangs_slopes = diff_ex(top_polygons, offset(bottom_polygons, slope_offset)); - top.dangling_areas = diff_ex(top_polygons, offset(bottom_polygons, between_layers_offset)); - } - } - } - } - }); + { return p1.second > p2.second; }); + ExPolygons overhangs_sorted; + for (auto &p : expolys_with_areas) + overhangs_sorted.emplace_back(std::move(*p.first)); + top.overhangs = std::move(overhangs_sorted); + top.overhangs_area *= float(SCALING_FACTOR * SCALING_FACTOR); + } + } + } + }, 8 /* gransize */); return layers; } @@ -252,21 +273,9 @@ void SupportPointGenerator::process(const std::vector& slices, const // Now iterate over all polygons and append new points if needed. for (Structure &s : layer_top->islands) { // Penalization resulting from large diff from the last layer: -// s.supports_force_inherited /= std::max(1.f, (layer_height / 0.3f) * e_area / s.area); s.supports_force_inherited /= std::max(1.f, 0.17f * (s.overhangs_area) / s.area); - //float force_deficit = s.support_force_deficit(m_config.tear_pressure()); - if (s.islands_below.empty()) { // completely new island - needs support no doubt - uniformly_cover({ *s.polygon }, s, point_grid, true); - } else if (! s.dangling_areas.empty()) { - // Let's see if there's anything that overlaps enough to need supports: - // What we now have in polygons needs support, regardless of what the forces are, so we can add them. - //FIXME is it an island point or not? Vojtech thinks it is. - uniformly_cover(s.dangling_areas, s, point_grid); - } else if (! s.overhangs_slopes.empty()) { - //FIXME add the support force deficit as a parameter, only cover until the defficiency is covered. - uniformly_cover(s.overhangs_slopes, s, point_grid); - } + add_support_points(s, point_grid); } m_throw_on_cancel(); @@ -284,6 +293,42 @@ void SupportPointGenerator::process(const std::vector& slices, const } } +void SupportPointGenerator::add_support_points(SupportPointGenerator::Structure &s, SupportPointGenerator::PointGrid3D &grid3d) +{ + // Select each type of surface (overrhang, dangling, slope), derive the support + // force deficit for it and call uniformly conver with the right params + + float tp = m_config.tear_pressure(); + float current = s.supports_force_total(); + static constexpr float SLOPE_DAMPING = .0015f; + static constexpr float DANGL_DAMPING = .09f; + + if (s.islands_below.empty()) { + // completely new island - needs support no doubt + // deficit is full, there is nothing below that would hold this island + uniformly_cover({ *s.polygon }, s, s.area * tp, grid3d, IslandCoverageFlags(icfIsNew | icfBoundaryOnly) ); + return; + } + + auto areafn = [](double sum, auto &p) { return sum + p.area() * SCALING_FACTOR * SCALING_FACTOR; }; + if (! s.dangling_areas.empty()) { + // Let's see if there's anything that overlaps enough to need supports: + // What we now have in polygons needs support, regardless of what the forces are, so we can add them. + + double a = std::accumulate(s.dangling_areas.begin(), s.dangling_areas.end(), 0., areafn); + uniformly_cover(s.dangling_areas, s, a * tp - current * DANGL_DAMPING * std::sqrt(1. - a / s.area), grid3d); + } + + if (! s.overhangs_slopes.empty()) { + double a = std::accumulate(s.overhangs_slopes.begin(), s.overhangs_slopes.end(), 0., areafn); + uniformly_cover(s.overhangs_slopes, s, a * tp - current * SLOPE_DAMPING * std::sqrt(1. - a / s.area), grid3d); + } + + if (! s.overhangs.empty()) { + uniformly_cover(s.overhangs, s, s.overhangs_area * tp, grid3d); + } +} + std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_mm2, std::mt19937 &rng) { // Triangulate the polygon with holes into triplets of 3D points. @@ -293,16 +338,16 @@ std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_m if (! triangles.empty()) { // Calculate area of each triangle. - std::vector areas; - areas.reserve(triangles.size() / 3); + auto areas = reserve_vector(triangles.size() / 3); + double aback = 0.; for (size_t i = 0; i < triangles.size(); ) { const Vec2f &a = triangles[i ++]; const Vec2f v1 = triangles[i ++] - a; const Vec2f v2 = triangles[i ++] - a; - areas.emplace_back(0.5f * std::abs(cross2(v1, v2))); - if (i != 3) - // Prefix sum of the areas. - areas.back() += areas[areas.size() - 2]; + + // Prefix sum of the areas. + areas.emplace_back(aback + 0.5f * std::abs(cross2(v1, v2))); + aback = areas.back(); } size_t num_samples = size_t(ceil(areas.back() * samples_per_mm2)); @@ -312,7 +357,7 @@ std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_m double r = random_triangle(rng); size_t idx_triangle = std::min(std::upper_bound(areas.begin(), areas.end(), (float)r) - areas.begin(), areas.size() - 1) * 3; // Select a random point on the triangle. - double u = float(sqrt(random_float(rng))); + double u = float(std::sqrt(random_float(rng))); double v = float(random_float(rng)); const Vec2f &a = triangles[idx_triangle ++]; const Vec2f &b = triangles[idx_triangle++]; @@ -324,16 +369,37 @@ std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_m return out; } + +std::vector sample_expolygon(const ExPolygons &expolys, float samples_per_mm2, std::mt19937 &rng) +{ + std::vector out; + for (const ExPolygon &expoly : expolys) + append(out, sample_expolygon(expoly, samples_per_mm2, rng)); + + return out; +} + +void sample_expolygon_boundary(const ExPolygon & expoly, + float samples_per_mm, + std::vector &out, + std::mt19937 & rng) +{ + double point_stepping_scaled = scale_(1.f) / samples_per_mm; + for (size_t i_contour = 0; i_contour <= expoly.holes.size(); ++ i_contour) { + const Polygon &contour = (i_contour == 0) ? expoly.contour : + expoly.holes[i_contour - 1]; + + const Points pts = contour.equally_spaced_points(point_stepping_scaled); + for (size_t i = 0; i < pts.size(); ++ i) + out.emplace_back(unscale(pts[i].x()), + unscale(pts[i].y())); + } +} + std::vector sample_expolygon_with_boundary(const ExPolygon &expoly, float samples_per_mm2, float samples_per_mm_boundary, std::mt19937 &rng) { std::vector out = sample_expolygon(expoly, samples_per_mm2, rng); - double point_stepping_scaled = scale_(1.f) / samples_per_mm_boundary; - for (size_t i_contour = 0; i_contour <= expoly.holes.size(); ++ i_contour) { - const Polygon &contour = (i_contour == 0) ? expoly.contour : expoly.holes[i_contour - 1]; - const Points pts = contour.equally_spaced_points(point_stepping_scaled); - for (size_t i = 0; i < pts.size(); ++ i) - out.emplace_back(unscale(pts[i].x()), unscale(pts[i].y())); - } + sample_expolygon_boundary(expoly, samples_per_mm_boundary, out, rng); return out; } @@ -355,17 +421,17 @@ static inline std::vector poisson_disk_from_samples(const std::vector raw_samples_sorted; - RawSample sample; - for (const Vec2f &pt : raw_samples) { - sample.coord = pt; - sample.cell_id = ((pt - corner_min) / radius).cast(); - raw_samples_sorted.emplace_back(sample); - } + + auto raw_samples_sorted = reserve_vector(raw_samples.size()); + for (const Vec2f &pt : raw_samples) + raw_samples_sorted.emplace_back(pt, ((pt - corner_min) / radius).cast()); + std::sort(raw_samples_sorted.begin(), raw_samples_sorted.end(), [](const RawSample &lhs, const RawSample &rhs) { return lhs.cell_id.x() < rhs.cell_id.x() || (lhs.cell_id.x() == rhs.cell_id.x() && lhs.cell_id.y() < rhs.cell_id.y()); }); @@ -460,11 +526,22 @@ static inline std::vector poisson_disk_from_samples(const std::vector bbdim.y()) std::swap(bbdim.x(), bbdim.y()); + double aspectr = bbdim.y() / bbdim.x(); + + support_force_deficit *= (1 + aspectr / 2.); + } + if (support_force_deficit < 0) return; @@ -481,13 +558,18 @@ void SupportPointGenerator::uniformly_cover(const ExPolygons& islands, Structure float min_spacing = poisson_radius; //FIXME share the random generator. The random generator may be not so cheap to initialize, also we don't want the random generator to be restarted for each polygon. - - std::vector raw_samples = sample_expolygon_with_boundary(islands, samples_per_mm2, 5.f / poisson_radius, m_rng); + + std::vector raw_samples = + flags & icfBoundaryOnly ? + sample_expolygon_with_boundary(islands, samples_per_mm2, + 5.f / poisson_radius, m_rng) : + sample_expolygon(islands, samples_per_mm2, m_rng); + std::vector poisson_samples; for (size_t iter = 0; iter < 4; ++ iter) { poisson_samples = poisson_disk_from_samples(raw_samples, poisson_radius, [&structure, &grid3d, min_spacing](const Vec2f &pos) { - return grid3d.collides_with(pos, &structure, min_spacing); + return grid3d.collides_with(pos, structure.layer->print_z, min_spacing); }); if (poisson_samples.size() >= poisson_samples_target || m_config.minimal_distance > poisson_radius-EPSILON) break; @@ -517,21 +599,19 @@ void SupportPointGenerator::uniformly_cover(const ExPolygons& islands, Structure poisson_samples.erase(poisson_samples.begin() + poisson_samples_target, poisson_samples.end()); } for (const Vec2f &pt : poisson_samples) { - m_output.emplace_back(float(pt(0)), float(pt(1)), structure.height, m_config.head_diameter/2.f, is_new_island); + m_output.emplace_back(float(pt(0)), float(pt(1)), structure.zlevel, m_config.head_diameter/2.f, flags & icfIsNew); structure.supports_force_this_layer += m_config.support_force(); grid3d.insert(pt, &structure); } } -void remove_bottom_points(std::vector &pts, double gnd_lvl, double tolerance) + +void remove_bottom_points(std::vector &pts, float lvl) { // get iterator to the reorganized vector end - auto endit = - std::remove_if(pts.begin(), pts.end(), - [tolerance, gnd_lvl](const sla::SupportPoint &sp) { - double diff = std::abs(gnd_lvl - - double(sp.pos(Z))); - return diff <= tolerance; + auto endit = std::remove_if(pts.begin(), pts.end(), [lvl] + (const sla::SupportPoint &sp) { + return sp.pos.z() <= lvl; }); // erase all elements after the new end diff --git a/src/libslic3r/SLA/SupportPointGenerator.hpp b/src/libslic3r/SLA/SupportPointGenerator.hpp index 2fe8e11fc7..30c221c041 100644 --- a/src/libslic3r/SLA/SupportPointGenerator.hpp +++ b/src/libslic3r/SLA/SupportPointGenerator.hpp @@ -3,9 +3,8 @@ #include -#include #include -#include +#include #include #include @@ -23,15 +22,16 @@ public: float density_relative {1.f}; float minimal_distance {1.f}; float head_diameter {0.4f}; - /////////////// - inline float support_force() const { return 7.7f / density_relative; } // a force one point can support (arbitrary force unit) + + // Originally calibrated to 7.7f, reduced density by Tamas to 70% which is 11.1 (7.7 / 0.7) to adjust for new algorithm changes in tm_suppt_gen_improve + inline float support_force() const { return 11.1f / density_relative; } // a force one point can support (arbitrary force unit) inline float tear_pressure() const { return 1.f; } // pressure that the display exerts (the force unit per mm2) }; - SupportPointGenerator(const EigenMesh3D& emesh, const std::vector& slices, + SupportPointGenerator(const IndexedMesh& emesh, const std::vector& slices, const std::vector& heights, const Config& config, std::function throw_on_cancel, std::function statusfn); - SupportPointGenerator(const EigenMesh3D& emesh, const Config& config, std::function throw_on_cancel, std::function statusfn); + SupportPointGenerator(const IndexedMesh& emesh, const Config& config, std::function throw_on_cancel, std::function statusfn); const std::vector& output() const { return m_output; } std::vector& output() { return m_output; } @@ -39,8 +39,8 @@ public: struct MyLayer; struct Structure { - Structure(MyLayer &layer, const ExPolygon& poly, const BoundingBox &bbox, const Vec2f ¢roid, float area, float h) : - layer(&layer), polygon(&poly), bbox(bbox), centroid(centroid), area(area), height(h) + Structure(MyLayer &layer, const ExPolygon& poly, const BoundingBox &bbox, const Vec2f ¢roid, float area, float h) : + layer(&layer), polygon(&poly), bbox(bbox), centroid(centroid), area(area), zlevel(h) #ifdef SLA_SUPPORTPOINTGEN_DEBUG , unique_id(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch())) #endif /* SLA_SUPPORTPOINTGEN_DEBUG */ @@ -50,7 +50,7 @@ public: const BoundingBox bbox; const Vec2f centroid = Vec2f::Zero(); const float area = 0.f; - float height = 0; + float zlevel = 0; // How well is this ExPolygon held to the print base? // Positive number, the higher the better. float supports_force_this_layer = 0.f; @@ -160,8 +160,8 @@ public: grid.emplace(cell_id(pt.position), pt); } - bool collides_with(const Vec2f &pos, Structure *island, float radius) { - Vec3f pos3d(pos.x(), pos.y(), float(island->layer->print_z)); + bool collides_with(const Vec2f &pos, float print_z, float radius) { + Vec3f pos3d(pos.x(), pos.y(), print_z); Vec3i cell = cell_id(pos3d); std::pair it_pair = grid.equal_range(cell); if (collides_with(pos3d, radius, it_pair.first, it_pair.second)) @@ -199,7 +199,16 @@ private: SupportPointGenerator::Config m_config; void process(const std::vector& slices, const std::vector& heights); - void uniformly_cover(const ExPolygons& islands, Structure& structure, PointGrid3D &grid3d, bool is_new_island = false, bool just_one = false); + +public: + enum IslandCoverageFlags : uint8_t { icfNone = 0x0, icfIsNew = 0x1, icfBoundaryOnly = 0x2 }; + +private: + + void uniformly_cover(const ExPolygons& islands, Structure& structure, float deficit, PointGrid3D &grid3d, IslandCoverageFlags flags = icfNone); + + void add_support_points(Structure& structure, PointGrid3D &grid3d); + void project_onto_mesh(std::vector& points) const; #ifdef SLA_SUPPORTPOINTGEN_DEBUG @@ -207,14 +216,17 @@ private: static void output_structures(const std::vector &structures); #endif // SLA_SUPPORTPOINTGEN_DEBUG - const EigenMesh3D& m_emesh; + const IndexedMesh& m_emesh; std::function m_throw_on_cancel; std::function m_statusfn; std::mt19937 m_rng; }; -void remove_bottom_points(std::vector &pts, double gnd_lvl, double tolerance); +void remove_bottom_points(std::vector &pts, float lvl); + +std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_mm2, std::mt19937 &rng); +void sample_expolygon_boundary(const ExPolygon &expoly, float samples_per_mm, std::vector &out, std::mt19937 &rng); }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportTree.cpp b/src/libslic3r/SLA/SupportTree.cpp index 528778b68b..1bb4cfab76 100644 --- a/src/libslic3r/SLA/SupportTree.cpp +++ b/src/libslic3r/SLA/SupportTree.cpp @@ -5,9 +5,9 @@ #include #include -#include #include #include +#include #include #include @@ -28,20 +28,6 @@ namespace Slic3r { namespace sla { -// Compile time configuration value definitions: - -// The max Z angle for a normal at which it will get completely ignored. -const double SupportConfig::normal_cutoff_angle = 150.0 * M_PI / 180.0; - -// The shortest distance of any support structure from the model surface -const double SupportConfig::safety_distance_mm = 0.5; - -const double SupportConfig::max_solo_pillar_height_mm = 15.0; -const double SupportConfig::max_dual_pillar_height_mm = 35.0; -const double SupportConfig::optimizer_rel_score_diff = 1e-6; -const unsigned SupportConfig::optimizer_max_iterations = 1000; -const unsigned SupportConfig::pillar_cascade_neighbors = 3; - void SupportTree::retrieve_full_mesh(TriangleMesh &outmesh) const { outmesh.merge(retrieve_mesh(MeshType::Support)); outmesh.merge(retrieve_mesh(MeshType::Pad)); @@ -103,9 +89,11 @@ SupportTree::UPtr SupportTree::create(const SupportableMesh &sm, builder->m_ctl = ctl; if (sm.cfg.enabled) { - builder->build(sm); + // Execute takes care about the ground_level + SupportTreeBuildsteps::execute(*builder, sm); builder->merge_and_cleanup(); // clean metadata, leave only the meshes. } else { + // If a pad gets added later, it will be in the right Z level builder->ground_level = sm.emesh.ground_level(); } diff --git a/src/libslic3r/SLA/SupportTree.hpp b/src/libslic3r/SLA/SupportTree.hpp index c6255aa2f2..4be90161d5 100644 --- a/src/libslic3r/SLA/SupportTree.hpp +++ b/src/libslic3r/SLA/SupportTree.hpp @@ -5,9 +5,8 @@ #include #include -#include #include -#include +#include #include #include @@ -32,7 +31,7 @@ enum class PillarConnectionMode dynamic }; -struct SupportConfig +struct SupportTreeConfig { bool enabled = true; @@ -45,6 +44,8 @@ struct SupportConfig // Radius of the back side of the 3d arrow. double head_back_radius_mm = 0.5; + double head_fallback_radius_mm = 0.25; + // Width in mm from the back sphere center to the front sphere center. double head_width_mm = 1.0; @@ -95,36 +96,43 @@ struct SupportConfig // ///////////////////////////////////////////////////////////////////////// // The max Z angle for a normal at which it will get completely ignored. - static const double normal_cutoff_angle; + static const double constexpr normal_cutoff_angle = 150.0 * M_PI / 180.0; // The shortest distance of any support structure from the model surface - static const double safety_distance_mm; + static const double constexpr safety_distance_mm = 0.5; - static const double max_solo_pillar_height_mm; - static const double max_dual_pillar_height_mm; - static const double optimizer_rel_score_diff; - static const unsigned optimizer_max_iterations; - static const unsigned pillar_cascade_neighbors; + static const double constexpr max_solo_pillar_height_mm = 15.0; + static const double constexpr max_dual_pillar_height_mm = 35.0; + static const double constexpr optimizer_rel_score_diff = 1e-6; + static const unsigned constexpr optimizer_max_iterations = 1000; + static const unsigned constexpr pillar_cascade_neighbors = 3; }; +// TODO: Part of future refactor +//class SupportConfig { +// std::optional tree_cfg {std::in_place_t{}}; // fill up +// std::optional pad_cfg; +//}; + enum class MeshType { Support, Pad }; struct SupportableMesh { - EigenMesh3D emesh; + IndexedMesh emesh; SupportPoints pts; - SupportConfig cfg; + SupportTreeConfig cfg; + PadConfig pad_cfg; explicit SupportableMesh(const TriangleMesh & trmsh, const SupportPoints &sp, - const SupportConfig &c) + const SupportTreeConfig &c) : emesh{trmsh}, pts{sp}, cfg{c} {} - explicit SupportableMesh(const EigenMesh3D &em, + explicit SupportableMesh(const IndexedMesh &em, const SupportPoints &sp, - const SupportConfig &c) + const SupportTreeConfig &c) : emesh{em}, pts{sp}, cfg{c} {} }; diff --git a/src/libslic3r/SLA/SupportTreeBuilder.cpp b/src/libslic3r/SLA/SupportTreeBuilder.cpp index cf6e7e0206..daa01ef24d 100644 --- a/src/libslic3r/SLA/SupportTreeBuilder.cpp +++ b/src/libslic3r/SLA/SupportTreeBuilder.cpp @@ -1,336 +1,26 @@ +#define NOMINMAX + #include #include +#include #include namespace Slic3r { namespace sla { -Contour3D sphere(double rho, Portion portion, double fa) { - - Contour3D ret; - - // prohibit close to zero radius - if(rho <= 1e-6 && rho >= -1e-6) return ret; - - auto& vertices = ret.points; - auto& facets = ret.faces3; - - // Algorithm: - // Add points one-by-one to the sphere grid and form facets using relative - // coordinates. Sphere is composed effectively of a mesh of stacked circles. - - // adjust via rounding to get an even multiple for any provided angle. - double angle = (2*PI / floor(2*PI / fa)); - - // Ring to be scaled to generate the steps of the sphere - std::vector ring; - - for (double i = 0; i < 2*PI; i+=angle) ring.emplace_back(i); - - const auto sbegin = size_t(2*std::get<0>(portion)/angle); - const auto send = size_t(2*std::get<1>(portion)/angle); - - const size_t steps = ring.size(); - const double increment = 1.0 / double(steps); - - // special case: first ring connects to 0,0,0 - // insert and form facets. - if(sbegin == 0) - vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*sbegin*2.0*rho)); - - auto id = coord_t(vertices.size()); - for (size_t i = 0; i < ring.size(); i++) { - // Fixed scaling - const double z = -rho + increment*rho*2.0 * (sbegin + 1.0); - // radius of the circle for this step. - const double r = std::sqrt(std::abs(rho*rho - z*z)); - Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r); - vertices.emplace_back(Vec3d(b(0), b(1), z)); - - if (sbegin == 0) - (i == 0) ? facets.emplace_back(coord_t(ring.size()), 0, 1) : - facets.emplace_back(id - 1, 0, id); - ++id; - } - - // General case: insert and form facets for each step, - // joining it to the ring below it. - for (size_t s = sbegin + 2; s < send - 1; s++) { - const double z = -rho + increment*double(s*2.0*rho); - const double r = std::sqrt(std::abs(rho*rho - z*z)); - - for (size_t i = 0; i < ring.size(); i++) { - Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r); - vertices.emplace_back(Vec3d(b(0), b(1), z)); - auto id_ringsize = coord_t(id - int(ring.size())); - if (i == 0) { - // wrap around - facets.emplace_back(id - 1, id, id + coord_t(ring.size() - 1) ); - facets.emplace_back(id - 1, id_ringsize, id); - } else { - facets.emplace_back(id_ringsize - 1, id_ringsize, id); - facets.emplace_back(id - 1, id_ringsize - 1, id); - } - id++; - } - } - - // special case: last ring connects to 0,0,rho*2.0 - // only form facets. - if(send >= size_t(2*PI / angle)) { - vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*send*2.0*rho)); - for (size_t i = 0; i < ring.size(); i++) { - auto id_ringsize = coord_t(id - int(ring.size())); - if (i == 0) { - // third vertex is on the other side of the ring. - facets.emplace_back(id - 1, id_ringsize, id); - } else { - auto ci = coord_t(id_ringsize + coord_t(i)); - facets.emplace_back(ci - 1, ci, id); - } - } - } - id++; - - return ret; -} - -Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp) -{ - Contour3D ret; - - auto steps = int(ssteps); - auto& points = ret.points; - auto& indices = ret.faces3; - points.reserve(2*ssteps); - double a = 2*PI/steps; - - Vec3d jp = sp; - Vec3d endp = {sp(X), sp(Y), sp(Z) + h}; - - // Upper circle points - for(int i = 0; i < steps; ++i) { - double phi = i*a; - double ex = endp(X) + r*std::cos(phi); - double ey = endp(Y) + r*std::sin(phi); - points.emplace_back(ex, ey, endp(Z)); - } - - // Lower circle points - for(int i = 0; i < steps; ++i) { - double phi = i*a; - double x = jp(X) + r*std::cos(phi); - double y = jp(Y) + r*std::sin(phi); - points.emplace_back(x, y, jp(Z)); - } - - // Now create long triangles connecting upper and lower circles - indices.reserve(2*ssteps); - auto offs = steps; - for(int i = 0; i < steps - 1; ++i) { - indices.emplace_back(i, i + offs, offs + i + 1); - indices.emplace_back(i, offs + i + 1, i + 1); - } - - // Last triangle connecting the first and last vertices - auto last = steps - 1; - indices.emplace_back(0, last, offs); - indices.emplace_back(last, offs + last, offs); - - // According to the slicing algorithms, we need to aid them with generating - // a watertight body. So we create a triangle fan for the upper and lower - // ending of the cylinder to close the geometry. - points.emplace_back(jp); int ci = int(points.size() - 1); - for(int i = 0; i < steps - 1; ++i) - indices.emplace_back(i + offs + 1, i + offs, ci); - - indices.emplace_back(offs, steps + offs - 1, ci); - - points.emplace_back(endp); ci = int(points.size() - 1); - for(int i = 0; i < steps - 1; ++i) - indices.emplace_back(ci, i, i + 1); - - indices.emplace_back(steps - 1, 0, ci); - - return ret; -} - Head::Head(double r_big_mm, double r_small_mm, double length_mm, double penetration, const Vec3d &direction, - const Vec3d &offset, - const size_t circlesteps) - : steps(circlesteps) - , dir(direction) - , tr(offset) + const Vec3d &offset) + : dir(direction) + , pos(offset) , r_back_mm(r_big_mm) , r_pin_mm(r_small_mm) , width_mm(length_mm) , penetration_mm(penetration) { - assert(width_mm > 0.); - assert(r_back_mm > 0.); - assert(r_pin_mm > 0.); - - // We create two spheres which will be connected with a robe that fits - // both circles perfectly. - - // Set up the model detail level - const double detail = 2*PI/steps; - - // We don't generate whole circles. Instead, we generate only the - // portions which are visible (not covered by the robe) To know the - // exact portion of the bottom and top circles we need to use some - // rules of tangent circles from which we can derive (using simple - // triangles the following relations: - - // The height of the whole mesh - const double h = r_big_mm + r_small_mm + width_mm; - double phi = PI/2 - std::acos( (r_big_mm - r_small_mm) / h ); - - // To generate a whole circle we would pass a portion of (0, Pi) - // To generate only a half horizontal circle we can pass (0, Pi/2) - // The calculated phi is an offset to the half circles needed to smooth - // the transition from the circle to the robe geometry - - auto&& s1 = sphere(r_big_mm, make_portion(0, PI/2 + phi), detail); - auto&& s2 = sphere(r_small_mm, make_portion(PI/2 + phi, PI), detail); - - for(auto& p : s2.points) p.z() += h; - - mesh.merge(s1); - mesh.merge(s2); - - for(size_t idx1 = s1.points.size() - steps, idx2 = s1.points.size(); - idx1 < s1.points.size() - 1; - idx1++, idx2++) - { - coord_t i1s1 = coord_t(idx1), i1s2 = coord_t(idx2); - coord_t i2s1 = i1s1 + 1, i2s2 = i1s2 + 1; - - mesh.faces3.emplace_back(i1s1, i2s1, i2s2); - mesh.faces3.emplace_back(i1s1, i2s2, i1s2); - } - - auto i1s1 = coord_t(s1.points.size()) - coord_t(steps); - auto i2s1 = coord_t(s1.points.size()) - 1; - auto i1s2 = coord_t(s1.points.size()); - auto i2s2 = coord_t(s1.points.size()) + coord_t(steps) - 1; - - mesh.faces3.emplace_back(i2s2, i2s1, i1s1); - mesh.faces3.emplace_back(i1s2, i2s2, i1s1); - - // To simplify further processing, we translate the mesh so that the - // last vertex of the pointing sphere (the pinpoint) will be at (0,0,0) - for(auto& p : mesh.points) p.z() -= (h + r_small_mm - penetration_mm); -} - -Pillar::Pillar(const Vec3d &jp, const Vec3d &endp, double radius, size_t st): - r(radius), steps(st), endpt(endp), starts_from_head(false) -{ - assert(steps > 0); - - height = jp(Z) - endp(Z); - if(height > EPSILON) { // Endpoint is below the starting point - - // We just create a bridge geometry with the pillar parameters and - // move the data. - Contour3D body = cylinder(radius, height, st, endp); - mesh.points.swap(body.points); - mesh.faces3.swap(body.faces3); - } -} - -Pillar &Pillar::add_base(double baseheight, double radius) -{ - if(baseheight <= 0) return *this; - if(baseheight > height) baseheight = height; - - assert(steps >= 0); - auto last = int(steps - 1); - - if(radius < r ) radius = r; - - double a = 2*PI/steps; - double z = endpt(Z) + baseheight; - - for(size_t i = 0; i < steps; ++i) { - double phi = i*a; - double x = endpt(X) + r*std::cos(phi); - double y = endpt(Y) + r*std::sin(phi); - base.points.emplace_back(x, y, z); - } - - for(size_t i = 0; i < steps; ++i) { - double phi = i*a; - double x = endpt(X) + radius*std::cos(phi); - double y = endpt(Y) + radius*std::sin(phi); - base.points.emplace_back(x, y, z - baseheight); - } - - auto ep = endpt; ep(Z) += baseheight; - base.points.emplace_back(endpt); - base.points.emplace_back(ep); - - auto& indices = base.faces3; - auto hcenter = int(base.points.size() - 1); - auto lcenter = int(base.points.size() - 2); - auto offs = int(steps); - for(int i = 0; i < last; ++i) { - indices.emplace_back(i, i + offs, offs + i + 1); - indices.emplace_back(i, offs + i + 1, i + 1); - indices.emplace_back(i, i + 1, hcenter); - indices.emplace_back(lcenter, offs + i + 1, offs + i); - } - - indices.emplace_back(0, last, offs); - indices.emplace_back(last, offs + last, offs); - indices.emplace_back(hcenter, last, 0); - indices.emplace_back(offs, offs + last, lcenter); - return *this; -} - -Bridge::Bridge(const Vec3d &j1, const Vec3d &j2, double r_mm, size_t steps): - r(r_mm), startp(j1), endp(j2) -{ - using Quaternion = Eigen::Quaternion; - Vec3d dir = (j2 - j1).normalized(); - double d = distance(j2, j1); - - mesh = cylinder(r, d, steps); - - auto quater = Quaternion::FromTwoVectors(Vec3d{0,0,1}, dir); - for(auto& p : mesh.points) p = quater * p + j1; -} - -CompactBridge::CompactBridge(const Vec3d &sp, - const Vec3d &ep, - const Vec3d &n, - double r, - bool endball, - size_t steps) -{ - Vec3d startp = sp + r * n; - Vec3d dir = (ep - startp).normalized(); - Vec3d endp = ep - r * dir; - - Bridge br(startp, endp, r, steps); - mesh.merge(br.mesh); - - // now add the pins - double fa = 2*PI/steps; - auto upperball = sphere(r, Portion{PI / 2 - fa, PI}, fa); - for(auto& p : upperball.points) p += startp; - - if(endball) { - auto lowerball = sphere(r, Portion{0, PI/2 + 2*fa}, fa); - for(auto& p : lowerball.points) p += endp; - mesh.merge(lowerball); - } - - mesh.merge(upperball); } Pad::Pad(const TriangleMesh &support_mesh, @@ -368,7 +58,6 @@ SupportTreeBuilder::SupportTreeBuilder(SupportTreeBuilder &&o) , m_pillars{std::move(o.m_pillars)} , m_bridges{std::move(o.m_bridges)} , m_crossbridges{std::move(o.m_crossbridges)} - , m_compact_bridges{std::move(o.m_compact_bridges)} , m_pad{std::move(o.m_pad)} , m_meshcache{std::move(o.m_meshcache)} , m_meshcache_valid{o.m_meshcache_valid} @@ -382,7 +71,6 @@ SupportTreeBuilder::SupportTreeBuilder(const SupportTreeBuilder &o) , m_pillars{o.m_pillars} , m_bridges{o.m_bridges} , m_crossbridges{o.m_crossbridges} - , m_compact_bridges{o.m_compact_bridges} , m_pad{o.m_pad} , m_meshcache{o.m_meshcache} , m_meshcache_valid{o.m_meshcache_valid} @@ -397,7 +85,6 @@ SupportTreeBuilder &SupportTreeBuilder::operator=(SupportTreeBuilder &&o) m_pillars = std::move(o.m_pillars); m_bridges = std::move(o.m_bridges); m_crossbridges = std::move(o.m_crossbridges); - m_compact_bridges = std::move(o.m_compact_bridges); m_pad = std::move(o.m_pad); m_meshcache = std::move(o.m_meshcache); m_meshcache_valid = o.m_meshcache_valid; @@ -413,7 +100,6 @@ SupportTreeBuilder &SupportTreeBuilder::operator=(const SupportTreeBuilder &o) m_pillars = o.m_pillars; m_bridges = o.m_bridges; m_crossbridges = o.m_crossbridges; - m_compact_bridges = o.m_compact_bridges; m_pad = o.m_pad; m_meshcache = o.m_meshcache; m_meshcache_valid = o.m_meshcache_valid; @@ -422,7 +108,19 @@ SupportTreeBuilder &SupportTreeBuilder::operator=(const SupportTreeBuilder &o) return *this; } -const TriangleMesh &SupportTreeBuilder::merged_mesh() const +void SupportTreeBuilder::add_pillar_base(long pid, double baseheight, double radius) +{ + std::lock_guard lk(m_mutex); + assert(pid >= 0 && size_t(pid) < m_pillars.size()); + Pillar& pll = m_pillars[size_t(pid)]; + m_pedestals.emplace_back(pll.endpt, std::min(baseheight, pll.height), + std::max(radius, pll.r), pll.r); + + m_pedestals.back().id = m_pedestals.size() - 1; + m_meshcache_valid = false; +} + +const TriangleMesh &SupportTreeBuilder::merged_mesh(size_t steps) const { if (m_meshcache_valid) return m_meshcache; @@ -430,35 +128,44 @@ const TriangleMesh &SupportTreeBuilder::merged_mesh() const for (auto &head : m_heads) { if (ctl().stopcondition()) break; - if (head.is_valid()) merged.merge(head.mesh); + if (head.is_valid()) merged.merge(get_mesh(head, steps)); } - for (auto &stick : m_pillars) { + for (auto &pill : m_pillars) { if (ctl().stopcondition()) break; - merged.merge(stick.mesh); - merged.merge(stick.base); + merged.merge(get_mesh(pill, steps)); + } + + for (auto &pedest : m_pedestals) { + if (ctl().stopcondition()) break; + merged.merge(get_mesh(pedest, steps)); } for (auto &j : m_junctions) { if (ctl().stopcondition()) break; - merged.merge(j.mesh); + merged.merge(get_mesh(j, steps)); } - - for (auto &cb : m_compact_bridges) { - if (ctl().stopcondition()) break; - merged.merge(cb.mesh); - } - + for (auto &bs : m_bridges) { if (ctl().stopcondition()) break; - merged.merge(bs.mesh); + merged.merge(get_mesh(bs, steps)); } for (auto &bs : m_crossbridges) { if (ctl().stopcondition()) break; - merged.merge(bs.mesh); + merged.merge(get_mesh(bs, steps)); } - + + for (auto &bs : m_diffbridges) { + if (ctl().stopcondition()) break; + merged.merge(get_mesh(bs, steps)); + } + + for (auto &anch : m_anchors) { + if (ctl().stopcondition()) break; + merged.merge(get_mesh(anch, steps)); + } + if (ctl().stopcondition()) { // In case of failure we have to return an empty mesh m_meshcache = TriangleMesh(); @@ -499,7 +206,6 @@ const TriangleMesh &SupportTreeBuilder::merge_and_cleanup() m_pillars = {}; m_junctions = {}; m_bridges = {}; - m_compact_bridges = {}; return ret; } @@ -514,11 +220,4 @@ const TriangleMesh &SupportTreeBuilder::retrieve_mesh(MeshType meshtype) const return m_meshcache; } -bool SupportTreeBuilder::build(const SupportableMesh &sm) -{ - ground_level = sm.emesh.ground_level() - sm.cfg.object_elevation_mm; - return SupportTreeBuildsteps::execute(*this, sm); -} - -} -} +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportTreeBuilder.hpp b/src/libslic3r/SLA/SupportTreeBuilder.hpp index 90cf417c83..f29263ca3f 100644 --- a/src/libslic3r/SLA/SupportTreeBuilder.hpp +++ b/src/libslic3r/SLA/SupportTreeBuilder.hpp @@ -2,7 +2,6 @@ #define SLA_SUPPORTTREEBUILDER_HPP #include -#include #include #include #include @@ -50,13 +49,6 @@ namespace sla { * nearby pillar. */ -using Coordf = double; -using Portion = std::tuple; - -inline Portion make_portion(double a, double b) { - return std::make_tuple(a, b); -} - template double distance(const Vec& p) { return std::sqrt(p.transpose() * p); } @@ -66,33 +58,25 @@ template double distance(const Vec& pp1, const Vec& pp2) { return distance(p); } -Contour3D sphere(double rho, Portion portion = make_portion(0.0, 2.0*PI), - double fa=(2*PI/360)); +const Vec3d DOWN = {0.0, 0.0, -1.0}; -// Down facing cylinder in Z direction with arguments: -// r: radius -// h: Height -// ssteps: how many edges will create the base circle -// sp: starting point -Contour3D cylinder(double r, double h, size_t ssteps = 45, const Vec3d &sp = {0,0,0}); +struct SupportTreeNode +{ + static const constexpr long ID_UNSET = -1; -const constexpr long ID_UNSET = -1; + long id = ID_UNSET; // For identification withing a tree. +}; -struct Head { - Contour3D mesh; - - size_t steps = 45; - Vec3d dir = {0, 0, -1}; - Vec3d tr = {0, 0, 0}; +// A pinhead originating from a support point +struct Head: public SupportTreeNode { + Vec3d dir = DOWN; + Vec3d pos = {0, 0, 0}; double r_back_mm = 1; double r_pin_mm = 0.5; double width_mm = 2; double penetration_mm = 0.5; - - // For identification purposes. This will be used as the index into the - // container holding the head structures. See SLASupportTree::Impl - long id = ID_UNSET; + // If there is a pillar connecting to this head, then the id will be set. long pillar_id = ID_UNSET; @@ -106,31 +90,23 @@ struct Head { double r_small_mm, double length_mm, double penetration, - const Vec3d &direction = {0, 0, -1}, // direction (normal to the dull end) - const Vec3d &offset = {0, 0, 0}, // displacement - const size_t circlesteps = 45); - - void transform() + const Vec3d &direction = DOWN, // direction (normal to the dull end) + const Vec3d &offset = {0, 0, 0} // displacement + ); + + inline double real_width() const { - using Quaternion = Eigen::Quaternion; - - // We rotate the head to the specified direction The head's pointing - // side is facing upwards so this means that it would hold a support - // point with a normal pointing straight down. This is the reason of - // the -1 z coordinate - auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, -1}, dir); - - for(auto& p : mesh.points) p = quatern * p + tr; + return 2 * r_pin_mm + width_mm + 2 * r_back_mm ; } - + inline double fullwidth() const { - return 2 * r_pin_mm + width_mm + 2*r_back_mm - penetration_mm; + return real_width() - penetration_mm; } inline Vec3d junction_point() const { - return tr + ( 2 * r_pin_mm + width_mm + r_back_mm - penetration_mm)*dir; + return pos + (fullwidth() - r_back_mm) * dir; } inline double request_pillar_radius(double radius) const @@ -140,31 +116,17 @@ struct Head { } }; -struct Junction { - Contour3D mesh; +// A junction connecting bridges and pillars +struct Junction: public SupportTreeNode { double r = 1; - size_t steps = 45; Vec3d pos; - - long id = ID_UNSET; - - Junction(const Vec3d& tr, double r_mm, size_t stepnum = 45): - r(r_mm), steps(stepnum), pos(tr) - { - mesh = sphere(r_mm, make_portion(0, PI), 2*PI/steps); - for(auto& p : mesh.points) p += tr; - } + + Junction(const Vec3d &tr, double r_mm) : r(r_mm), pos(tr) {} }; -struct Pillar { - Contour3D mesh; - Contour3D base; - double r = 1; - size_t steps = 0; +struct Pillar: public SupportTreeNode { + double height, r; Vec3d endpt; - double height = 0; - - long id = ID_UNSET; // If the pillar connects to a head, this is the id of that head bool starts_from_head = true; // Could start from a junction as well @@ -175,54 +137,52 @@ struct Pillar { // How many pillars are cascaded with this one unsigned links = 0; - - Pillar(const Vec3d& jp, const Vec3d& endp, - double radius = 1, size_t st = 45); - - Pillar(const Junction &junc, const Vec3d &endp) - : Pillar(junc.pos, endp, junc.r, junc.steps) - {} - - Pillar(const Head &head, const Vec3d &endp, double radius = 1) - : Pillar(head.junction_point(), endp, - head.request_pillar_radius(radius), head.steps) - {} - - inline Vec3d startpoint() const + + Pillar(const Vec3d &endp, double h, double radius = 1.): + height{h}, r(radius), endpt(endp), starts_from_head(false) {} + + Vec3d startpoint() const { - return {endpt(X), endpt(Y), endpt(Z) + height}; + return {endpt.x(), endpt.y(), endpt.z() + height}; } - inline const Vec3d& endpoint() const { return endpt; } - - Pillar& add_base(double baseheight = 3, double radius = 2); + const Vec3d& endpoint() const { return endpt; } }; +// A base for pillars or bridges that end on the ground +struct Pedestal: public SupportTreeNode { + Vec3d pos; + double height, r_bottom, r_top; + + Pedestal(const Vec3d &p, double h, double rbottom, double rtop) + : pos{p}, height{h}, r_bottom{rbottom}, r_top{rtop} + {} +}; + +// This is the thing that anchors a pillar or bridge to the model body. +// It is actually a reverse pinhead. +struct Anchor: public Head { using Head::Head; }; + // A Bridge between two pillars (with junction endpoints) -struct Bridge { - Contour3D mesh; +struct Bridge: public SupportTreeNode { double r = 0.8; - long id = ID_UNSET; Vec3d startp = Vec3d::Zero(), endp = Vec3d::Zero(); Bridge(const Vec3d &j1, const Vec3d &j2, - double r_mm = 0.8, - size_t steps = 45); + double r_mm = 0.8): r{r_mm}, startp{j1}, endp{j2} + {} + + double get_length() const { return (endp - startp).norm(); } + Vec3d get_dir() const { return (endp - startp).normalized(); } }; -// A bridge that spans from model surface to model surface with small connecting -// edges on the endpoints. Used for headless support points. -struct CompactBridge { - Contour3D mesh; - long id = ID_UNSET; - - CompactBridge(const Vec3d& sp, - const Vec3d& ep, - const Vec3d& n, - double r, - bool endball = true, - size_t steps = 45); +struct DiffBridge: public Bridge { + double end_r; + + DiffBridge(const Vec3d &p_s, const Vec3d &p_e, double r_s, double r_e) + : Bridge{p_s, p_e, r_s}, end_r{r_e} + {} }; // A wrapper struct around the pad @@ -258,13 +218,16 @@ struct Pad { // merged mesh. It can be retrieved using a dedicated method (pad()) class SupportTreeBuilder: public SupportTree { // For heads it is beneficial to use the same IDs as for the support points. - std::vector m_heads; - std::vector m_head_indices; - std::vector m_pillars; - std::vector m_junctions; - std::vector m_bridges; - std::vector m_crossbridges; - std::vector m_compact_bridges; + std::vector m_heads; + std::vector m_head_indices; + std::vector m_pillars; + std::vector m_junctions; + std::vector m_bridges; + std::vector m_crossbridges; + std::vector m_diffbridges; + std::vector m_pedestals; + std::vector m_anchors; + Pad m_pad; using Mutex = ccr::SpinningMutex; @@ -274,8 +237,8 @@ class SupportTreeBuilder: public SupportTree { mutable bool m_meshcache_valid = false; mutable double m_model_height = 0; // the full height of the model - template - const Bridge& _add_bridge(std::vector &br, Args&&... args) + template + const BridgeT& _add_bridge(std::vector &br, Args&&... args) { std::lock_guard lk(m_mutex); br.emplace_back(std::forward(args)...); @@ -306,7 +269,7 @@ public: return m_heads.back(); } - template long add_pillar(long headid, Args&&... args) + template long add_pillar(long headid, double length) { std::lock_guard lk(m_mutex); if (m_pillars.capacity() < m_heads.size()) @@ -315,7 +278,9 @@ public: assert(headid >= 0 && size_t(headid) < m_head_indices.size()); Head &head = m_heads[m_head_indices[size_t(headid)]]; - m_pillars.emplace_back(head, std::forward(args)...); + Vec3d hjp = head.junction_point() - Vec3d{0, 0, length}; + m_pillars.emplace_back(hjp, length, head.r_back_mm); + Pillar& pillar = m_pillars.back(); pillar.id = long(m_pillars.size() - 1); head.pillar_id = pillar.id; @@ -326,11 +291,15 @@ public: return pillar.id; } - void add_pillar_base(long pid, double baseheight = 3, double radius = 2) + void add_pillar_base(long pid, double baseheight = 3, double radius = 2); + + template const Anchor& add_anchor(Args&&...args) { std::lock_guard lk(m_mutex); - assert(pid >= 0 && size_t(pid) < m_pillars.size()); - m_pillars[size_t(pid)].add_base(baseheight, radius); + m_anchors.emplace_back(std::forward(args)...); + m_anchors.back().id = long(m_junctions.size() - 1); + m_meshcache_valid = false; + return m_anchors.back(); } void increment_bridges(const Pillar& pillar) @@ -371,17 +340,6 @@ public: return pillar.id; } - const Pillar& head_pillar(unsigned headid) const - { - std::lock_guard lk(m_mutex); - assert(headid < m_head_indices.size()); - - const Head& h = m_heads[m_head_indices[headid]]; - assert(h.pillar_id >= 0 && h.pillar_id < long(m_pillars.size())); - - return m_pillars[size_t(h.pillar_id)]; - } - template const Junction& add_junction(Args&&... args) { std::lock_guard lk(m_mutex); @@ -391,18 +349,18 @@ public: return m_junctions.back(); } - const Bridge& add_bridge(const Vec3d &s, const Vec3d &e, double r, size_t n = 45) + const Bridge& add_bridge(const Vec3d &s, const Vec3d &e, double r) { - return _add_bridge(m_bridges, s, e, r, n); + return _add_bridge(m_bridges, s, e, r); } - const Bridge& add_bridge(long headid, const Vec3d &endp, size_t s = 45) + const Bridge& add_bridge(long headid, const Vec3d &endp) { std::lock_guard lk(m_mutex); assert(headid >= 0 && size_t(headid) < m_head_indices.size()); Head &h = m_heads[m_head_indices[size_t(headid)]]; - m_bridges.emplace_back(h.junction_point(), endp, h.r_back_mm, s); + m_bridges.emplace_back(h.junction_point(), endp, h.r_back_mm); m_bridges.back().id = long(m_bridges.size() - 1); h.bridge_id = m_bridges.back().id; @@ -414,14 +372,10 @@ public: { return _add_bridge(m_crossbridges, std::forward(args)...); } - - template const CompactBridge& add_compact_bridge(Args&&...args) + + template const DiffBridge& add_diffbridge(Args&&... args) { - std::lock_guard lk(m_mutex); - m_compact_bridges.emplace_back(std::forward(args)...); - m_compact_bridges.back().id = long(m_compact_bridges.size() - 1); - m_meshcache_valid = false; - return m_compact_bridges.back(); + return _add_bridge(m_diffbridges, std::forward(args)...); } Head &head(unsigned id) @@ -439,7 +393,7 @@ public: } inline const std::vector &pillars() const { return m_pillars; } - inline const std::vector &heads() const { return m_heads; } + inline const std::vector &heads() const { return m_heads; } inline const std::vector &bridges() const { return m_bridges; } inline const std::vector &crossbridges() const { return m_crossbridges; } @@ -464,7 +418,7 @@ public: const Pad& pad() const { return m_pad; } // WITHOUT THE PAD!!! - const TriangleMesh &merged_mesh() const; + const TriangleMesh &merged_mesh(size_t steps = 45) const; // WITH THE PAD double full_height() const; @@ -488,8 +442,6 @@ public: virtual const TriangleMesh &retrieve_mesh( MeshType meshtype = MeshType::Support) const override; - - bool build(const SupportableMesh &supportable_mesh); }; }} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportTreeBuildsteps.cpp b/src/libslic3r/SLA/SupportTreeBuildsteps.cpp index 29ad6057f1..3c39c64e6b 100644 --- a/src/libslic3r/SLA/SupportTreeBuildsteps.cpp +++ b/src/libslic3r/SLA/SupportTreeBuildsteps.cpp @@ -1,19 +1,36 @@ #include -#include -#include +#include +#include #include namespace Slic3r { namespace sla { -static const Vec3d DOWN = {0.0, 0.0, -1.0}; +using Slic3r::opt::initvals; +using Slic3r::opt::bounds; +using Slic3r::opt::StopCriteria; +using Slic3r::opt::Optimizer; +using Slic3r::opt::AlgNLoptSubplex; +using Slic3r::opt::AlgNLoptGenetic; -using libnest2d::opt::initvals; -using libnest2d::opt::bound; -using libnest2d::opt::StopCriteria; -using libnest2d::opt::GeneticOptimizer; -using libnest2d::opt::SubplexOptimizer; +StopCriteria get_criteria(const SupportTreeConfig &cfg) +{ + return StopCriteria{} + .rel_score_diff(cfg.optimizer_rel_score_diff) + .max_iterations(cfg.optimizer_max_iterations); +} + +template +static Hit min_hit(const C &hits) +{ + auto mit = std::min_element(hits.begin(), hits.end(), + [](const Hit &h1, const Hit &h2) { + return h1.distance() < h2.distance(); + }); + + return *mit; +} SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm) @@ -27,7 +44,7 @@ SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder, { // Prepare the support points in Eigen/IGL format as well, we will use // it mostly in this form. - + long i = 0; for (const SupportPoint &sp : m_support_pts) { m_points.row(i)(X) = double(sp.pos(X)); @@ -41,9 +58,11 @@ bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder, const SupportableMesh &sm) { if(sm.pts.empty()) return false; - + + builder.ground_level = sm.emesh.ground_level() - sm.cfg.object_elevation_mm; + SupportTreeBuildsteps alg(builder, sm); - + // Let's define the individual steps of the processing. We can experiment // later with the ordering and the dependencies between them. enum Steps { @@ -54,59 +73,52 @@ bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder, ROUTING_GROUND, ROUTING_NONGROUND, CASCADE_PILLARS, - HEADLESS, MERGE_RESULT, DONE, ABORT, NUM_STEPS //... }; - + // Collect the algorithm steps into a nice sequence std::array, NUM_STEPS> program = { [] () { // Begin... // Potentially clear up the shared data (not needed for now) }, - + std::bind(&SupportTreeBuildsteps::filter, &alg), - + std::bind(&SupportTreeBuildsteps::add_pinheads, &alg), - + std::bind(&SupportTreeBuildsteps::classify, &alg), - + std::bind(&SupportTreeBuildsteps::routing_to_ground, &alg), - + std::bind(&SupportTreeBuildsteps::routing_to_model, &alg), - + std::bind(&SupportTreeBuildsteps::interconnect_pillars, &alg), - - std::bind(&SupportTreeBuildsteps::routing_headless, &alg), - + std::bind(&SupportTreeBuildsteps::merge_result, &alg), - + [] () { // Done }, - + [] () { // Abort } }; - + Steps pc = BEGIN; - + if(sm.cfg.ground_facing_only) { program[ROUTING_NONGROUND] = []() { BOOST_LOG_TRIVIAL(info) << "Skipping model-facing supports as requested."; }; - program[HEADLESS] = []() { - BOOST_LOG_TRIVIAL(info) << "Skipping headless stick generation as" - " requested."; - }; } - + // Let's define a simple automaton that will run our program. auto progress = [&builder, &pc] () { static const std::array stepstr { @@ -117,12 +129,11 @@ bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder, "Routing to ground", "Routing supports to model surface", "Interconnecting pillars", - "Processing small holes", "Merging support mesh", "Done", "Abort" }; - + static const std::array stepstate { 0, 10, @@ -131,14 +142,13 @@ bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder, 60, 70, 80, - 85, 99, 100, 0 }; - + if(builder.ctl().stopcondition()) pc = ABORT; - + switch(pc) { case BEGIN: pc = FILTER; break; case FILTER: pc = PINHEADS; break; @@ -146,143 +156,78 @@ bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder, case CLASSIFY: pc = ROUTING_GROUND; break; case ROUTING_GROUND: pc = ROUTING_NONGROUND; break; case ROUTING_NONGROUND: pc = CASCADE_PILLARS; break; - case CASCADE_PILLARS: pc = HEADLESS; break; - case HEADLESS: pc = MERGE_RESULT; break; + case CASCADE_PILLARS: pc = MERGE_RESULT; break; case MERGE_RESULT: pc = DONE; break; case DONE: case ABORT: break; default: ; } - + builder.ctl().statuscb(stepstate[pc], stepstr[pc]); }; - + // Just here we run the computation... while(pc < DONE) { progress(); program[pc](); } - + return pc == ABORT; } -// Give points on a 3D ring with given center, radius and orientation -// method based on: -// https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space -template -class PointRing { - std::array m_phis; - - // Two vectors that will be perpendicular to each other and to the - // axis. Values for a(X) and a(Y) are now arbitrary, a(Z) is just a - // placeholder. - // a and b vectors are perpendicular to the ring direction and to each other. - // Together they define the plane where we have to iterate with the - // given angles in the 'm_phis' vector - Vec3d a = {0, 1, 0}, b; - double m_radius = 0.; - - static inline bool constexpr is_one(double val) - { - return std::abs(std::abs(val) - 1) < 1e-20; - } - -public: - - PointRing(const Vec3d &n) - { - m_phis = linspace_array(0., 2 * PI); - - // We have to address the case when the direction vector v (same as - // dir) is coincident with one of the world axes. In this case two of - // its components will be completely zero and one is 1.0. Our method - // becomes dangerous here due to division with zero. Instead, vector - // 'a' can be an element-wise rotated version of 'v' - if(is_one(n(X)) || is_one(n(Y)) || is_one(n(Z))) { - a = {n(Z), n(X), n(Y)}; - b = {n(Y), n(Z), n(X)}; - } - else { - a(Z) = -(n(Y)*a(Y)) / n(Z); a.normalize(); - b = a.cross(n); - } - } - - Vec3d get(size_t idx, const Vec3d src, double r) const - { - double phi = m_phis[idx]; - double sinphi = std::sin(phi); - double cosphi = std::cos(phi); - - double rpscos = r * cosphi; - double rpssin = r * sinphi; - - // Point on the sphere - return {src(X) + rpscos * a(X) + rpssin * b(X), - src(Y) + rpscos * a(Y) + rpssin * b(Y), - src(Z) + rpscos * a(Z) + rpssin * b(Z)}; - } -}; - -template -static Hit min_hit(const C &hits) -{ - auto mit = std::min_element(hits.begin(), hits.end(), - [](const Hit &h1, const Hit &h2) { - return h1.distance() < h2.distance(); - }); - - return *mit; -} - -EigenMesh3D::hit_result SupportTreeBuildsteps::pinhead_mesh_intersect( - const Vec3d &s, const Vec3d &dir, double r_pin, double r_back, double width) +IndexedMesh::hit_result SupportTreeBuildsteps::pinhead_mesh_intersect( + const Vec3d &s, + const Vec3d &dir, + double r_pin, + double r_back, + double width, + double sd) { static const size_t SAMPLES = 8; - + // Move away slightly from the touching point to avoid raycasting on the // inner surface of the mesh. - - const double& sd = m_cfg.safety_distance_mm; - + auto& m = m_mesh; - using HitResult = EigenMesh3D::hit_result; - + using HitResult = IndexedMesh::hit_result; + // Hit results std::array hits; - + struct Rings { double rpin; double rback; Vec3d spin; Vec3d sback; PointRing ring; - + Vec3d backring(size_t idx) { return ring.get(idx, sback, rback); } Vec3d pinring(size_t idx) { return ring.get(idx, spin, rpin); } } rings {r_pin + sd, r_back + sd, s, s + width * dir, dir}; - + // We will shoot multiple rays from the head pinpoint in the direction // of the pinhead robe (side) surface. The result will be the smallest // hit distance. - - ccr::enumerate(hits.begin(), hits.end(), - [&m, &rings, sd](HitResult &hit, size_t i) { - + + ccr::for_each(size_t(0), hits.size(), + [&m, &rings, sd, &hits](size_t i) { + // Point on the circle on the pin sphere Vec3d ps = rings.pinring(i); // This is the point on the circle on the back sphere Vec3d p = rings.backring(i); - + + auto &hit = hits[i]; + // Point ps is not on mesh but can be inside or // outside as well. This would cause many problems // with ray-casting. To detect the position we will // use the ray-casting result (which has an is_inside - // predicate). - + // predicate). + Vec3d n = (p - ps).normalized(); auto q = m.query_ray_hit(ps + sd * n, n); - + if (q.is_inside()) { // the hit is inside the model if (q.distance() > rings.rpin) { // If we are inside the model and the hit @@ -307,40 +252,40 @@ EigenMesh3D::hit_result SupportTreeBuildsteps::pinhead_mesh_intersect( } else hit = q; }); - + return min_hit(hits); } -EigenMesh3D::hit_result SupportTreeBuildsteps::bridge_mesh_intersect( - const Vec3d &src, const Vec3d &dir, double r, bool ins_check) +IndexedMesh::hit_result SupportTreeBuildsteps::bridge_mesh_intersect( + const Vec3d &src, const Vec3d &dir, double r, double sd) { static const size_t SAMPLES = 8; PointRing ring{dir}; - - using Hit = EigenMesh3D::hit_result; - + + using Hit = IndexedMesh::hit_result; + // Hit results std::array hits; - - ccr::enumerate(hits.begin(), hits.end(), - [this, r, src, ins_check, &ring, dir] (Hit &hit, size_t i) { - - const double sd = m_cfg.safety_distance_mm; - + + ccr::for_each(size_t(0), hits.size(), + [this, r, src, /*ins_check,*/ &ring, dir, sd, &hits] (size_t i) + { + Hit &hit = hits[i]; + // Point on the circle on the pin sphere Vec3d p = ring.get(i, src, r + sd); - - auto hr = m_mesh.query_ray_hit(p + sd * dir, dir); - - if(ins_check && hr.is_inside()) { + + auto hr = m_mesh.query_ray_hit(p + r * dir, dir); + + if(/*ins_check && */hr.is_inside()) { if(hr.distance() > 2 * r + sd) hit = Hit(0.0); else { // re-cast the ray from the outside of the object - hit = m_mesh.query_ray_hit(p + (hr.distance() + 2 * sd) * dir, dir); + hit = m_mesh.query_ray_hit(p + (hr.distance() + EPSILON) * dir, dir); } } else hit = hr; }); - + return min_hit(hits); } @@ -354,61 +299,61 @@ bool SupportTreeBuildsteps::interconnect(const Pillar &pillar, // shorter pillar is too short to start a new bridge but the taller // pillar could still be bridged with the shorter one. bool was_connected = false; - + Vec3d supper = pillar.startpoint(); Vec3d slower = nextpillar.startpoint(); Vec3d eupper = pillar.endpoint(); Vec3d elower = nextpillar.endpoint(); - + double zmin = m_builder.ground_level + m_cfg.base_height_mm; eupper(Z) = std::max(eupper(Z), zmin); elower(Z) = std::max(elower(Z), zmin); - + // The usable length of both pillars should be positive if(slower(Z) - elower(Z) < 0) return false; if(supper(Z) - eupper(Z) < 0) return false; - + double pillar_dist = distance(Vec2d{slower(X), slower(Y)}, Vec2d{supper(X), supper(Y)}); double bridge_distance = pillar_dist / std::cos(-m_cfg.bridge_slope); double zstep = pillar_dist * std::tan(-m_cfg.bridge_slope); - + if(pillar_dist < 2 * m_cfg.head_back_radius_mm || pillar_dist > m_cfg.max_pillar_link_distance_mm) return false; - + if(supper(Z) < slower(Z)) supper.swap(slower); if(eupper(Z) < elower(Z)) eupper.swap(elower); - + double startz = 0, endz = 0; - + startz = slower(Z) - zstep < supper(Z) ? slower(Z) - zstep : slower(Z); endz = eupper(Z) + zstep > elower(Z) ? eupper(Z) + zstep : eupper(Z); - + if(slower(Z) - eupper(Z) < std::abs(zstep)) { // no space for even one cross - + // Get max available space startz = std::min(supper(Z), slower(Z) - zstep); endz = std::max(eupper(Z) + zstep, elower(Z)); - + // Align to center double available_dist = (startz - endz); double rounds = std::floor(available_dist / std::abs(zstep)); startz -= 0.5 * (available_dist - rounds * std::abs(zstep)); } - + auto pcm = m_cfg.pillar_connection_mode; bool docrosses = pcm == PillarConnectionMode::cross || (pcm == PillarConnectionMode::dynamic && pillar_dist > 2*m_cfg.base_radius_mm); - + // 'sj' means starting junction, 'ej' is the end junction of a bridge. // They will be swapped in every iteration thus the zig-zag pattern. // According to a config parameter, a second bridge may be added which // results in a cross connection between the pillars. Vec3d sj = supper, ej = slower; sj(Z) = startz; ej(Z) = sj(Z) + zstep; - + // TODO: This is a workaround to not have a faulty last bridge while(ej(Z) >= eupper(Z) /*endz*/) { if(bridge_mesh_distance(sj, dirv(sj, ej), pillar.r) >= bridge_distance) @@ -416,7 +361,7 @@ bool SupportTreeBuildsteps::interconnect(const Pillar &pillar, m_builder.add_crossbridge(sj, ej, pillar.r); was_connected = true; } - + // double bridging: (crosses) if(docrosses) { Vec3d sjback(ej(X), ej(Y), sj(Z)); @@ -429,11 +374,11 @@ bool SupportTreeBuildsteps::interconnect(const Pillar &pillar, was_connected = true; } } - + sj.swap(ej); ej(Z) = sj(Z) + zstep; } - + return was_connected; } @@ -443,228 +388,242 @@ bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head, auto nearpillar = [this, nearpillar_id]() -> const Pillar& { return m_builder.pillar(nearpillar_id); }; - - if (m_builder.bridgecount(nearpillar()) > m_cfg.max_bridges_on_pillar) + + if (m_builder.bridgecount(nearpillar()) > m_cfg.max_bridges_on_pillar) return false; - + Vec3d headjp = head.junction_point(); Vec3d nearjp_u = nearpillar().startpoint(); Vec3d nearjp_l = nearpillar().endpoint(); - + double r = head.r_back_mm; double d2d = distance(to_2d(headjp), to_2d(nearjp_u)); double d3d = distance(headjp, nearjp_u); - + double hdiff = nearjp_u(Z) - headjp(Z); double slope = std::atan2(hdiff, d2d); - + Vec3d bridgestart = headjp; Vec3d bridgeend = nearjp_u; - double max_len = m_cfg.max_bridge_length_mm; + double max_len = r * m_cfg.max_bridge_length_mm / m_cfg.head_back_radius_mm; double max_slope = m_cfg.bridge_slope; double zdiff = 0.0; - + // check the default situation if feasible for a bridge if(d3d > max_len || slope > -max_slope) { // not feasible to connect the two head junctions. We have to search // for a suitable touch point. - + double Zdown = headjp(Z) + d2d * std::tan(-max_slope); Vec3d touchjp = bridgeend; touchjp(Z) = Zdown; double D = distance(headjp, touchjp); zdiff = Zdown - nearjp_u(Z); - + if(zdiff > 0) { Zdown -= zdiff; bridgestart(Z) -= zdiff; touchjp(Z) = Zdown; - + double t = bridge_mesh_distance(headjp, DOWN, r); - + // We can't insert a pillar under the source head to connect // with the nearby pillar's starting junction if(t < zdiff) return false; } - + if(Zdown <= nearjp_u(Z) && Zdown >= nearjp_l(Z) && D < max_len) bridgeend(Z) = Zdown; else return false; } - + // There will be a minimum distance from the ground where the // bridge is allowed to connect. This is an empiric value. - double minz = m_builder.ground_level + 2 * m_cfg.head_width_mm; + double minz = m_builder.ground_level + 4 * head.r_back_mm; if(bridgeend(Z) < minz) return false; - + double t = bridge_mesh_distance(bridgestart, dirv(bridgestart, bridgeend), r); - + // Cannot insert the bridge. (further search might not worth the hassle) if(t < distance(bridgestart, bridgeend)) return false; - + std::lock_guard lk(m_bridge_mutex); - + if (m_builder.bridgecount(nearpillar()) < m_cfg.max_bridges_on_pillar) { // A partial pillar is needed under the starting head. if(zdiff > 0) { - m_builder.add_pillar(head.id, bridgestart, r); + m_builder.add_pillar(head.id, headjp.z() - bridgestart.z()); m_builder.add_junction(bridgestart, r); - m_builder.add_bridge(bridgestart, bridgeend, head.r_back_mm); + m_builder.add_bridge(bridgestart, bridgeend, r); } else { m_builder.add_bridge(head.id, bridgeend); } - + m_builder.increment_bridges(nearpillar()); } else return false; - + return true; } -bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &head) -{ - PointIndex spindex = m_pillar_index.guarded_clone(); - - long nearest_id = ID_UNSET; - - Vec3d querypoint = head.junction_point(); - - while(nearest_id < 0 && !spindex.empty()) { m_thr(); - // loop until a suitable head is not found - // if there is a pillar closer than the cluster center - // (this may happen as the clustering is not perfect) - // than we will bridge to this closer pillar - - Vec3d qp(querypoint(X), querypoint(Y), m_builder.ground_level); - auto qres = spindex.nearest(qp, 1); - if(qres.empty()) break; - - auto ne = qres.front(); - nearest_id = ne.second; - - if(nearest_id >= 0) { - if(size_t(nearest_id) < m_builder.pillarcount()) { - if(!connect_to_nearpillar(head, nearest_id)) { - nearest_id = ID_UNSET; // continue searching - spindex.remove(ne); // without the current pillar - } - } - } - } - - return nearest_id >= 0; -} - -void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, +bool SupportTreeBuildsteps::create_ground_pillar(const Vec3d &hjp, const Vec3d &sourcedir, double radius, long head_id) { - const double SLOPE = 1. / std::cos(m_cfg.bridge_slope); - - double gndlvl = m_builder.ground_level; - Vec3d endp = {jp(X), jp(Y), gndlvl}; - double sd = m_cfg.pillar_base_safety_distance_mm; - long pillar_id = ID_UNSET; - double min_dist = sd + m_cfg.base_radius_mm + EPSILON; - double dist = 0; - bool can_add_base = true; - bool normal_mode = true; - - // If in zero elevation mode and the pillar is too close to the model body, - // the support pillar can not be placed in the gap between the model and - // the pad, and the pillar bases must not touch the model body either. - // To solve this, a corrector bridge is inserted between the starting point - // (jp) and the new pillar. - if (m_cfg.object_elevation_mm < EPSILON - && (dist = std::sqrt(m_mesh.squared_distance(endp))) < min_dist) { - // Get the distance from the mesh. This can be later optimized - // to get the distance in 2D plane because we are dealing with - // the ground level only. + Vec3d jp = hjp, endp = jp, dir = sourcedir; + long pillar_id = SupportTreeNode::ID_UNSET; + bool can_add_base = false, non_head = false; - normal_mode = false; + double gndlvl = 0.; // The Z level where pedestals should be + double jp_gnd = 0.; // The lowest Z where a junction center can be + double gap_dist = 0.; // The gap distance between the model and the pad - // The min distance needed to move away from the model in XY plane. - double current_d = min_dist - dist; - double current_bride_d = SLOPE * current_d; + auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; }; + auto eval_limits = [this, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd] + (bool base_en = true) + { + can_add_base = base_en && radius >= m_cfg.head_back_radius_mm; + double base_r = can_add_base ? m_cfg.base_radius_mm : 0.; + gndlvl = m_builder.ground_level; + if (!can_add_base) gndlvl -= m_mesh.ground_level_offset(); + jp_gnd = gndlvl + (can_add_base ? 0. : m_cfg.head_back_radius_mm); + gap_dist = m_cfg.pillar_base_safety_distance_mm + base_r + EPSILON; + }; + + eval_limits(); + + // We are dealing with a mini pillar that's potentially too long + if (radius < m_cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius) + { + std::optional diffbr = + search_widening_path(jp, dir, radius, m_cfg.head_back_radius_mm); + + if (diffbr && diffbr->endp.z() > jp_gnd) { + auto &br = m_builder.add_diffbridge(*diffbr); + if (head_id >= 0) m_builder.head(head_id).bridge_id = br.id; + endp = diffbr->endp; + radius = diffbr->end_r; + m_builder.add_junction(endp, radius); + non_head = true; + dir = diffbr->get_dir(); + eval_limits(); + } else return false; + } + + if (m_cfg.object_elevation_mm < EPSILON) + { // get a suitable direction for the corrector bridge. It is the // original sourcedir's azimuth but the polar angle is saturated to the // configured bridge slope. - auto [polar, azimuth] = dir_to_spheric(sourcedir); + auto [polar, azimuth] = dir_to_spheric(dir); polar = PI - m_cfg.bridge_slope; - auto dir = spheric_to_dir(polar, azimuth).normalized(); - - StopCriteria scr; - scr.stop_score = min_dist; - SubplexOptimizer solver(scr); - - // Search for a distance along the corrector bridge to move the endpoint - // sufficiently away form the model body. The first few optimization - // cycles should succeed here. - auto result = solver.optimize_max( - [this, dir, jp, gndlvl](double mv) { - Vec3d endpt = jp + mv * dir; - endpt(Z) = gndlvl; - return std::sqrt(m_mesh.squared_distance(endpt)); - }, - initvals(current_bride_d), - bound(0.0, m_cfg.max_bridge_length_mm - current_bride_d)); - - endp = jp + std::get<0>(result.optimum) * dir; - Vec3d pgnd = {endp(X), endp(Y), gndlvl}; - can_add_base = result.score > min_dist; - - double gnd_offs = m_mesh.ground_level_offset(); - auto abort_in_shame = - [gnd_offs, &normal_mode, &can_add_base, &endp, jp, gndlvl]() - { - normal_mode = true; - can_add_base = false; // Nothing left to do, hope for the best - endp = {jp(X), jp(Y), gndlvl - gnd_offs }; - }; - - // We have to check if the bridge is feasible. - if (bridge_mesh_distance(jp, dir, radius) < (endp - jp).norm()) - abort_in_shame(); - else { - // If the new endpoint is below ground, do not make a pillar - if (endp(Z) < gndlvl) - endp = endp - SLOPE * (gndlvl - endp(Z)) * dir; // back off - else { - - auto hit = bridge_mesh_intersect(endp, DOWN, radius); - if (!std::isinf(hit.distance())) abort_in_shame(); - - pillar_id = m_builder.add_pillar(endp, pgnd, radius); - - if (can_add_base) - m_builder.add_pillar_base(pillar_id, m_cfg.base_height_mm, - m_cfg.base_radius_mm); + Vec3d d = spheric_to_dir(polar, azimuth).normalized(); + double t = bridge_mesh_distance(endp, dir, radius); + double tmax = std::min(m_cfg.max_bridge_length_mm, t); + t = 0.; + + double zd = endp.z() - jp_gnd; + double tmax2 = zd / std::sqrt(1 - m_cfg.bridge_slope * m_cfg.bridge_slope); + tmax = std::min(tmax, tmax2); + + Vec3d nexp = endp; + double dlast = 0.; + while (((dlast = std::sqrt(m_mesh.squared_distance(to_floor(nexp)))) < gap_dist || + !std::isinf(bridge_mesh_distance(nexp, DOWN, radius))) && t < tmax) { + t += radius; + nexp = endp + t * d; + } + + if (dlast < gap_dist && can_add_base) { + nexp = endp; + t = 0.; + can_add_base = false; + eval_limits(can_add_base); + + zd = endp.z() - jp_gnd; + tmax2 = zd / std::sqrt(1 - m_cfg.bridge_slope * m_cfg.bridge_slope); + tmax = std::min(tmax, tmax2); + + while (((dlast = std::sqrt(m_mesh.squared_distance(to_floor(nexp)))) < gap_dist || + !std::isinf(bridge_mesh_distance(nexp, DOWN, radius))) && t < tmax) { + t += radius; + nexp = endp + t * d; } - - m_builder.add_bridge(jp, endp, radius); - m_builder.add_junction(endp, radius); - - // Add a degenerated pillar and the bridge. - // The degenerate pillar will have zero length and it will - // prevent from queries of head_pillar() to have non-existing - // pillar when the head should have one. - if (head_id >= 0) - m_builder.add_pillar(head_id, jp, radius); + } + + // Could not find a path to avoid the pad gap + if (dlast < gap_dist) return false; + + if (t > 0.) { // Need to make additional bridge + const Bridge& br = m_builder.add_bridge(endp, nexp, radius); + if (head_id >= 0) m_builder.head(head_id).bridge_id = br.id; + + m_builder.add_junction(nexp, radius); + endp = nexp; + non_head = true; } } - - if (normal_mode) { - pillar_id = head_id >= 0 ? m_builder.add_pillar(head_id, endp, radius) : - m_builder.add_pillar(jp, endp, radius); - if (can_add_base) - m_builder.add_pillar_base(pillar_id, m_cfg.base_height_mm, - m_cfg.base_radius_mm); - } - + Vec3d gp = to_floor(endp); + double h = endp.z() - gp.z(); + + pillar_id = head_id >= 0 && !non_head ? m_builder.add_pillar(head_id, h) : + m_builder.add_pillar(gp, h, radius); + + if (can_add_base) + add_pillar_base(pillar_id); + if(pillar_id >= 0) // Save the pillar endpoint in the spatial index - m_pillar_index.guarded_insert(endp, unsigned(pillar_id)); + m_pillar_index.guarded_insert(m_builder.pillar(pillar_id).endpt, + unsigned(pillar_id)); + + return true; +} + +std::optional SupportTreeBuildsteps::search_widening_path( + const Vec3d &jp, const Vec3d &dir, double radius, double new_radius) +{ + double w = radius + 2 * m_cfg.head_back_radius_mm; + double stopval = w + jp.z() - m_builder.ground_level; + Optimizer solver(get_criteria(m_cfg).stop_score(stopval)); + + auto [polar, azimuth] = dir_to_spheric(dir); + + double fallback_ratio = radius / m_cfg.head_back_radius_mm; + + auto oresult = solver.to_max().optimize( + [this, jp, radius, new_radius](const opt::Input<3> &input) { + auto &[plr, azm, t] = input; + + auto d = spheric_to_dir(plr, azm).normalized(); + double ret = pinhead_mesh_intersect(jp, d, radius, new_radius, t) + .distance(); + double down = bridge_mesh_distance(jp + t * d, d, new_radius); + + if (ret > t && std::isinf(down)) + ret += jp.z() - m_builder.ground_level; + + return ret; + }, + initvals({polar, azimuth, w}), // start with what we have + bounds({ + {PI - m_cfg.bridge_slope, PI}, // Must not exceed the slope limit + {-PI, PI}, // azimuth can be a full search + {radius + m_cfg.head_back_radius_mm, + fallback_ratio * m_cfg.max_bridge_length_mm} + })); + + if (oresult.score >= stopval) { + polar = std::get<0>(oresult.optimum); + azimuth = std::get<1>(oresult.optimum); + double t = std::get<2>(oresult.optimum); + Vec3d endp = jp + t * spheric_to_dir(polar, azimuth); + + return DiffBridge(jp, endp, radius, m_cfg.head_back_radius_mm); + } + + return {}; } void SupportTreeBuildsteps::filter() @@ -672,7 +631,7 @@ void SupportTreeBuildsteps::filter() // Get the points that are too close to each other and keep only the // first one auto aliases = cluster(m_points, D_SP, 2); - + PtIndices filtered_indices; filtered_indices.reserve(aliases.size()); m_iheads.reserve(aliases.size()); @@ -681,136 +640,130 @@ void SupportTreeBuildsteps::filter() // Here we keep only the front point of the cluster. filtered_indices.emplace_back(a.front()); } - + // calculate the normals to the triangles for filtered points auto nmls = sla::normals(m_points, m_mesh, m_cfg.head_front_radius_mm, m_thr, filtered_indices); - + // Not all of the support points have to be a valid position for // support creation. The angle may be inappropriate or there may // not be enough space for the pinhead. Filtering is applied for // these reasons. - - ccr::SpinningMutex mutex; - auto addfn = [&mutex](PtIndices &container, unsigned val) { - std::lock_guard lk(mutex); - container.emplace_back(val); - }; - - auto filterfn = [this, &nmls, addfn](unsigned fidx, size_t i) { + + std::vector heads; heads.reserve(m_support_pts.size()); + for (const SupportPoint &sp : m_support_pts) { m_thr(); - + heads.emplace_back( + std::nan(""), + sp.head_front_radius, + 0., + m_cfg.head_penetration_mm, + Vec3d::Zero(), // dir + sp.pos.cast() // displacement + ); + } + + std::function filterfn; + filterfn = [this, &nmls, &heads, &filterfn](unsigned fidx, size_t i, double back_r) { + m_thr(); + auto n = nmls.row(Eigen::Index(i)); - + // for all normals we generate the spherical coordinates and // saturate the polar angle to 45 degrees from the bottom then // convert back to standard coordinates to get the new normal. // Then we just create a quaternion from the two normals // (Quaternion::FromTwoVectors) and apply the rotation to the // arrow head. - + auto [polar, azimuth] = dir_to_spheric(n); - + // skip if the tilt is not sane - if(polar >= PI - m_cfg.normal_cutoff_angle) { - - // We saturate the polar angle to 3pi/4 - polar = std::max(polar, 3*PI / 4); - - // save the head (pinpoint) position - Vec3d hp = m_points.row(fidx); - - double w = m_cfg.head_width_mm + - m_cfg.head_back_radius_mm + - 2*m_cfg.head_front_radius_mm; - - double pin_r = double(m_support_pts[fidx].head_front_radius); - - // Reassemble the now corrected normal - auto nn = spheric_to_dir(polar, azimuth).normalized(); - - // check available distance - EigenMesh3D::hit_result t - = pinhead_mesh_intersect(hp, // touching point - nn, // normal - pin_r, - m_cfg.head_back_radius_mm, - w); - - if(t.distance() <= w) { - - // Let's try to optimize this angle, there might be a - // viable normal that doesn't collide with the model - // geometry and its very close to the default. - - StopCriteria stc; - stc.max_iterations = m_cfg.optimizer_max_iterations; - stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; - stc.stop_score = w; // space greater than w is enough - GeneticOptimizer solver(stc); - solver.seed(0); // we want deterministic behavior - - auto oresult = solver.optimize_max( - [this, pin_r, w, hp](double plr, double azm) - { - auto dir = spheric_to_dir(plr, azm).normalized(); - - double score = pinhead_mesh_distance( - hp, dir, pin_r, m_cfg.head_back_radius_mm, w); - - return score; - }, - initvals(polar, azimuth), // start with what we have - bound(3 * PI / 4, PI), // Must not exceed the tilt limit - bound(-PI, PI) // azimuth can be a full search - ); - - if(oresult.score > w) { - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - nn = spheric_to_dir(polar, azimuth).normalized(); - t = EigenMesh3D::hit_result(oresult.score); - } - } - - // save the verified and corrected normal - m_support_nmls.row(fidx) = nn; - - if (t.distance() > w) { - // Check distance from ground, we might have zero elevation. - if (hp(Z) + w * nn(Z) < m_builder.ground_level) { - addfn(m_iheadless, fidx); - } else { - // mark the point for needing a head. - addfn(m_iheads, fidx); - } - } else if (polar >= 3 * PI / 4) { - // Headless supports do not tilt like the headed ones - // so the normal should point almost to the ground. - addfn(m_iheadless, fidx); + if (polar < PI - m_cfg.normal_cutoff_angle) return; + + // We saturate the polar angle to 3pi/4 + polar = std::max(polar, PI - m_cfg.bridge_slope); + + // save the head (pinpoint) position + Vec3d hp = m_points.row(fidx); + + double lmin = m_cfg.head_width_mm, lmax = lmin; + + if (back_r < m_cfg.head_back_radius_mm) { + lmin = 0., lmax = m_cfg.head_penetration_mm; + } + + // The distance needed for a pinhead to not collide with model. + double w = lmin + 2 * back_r + 2 * m_cfg.head_front_radius_mm - + m_cfg.head_penetration_mm; + + double pin_r = double(m_support_pts[fidx].head_front_radius); + + // Reassemble the now corrected normal + auto nn = spheric_to_dir(polar, azimuth).normalized(); + + // check available distance + IndexedMesh::hit_result t = pinhead_mesh_intersect(hp, nn, pin_r, + back_r, w); + + if (t.distance() < w) { + // Let's try to optimize this angle, there might be a + // viable normal that doesn't collide with the model + // geometry and its very close to the default. + + Optimizer solver(get_criteria(m_cfg)); + solver.seed(0); // we want deterministic behavior + + auto oresult = solver.to_max().optimize( + [this, pin_r, back_r, hp](const opt::Input<3> &input) + { + auto &[plr, azm, l] = input; + + auto dir = spheric_to_dir(plr, azm).normalized(); + + return pinhead_mesh_intersect( + hp, dir, pin_r, back_r, l).distance(); + }, + initvals({polar, azimuth, (lmin + lmax) / 2.}), // start with what we have + bounds({ + {PI - m_cfg.bridge_slope, PI}, // Must not exceed the slope limit + {-PI, PI}, // azimuth can be a full search + {lmin, lmax} + })); + + if(oresult.score > w) { + polar = std::get<0>(oresult.optimum); + azimuth = std::get<1>(oresult.optimum); + nn = spheric_to_dir(polar, azimuth).normalized(); + lmin = std::get<2>(oresult.optimum); + t = IndexedMesh::hit_result(oresult.score); } } + + if (t.distance() > w && hp(Z) + w * nn(Z) >= m_builder.ground_level) { + Head &h = heads[fidx]; + h.id = fidx; h.dir = nn; h.width_mm = lmin; h.r_back_mm = back_r; + } else if (back_r > m_cfg.head_fallback_radius_mm) { + filterfn(fidx, i, m_cfg.head_fallback_radius_mm); + } }; - - ccr::enumerate(filtered_indices.begin(), filtered_indices.end(), filterfn); - + + ccr::for_each(size_t(0), filtered_indices.size(), + [this, &filterfn, &filtered_indices] (size_t i) { + filterfn(filtered_indices[i], i, m_cfg.head_back_radius_mm); + }); + + for (size_t i = 0; i < heads.size(); ++i) + if (heads[i].is_valid()) { + m_builder.add_head(i, heads[i]); + m_iheads.emplace_back(i); + } + m_thr(); } void SupportTreeBuildsteps::add_pinheads() { - for (unsigned i : m_iheads) { - m_thr(); - m_builder.add_head( - i, - m_cfg.head_back_radius_mm, - m_support_pts[i].head_front_radius, - m_cfg.head_width_mm, - m_cfg.head_penetration_mm, - m_support_nmls.row(i), // dir - m_support_pts[i].pos.cast() // displacement - ); - } } void SupportTreeBuildsteps::classify() @@ -819,37 +772,37 @@ void SupportTreeBuildsteps::classify() PtIndices ground_head_indices; ground_head_indices.reserve(m_iheads.size()); m_iheads_onmodel.reserve(m_iheads.size()); - + // First we decide which heads reach the ground and can be full // pillars and which shall be connected to the model surface (or // search a suitable path around the surface that leads to the // ground -- TODO) for(unsigned i : m_iheads) { m_thr(); - - auto& head = m_builder.head(i); + + Head &head = m_builder.head(i); double r = head.r_back_mm; Vec3d headjp = head.junction_point(); - + // collision check auto hit = bridge_mesh_intersect(headjp, DOWN, r); - + if(std::isinf(hit.distance())) ground_head_indices.emplace_back(i); else if(m_cfg.ground_facing_only) head.invalidate(); else m_iheads_onmodel.emplace_back(i); - + m_head_to_ground_scans[i] = hit; } - + // We want to search for clusters of points that are far enough // from each other in the XY plane to not cross their pillar bases // These clusters of support points will join in one pillar, // possibly in their centroid support point. - + auto pointfn = [this](unsigned i) { return m_builder.head(i).junction_point(); }; - + auto predicate = [this](const PointIndexEl &e1, const PointIndexEl &e2) { double d2d = distance(to_2d(e1.first), to_2d(e2.first)); @@ -864,14 +817,12 @@ void SupportTreeBuildsteps::classify() void SupportTreeBuildsteps::routing_to_ground() { - const double pradius = m_cfg.head_back_radius_mm; - ClusterEl cl_centroids; cl_centroids.reserve(m_pillar_clusters.size()); - + for (auto &cl : m_pillar_clusters) { m_thr(); - + // place all the centroid head positions into the index. We // will query for alternative pillar positions. If a sidehead // cannot connect to the cluster centroid, we have to search @@ -879,9 +830,9 @@ void SupportTreeBuildsteps::routing_to_ground() // elements in the cluster, the centroid is arbitrary and the // sidehead is allowed to connect to a nearby pillar to // increase structural stability. - + if (cl.empty()) continue; - + // get the current cluster centroid auto & thr = m_thr; const auto &points = m_points; @@ -895,43 +846,44 @@ void SupportTreeBuildsteps::routing_to_ground() assert(lcid >= 0); unsigned hid = cl[size_t(lcid)]; // Head ID - + cl_centroids.emplace_back(hid); - + Head &h = m_builder.head(hid); - h.transform(); - - create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id); + + if (!create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id)) { + BOOST_LOG_TRIVIAL(warning) + << "Pillar cannot be created for support point id: " << hid; + m_iheads_onmodel.emplace_back(h.id); + continue; + } } - + // now we will go through the clusters ones again and connect the // sidepoints with the cluster centroid (which is a ground pillar) // or a nearby pillar if the centroid is unreachable. size_t ci = 0; for (auto cl : m_pillar_clusters) { m_thr(); - + auto cidx = cl_centroids[ci++]; - - // TODO: don't consider the cluster centroid but calculate a - // central position where the pillar can be placed. this way - // the weight is distributed more effectively on the pillar. - - auto centerpillarID = m_builder.head_pillar(cidx).id; - - for (auto c : cl) { - m_thr(); - if (c == cidx) continue; - - auto &sidehead = m_builder.head(c); - sidehead.transform(); - - if (!connect_to_nearpillar(sidehead, centerpillarID) && - !search_pillar_and_connect(sidehead)) { - Vec3d pstart = sidehead.junction_point(); - // Vec3d pend = Vec3d{pstart(X), pstart(Y), gndlvl}; - // Could not find a pillar, create one - create_ground_pillar(pstart, sidehead.dir, pradius, sidehead.id); + + auto q = m_pillar_index.query(m_builder.head(cidx).junction_point(), 1); + if (!q.empty()) { + long centerpillarID = q.front().second; + for (auto c : cl) { + m_thr(); + if (c == cidx) continue; + + auto &sidehead = m_builder.head(c); + + if (!connect_to_nearpillar(sidehead, centerpillarID) && + !search_pillar_and_connect(sidehead)) { + Vec3d pstart = sidehead.junction_point(); + // Vec3d pend = Vec3d{pstart(X), pstart(Y), gndlvl}; + // Could not find a pillar, create one + create_ground_pillar(pstart, sidehead.dir, sidehead.r_back_mm, sidehead.id); + } } } } @@ -943,62 +895,66 @@ bool SupportTreeBuildsteps::connect_to_ground(Head &head, const Vec3d &dir) double r = head.r_back_mm; double t = bridge_mesh_distance(hjp, dir, head.r_back_mm); double d = 0, tdown = 0; - t = std::min(t, m_cfg.max_bridge_length_mm); + t = std::min(t, m_cfg.max_bridge_length_mm * r / m_cfg.head_back_radius_mm); while (d < t && !std::isinf(tdown = bridge_mesh_distance(hjp + d * dir, DOWN, r))) d += r; - + if(!std::isinf(tdown)) return false; - + Vec3d endp = hjp + d * dir; - m_builder.add_bridge(head.id, endp); - m_builder.add_junction(endp, head.r_back_mm); - - this->create_ground_pillar(endp, dir, head.r_back_mm); - - return true; + bool ret = false; + + if ((ret = create_ground_pillar(endp, dir, head.r_back_mm))) { + m_builder.add_bridge(head.id, endp); + m_builder.add_junction(endp, head.r_back_mm); + } + + return ret; } bool SupportTreeBuildsteps::connect_to_ground(Head &head) { if (connect_to_ground(head, head.dir)) return true; - + // Optimize bridge direction: // Straight path failed so we will try to search for a suitable // direction out of the cavity. auto [polar, azimuth] = dir_to_spheric(head.dir); - - StopCriteria stc; - stc.max_iterations = m_cfg.optimizer_max_iterations; - stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; - stc.stop_score = 1e6; - GeneticOptimizer solver(stc); + + Optimizer solver(get_criteria(m_cfg).stop_score(1e6)); solver.seed(0); // we want deterministic behavior - + double r_back = head.r_back_mm; - Vec3d hjp = head.junction_point(); - auto oresult = solver.optimize_max( - [this, hjp, r_back](double plr, double azm) { + Vec3d hjp = head.junction_point(); + auto oresult = solver.to_max().optimize( + [this, hjp, r_back](const opt::Input<2> &input) { + auto &[plr, azm] = input; Vec3d n = spheric_to_dir(plr, azm).normalized(); return bridge_mesh_distance(hjp, n, r_back); }, - initvals(polar, azimuth), // let's start with what we have - bound(3*PI/4, PI), // Must not exceed the slope limit - bound(-PI, PI) // azimuth can be a full range search - ); - + initvals({polar, azimuth}), // let's start with what we have + bounds({ {PI - m_cfg.bridge_slope, PI}, {-PI, PI} }) + ); + Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); return connect_to_ground(head, bridgedir); } bool SupportTreeBuildsteps::connect_to_model_body(Head &head) { - if (head.id <= ID_UNSET) return false; - + if (head.id <= SupportTreeNode::ID_UNSET) return false; + auto it = m_head_to_ground_scans.find(unsigned(head.id)); if (it == m_head_to_ground_scans.end()) return false; - + auto &hit = it->second; + + if (!hit.is_hit()) { + // TODO scan for potential anchor points on model surface + return false; + } + Vec3d hjp = head.junction_point(); double zangle = std::asin(hit.direction()(Z)); zangle = std::max(zangle, PI/4); @@ -1006,9 +962,11 @@ bool SupportTreeBuildsteps::connect_to_model_body(Head &head) // The width of the tail head that we would like to have... h = std::min(hit.distance() - head.r_back_mm, h); - - if(h <= 0.) return false; - + + // If this is a mini pillar dont bother with the tail width, can be 0. + if (head.r_back_mm < m_cfg.head_back_radius_mm) h = std::max(h, 0.); + else if (h <= 0.) return false; + Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h}; auto center_hit = m_mesh.query_ray_hit(hjp, DOWN); @@ -1016,13 +974,11 @@ bool SupportTreeBuildsteps::connect_to_model_body(Head &head) Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm? center_hit.position() : hit.position(); - head.transform(); - - long pillar_id = m_builder.add_pillar(head.id, endp, head.r_back_mm); + long pillar_id = m_builder.add_pillar(head.id, hjp.z() - endp.z()); Pillar &pill = m_builder.pillar(pillar_id); Vec3d taildir = endp - hitp; - double dist = distance(endp, hitp) + m_cfg.head_penetration_mm; + double dist = (hitp - endp).norm() + m_cfg.head_penetration_mm; double w = dist - 2 * head.r_pin_mm - head.r_back_mm; if (w < 0.) { @@ -1030,43 +986,77 @@ bool SupportTreeBuildsteps::connect_to_model_body(Head &head) w = 0.; } - Head tailhead(head.r_back_mm, head.r_pin_mm, w, - m_cfg.head_penetration_mm, taildir, hitp); + m_builder.add_anchor(head.r_back_mm, head.r_pin_mm, w, + m_cfg.head_penetration_mm, taildir, hitp); - tailhead.transform(); - pill.base = tailhead.mesh; - m_pillar_index.guarded_insert(pill.endpoint(), pill.id); - + return true; } +bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &source) +{ + // Hope that a local copy takes less time than the whole search loop. + // We also need to remove elements progressively from the copied index. + PointIndex spindex = m_pillar_index.guarded_clone(); + + long nearest_id = SupportTreeNode::ID_UNSET; + + Vec3d querypt = source.junction_point(); + + while(nearest_id < 0 && !spindex.empty()) { m_thr(); + // loop until a suitable head is not found + // if there is a pillar closer than the cluster center + // (this may happen as the clustering is not perfect) + // than we will bridge to this closer pillar + + Vec3d qp(querypt(X), querypt(Y), m_builder.ground_level); + auto qres = spindex.nearest(qp, 1); + if(qres.empty()) break; + + auto ne = qres.front(); + nearest_id = ne.second; + + if(nearest_id >= 0) { + if (size_t(nearest_id) < m_builder.pillarcount()) { + if(!connect_to_nearpillar(source, nearest_id) || + m_builder.pillar(nearest_id).r < source.r_back_mm) { + nearest_id = SupportTreeNode::ID_UNSET; // continue searching + spindex.remove(ne); // without the current pillar + } + } + } + } + + return nearest_id >= 0; +} + void SupportTreeBuildsteps::routing_to_model() -{ +{ // We need to check if there is an easy way out to the bed surface. // If it can be routed there with a bridge shorter than // min_bridge_distance. - ccr::enumerate(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), - [this] (const unsigned idx, size_t) { + ccr::for_each(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), + [this] (const unsigned idx) { m_thr(); - + auto& head = m_builder.head(idx); - + // Search nearby pillar - if(search_pillar_and_connect(head)) { head.transform(); return; } - + if (search_pillar_and_connect(head)) { return; } + // Cannot connect to nearby pillar. We will try to search for // a route to the ground. - if(connect_to_ground(head)) { head.transform(); return; } - + if (connect_to_ground(head)) { return; } + // No route to the ground, so connect to the model body as a last resort if (connect_to_model_body(head)) { return; } - + // We have failed to route this head. BOOST_LOG_TRIVIAL(warning) - << "Failed to route model facing support point. ID: " << idx; - + << "Failed to route model facing support point. ID: " << idx; + head.invalidate(); }); } @@ -1076,19 +1066,19 @@ void SupportTreeBuildsteps::interconnect_pillars() // Now comes the algorithm that connects pillars with each other. // Ideally every pillar should be connected with at least one of its // neighbors if that neighbor is within max_pillar_link_distance - + // Pillars with height exceeding H1 will require at least one neighbor // to connect with. Height exceeding H2 require two neighbors. double H1 = m_cfg.max_solo_pillar_height_mm; double H2 = m_cfg.max_dual_pillar_height_mm; double d = m_cfg.max_pillar_link_distance_mm; - + //A connection between two pillars only counts if the height ratio is // bigger than 50% double min_height_ratio = 0.5; - + std::set pairs; - + // A function to connect one pillar with its neighbors. THe number of // neighbors is given in the configuration. This function if called // for every pillar in the pillar index. A pair of pillar will not @@ -1098,66 +1088,68 @@ void SupportTreeBuildsteps::interconnect_pillars() [this, d, &pairs, min_height_ratio, H1] (const PointIndexEl& el) { Vec3d qp = el.first; // endpoint of the pillar - + const Pillar& pillar = m_builder.pillar(el.second); // actual pillar - + // Get the max number of neighbors a pillar should connect to unsigned neighbors = m_cfg.pillar_cascade_neighbors; - + // connections are already enough for the pillar if(pillar.links >= neighbors) return; - + + double max_d = d * pillar.r / m_cfg.head_back_radius_mm; // Query all remaining points within reach - auto qres = m_pillar_index.query([qp, d](const PointIndexEl& e){ - return distance(e.first, qp) < d; + auto qres = m_pillar_index.query([qp, max_d](const PointIndexEl& e){ + return distance(e.first, qp) < max_d; }); - + // sort the result by distance (have to check if this is needed) std::sort(qres.begin(), qres.end(), [qp](const PointIndexEl& e1, const PointIndexEl& e2){ return distance(e1.first, qp) < distance(e2.first, qp); }); - + for(auto& re : qres) { // process the queried neighbors - + if(re.second == el.second) continue; // Skip self - + auto a = el.second, b = re.second; - + // Get unique hash for the given pair (order doesn't matter) auto hashval = pairhash(a, b); - + // Search for the pair amongst the remembered pairs if(pairs.find(hashval) != pairs.end()) continue; - + const Pillar& neighborpillar = m_builder.pillar(re.second); - + // this neighbor is occupied, skip - if(neighborpillar.links >= neighbors) continue; - + if (neighborpillar.links >= neighbors) continue; + if (neighborpillar.r < pillar.r) continue; + if(interconnect(pillar, neighborpillar)) { pairs.insert(hashval); - + // If the interconnection length between the two pillars is // less than 50% of the longer pillar's height, don't count if(pillar.height < H1 || neighborpillar.height / pillar.height > min_height_ratio) m_builder.increment_links(pillar); - + if(neighborpillar.height < H1 || pillar.height / neighborpillar.height > min_height_ratio) m_builder.increment_links(neighborpillar); - + } - + // connections are enough for one pillar if(pillar.links >= neighbors) break; } }; - + // Run the cascade for the pillars in the index m_pillar_index.foreach(cascadefn); - + // We would be done here if we could allow some pillars to not be // connected with any neighbors. But this might leave the support tree // unprintable. @@ -1165,16 +1157,16 @@ void SupportTreeBuildsteps::interconnect_pillars() // The current solution is to insert additional pillars next to these // lonely pillars. One or even two additional pillar might get inserted // depending on the length of the lonely pillar. - + size_t pillarcount = m_builder.pillarcount(); - + // Again, go through all pillars, this time in the whole support tree // not just the index. for(size_t pid = 0; pid < pillarcount; pid++) { auto pillar = [this, pid]() { return m_builder.pillar(pid); }; - + // Decide how many additional pillars will be needed: - + unsigned needpillars = 0; if (pillar().bridges > m_cfg.max_bridges_on_pillar) needpillars = 3; @@ -1185,28 +1177,28 @@ void SupportTreeBuildsteps::interconnect_pillars() // No neighbors could be found and the pillar is too long. needpillars = 1; } - + needpillars = std::max(pillar().links, needpillars) - pillar().links; if (needpillars == 0) continue; - + // Search for new pillar locations: - + bool found = false; double alpha = 0; // goes to 2Pi double r = 2 * m_cfg.base_radius_mm; Vec3d pillarsp = pillar().startpoint(); - + // temp value for starting point detection Vec3d sp(pillarsp(X), pillarsp(Y), pillarsp(Z) - r); - + // A vector of bool for placement feasbility std::vector canplace(needpillars, false); std::vector spts(needpillars); // vector of starting points - + double gnd = m_builder.ground_level; double min_dist = m_cfg.pillar_base_safety_distance_mm + m_cfg.base_radius_mm + EPSILON; - + while(!found && alpha < 2*PI) { for (unsigned n = 0; n < needpillars && (!n || canplace[n - 1]); @@ -1217,36 +1209,38 @@ void SupportTreeBuildsteps::interconnect_pillars() s(X) += std::cos(a) * r; s(Y) += std::sin(a) * r; spts[n] = s; - + // Check the path vertically down Vec3d check_from = s + Vec3d{0., 0., pillar().r}; auto hr = bridge_mesh_intersect(check_from, DOWN, pillar().r); Vec3d gndsp{s(X), s(Y), gnd}; - + // If the path is clear, check for pillar base collisions canplace[n] = std::isinf(hr.distance()) && std::sqrt(m_mesh.squared_distance(gndsp)) > min_dist; } - + found = std::all_of(canplace.begin(), canplace.end(), [](bool v) { return v; }); - + // 20 angles will be tried... alpha += 0.1 * PI; } - + std::vector newpills; newpills.reserve(needpillars); if (found) for (unsigned n = 0; n < needpillars; n++) { - Vec3d s = spts[n]; - Pillar p(s, Vec3d(s(X), s(Y), gnd), pillar().r); - p.add_base(m_cfg.base_height_mm, m_cfg.base_radius_mm); + Vec3d s = spts[n]; + Pillar p(Vec3d{s.x(), s.y(), gnd}, s.z() - gnd, pillar().r); if (interconnect(pillar(), p)) { Pillar &pp = m_builder.pillar(m_builder.add_pillar(p)); + + add_pillar_base(pp.id); + m_pillar_index.insert(pp.endpoint(), unsigned(pp.id)); m_builder.add_junction(s, pillar().r); @@ -1255,9 +1249,8 @@ void SupportTreeBuildsteps::interconnect_pillars() if (distance(pillarsp, s) < t) m_builder.add_bridge(pillarsp, s, pillar().r); - if (pillar().endpoint()(Z) > m_builder.ground_level) - m_builder.add_junction(pillar().endpoint(), - pillar().r); + if (pillar().endpoint()(Z) > m_builder.ground_level + pillar().r) + m_builder.add_junction(pillar().endpoint(), pillar().r); newpills.emplace_back(pp.id); m_builder.increment_links(pillar()); @@ -1275,51 +1268,10 @@ void SupportTreeBuildsteps::interconnect_pillars() m_builder.increment_links(nxpll); } } - + m_pillar_index.foreach(cascadefn); } } } -void SupportTreeBuildsteps::routing_headless() -{ - // For now we will just generate smaller headless sticks with a sharp - // ending point that connects to the mesh surface. - - // We will sink the pins into the model surface for a distance of 1/3 of - // the pin radius - for(unsigned i : m_iheadless) { - m_thr(); - - const auto R = double(m_support_pts[i].head_front_radius); - const double HWIDTH_MM = std::min(R, m_cfg.head_penetration_mm); - - // Exact support position - Vec3d sph = m_support_pts[i].pos.cast(); - Vec3d n = m_support_nmls.row(i); // mesh outward normal - Vec3d sp = sph - n * HWIDTH_MM; // stick head start point - - Vec3d sj = sp + R * n; // stick start point - - // This is only for checking - double idist = bridge_mesh_distance(sph, DOWN, R, true); - double realdist = ray_mesh_intersect(sj, DOWN).distance(); - double dist = realdist; - - if (std::isinf(dist)) dist = sph(Z) - m_builder.ground_level; - - if(std::isnan(idist) || idist < 2*R || std::isnan(dist) || dist < 2*R) { - BOOST_LOG_TRIVIAL(warning) << "Can not find route for headless" - << " support stick at: " - << sj.transpose(); - continue; - } - - bool use_endball = !std::isinf(realdist); - Vec3d ej = sj + (dist + HWIDTH_MM) * DOWN ; - m_builder.add_compact_bridge(sp, ej, n, R, use_endball); - } -} - -} -} +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportTreeBuildsteps.hpp b/src/libslic3r/SLA/SupportTreeBuildsteps.hpp index cfe78fe97a..013666f074 100644 --- a/src/libslic3r/SLA/SupportTreeBuildsteps.hpp +++ b/src/libslic3r/SLA/SupportTreeBuildsteps.hpp @@ -5,6 +5,7 @@ #include #include +#include namespace Slic3r { namespace sla { @@ -16,9 +17,7 @@ enum { // For indexing Eigen vectors as v(X), v(Y), v(Z) instead of numbers X, Y, Z }; -inline Vec2d to_vec2(const Vec3d& v3) { - return {v3(X), v3(Y)}; -} +inline Vec2d to_vec2(const Vec3d &v3) { return {v3(X), v3(Y)}; } inline std::pair dir_to_spheric(const Vec3d &n, double norm = 1.) { @@ -46,55 +45,71 @@ inline Vec3d spheric_to_dir(const std::pair &v) return spheric_to_dir(v.first, v.second); } -// This function returns the position of the centroid in the input 'clust' -// vector of point indices. -template -long cluster_centroid(const ClusterEl& clust, - const std::function &pointfn, - DistFn df) +inline Vec3d spheric_to_dir(const std::array &v) { - switch(clust.size()) { - case 0: /* empty cluster */ return ID_UNSET; - case 1: /* only one element */ return 0; - case 2: /* if two elements, there is no center */ return 0; - default: ; + return spheric_to_dir(v[0], v[1]); +} + +// Give points on a 3D ring with given center, radius and orientation +// method based on: +// https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space +template +class PointRing { + std::array m_phis; + + // Two vectors that will be perpendicular to each other and to the + // axis. Values for a(X) and a(Y) are now arbitrary, a(Z) is just a + // placeholder. + // a and b vectors are perpendicular to the ring direction and to each other. + // Together they define the plane where we have to iterate with the + // given angles in the 'm_phis' vector + Vec3d a = {0, 1, 0}, b; + double m_radius = 0.; + + static inline bool constexpr is_one(double val) + { + return std::abs(std::abs(val) - 1) < 1e-20; } - // The function works by calculating for each point the average distance - // from all the other points in the cluster. We create a selector bitmask of - // the same size as the cluster. The bitmask will have two true bits and - // false bits for the rest of items and we will loop through all the - // permutations of the bitmask (combinations of two points). Get the - // distance for the two points and add the distance to the averages. - // The point with the smallest average than wins. +public: - // The complexity should be O(n^2) but we will mostly apply this function - // for small clusters only (cca 3 elements) + PointRing(const Vec3d &n) + { + m_phis = linspace_array(0., 2 * PI); - std::vector sel(clust.size(), false); // create full zero bitmask - std::fill(sel.end() - 2, sel.end(), true); // insert the two ones - std::vector avgs(clust.size(), 0.0); // store the average distances + // We have to address the case when the direction vector v (same as + // dir) is coincident with one of the world axes. In this case two of + // its components will be completely zero and one is 1.0. Our method + // becomes dangerous here due to division with zero. Instead, vector + // 'a' can be an element-wise rotated version of 'v' + if(is_one(n(X)) || is_one(n(Y)) || is_one(n(Z))) { + a = {n(Z), n(X), n(Y)}; + b = {n(Y), n(Z), n(X)}; + } + else { + a(Z) = -(n(Y)*a(Y)) / n(Z); a.normalize(); + b = a.cross(n); + } + } - do { - std::array idx; - for(size_t i = 0, j = 0; i < clust.size(); i++) if(sel[i]) idx[j++] = i; + Vec3d get(size_t idx, const Vec3d src, double r) const + { + double phi = m_phis[idx]; + double sinphi = std::sin(phi); + double cosphi = std::cos(phi); - double d = df(pointfn(clust[idx[0]]), - pointfn(clust[idx[1]])); + double rpscos = r * cosphi; + double rpssin = r * sinphi; - // add the distance to the sums for both associated points - for(auto i : idx) avgs[i] += d; + // Point on the sphere + return {src(X) + rpscos * a(X) + rpssin * b(X), + src(Y) + rpscos * a(Y) + rpssin * b(Y), + src(Z) + rpscos * a(Z) + rpssin * b(Z)}; + } +}; - // now continue with the next permutation of the bitmask with two 1s - } while(std::next_permutation(sel.begin(), sel.end())); - - // Divide by point size in the cluster to get the average (may be redundant) - for(auto& a : avgs) a /= clust.size(); - - // get the lowest average distance and return the index - auto minit = std::min_element(avgs.begin(), avgs.end()); - return long(minit - avgs.begin()); -} +//IndexedMesh::hit_result query_hit(const SupportableMesh &msh, const Bridge &br, double safety_d = std::nan("")); +//IndexedMesh::hit_result query_hit(const SupportableMesh &msh, const Head &br, double safety_d = std::nan("")); inline Vec3d dirv(const Vec3d& startp, const Vec3d& endp) { return (endp - startp).normalized(); @@ -170,8 +185,8 @@ IntegerOnly pairhash(I a, I b) } class SupportTreeBuildsteps { - const SupportConfig& m_cfg; - const EigenMesh3D& m_mesh; + const SupportTreeConfig& m_cfg; + const IndexedMesh& m_mesh; const std::vector& m_support_pts; using PtIndices = std::vector; @@ -180,7 +195,7 @@ class SupportTreeBuildsteps { PtIndices m_iheads_onmodel; PtIndices m_iheadless; // headless support points - std::map m_head_to_ground_scans; + std::map m_head_to_ground_scans; // normals for support points from model faces. PointSet m_support_nmls; @@ -206,7 +221,7 @@ class SupportTreeBuildsteps { // When bridging heads to pillars... TODO: find a cleaner solution ccr::BlockingMutex m_bridge_mutex; - inline EigenMesh3D::hit_result ray_mesh_intersect(const Vec3d& s, + inline IndexedMesh::hit_result ray_mesh_intersect(const Vec3d& s, const Vec3d& dir) { return m_mesh.query_ray_hit(s, dir); @@ -223,16 +238,24 @@ class SupportTreeBuildsteps { // point was inside the model, an "invalid" hit_result will be returned // with a zero distance value instead of a NAN. This way the result can // be used safely for comparison with other distances. - EigenMesh3D::hit_result pinhead_mesh_intersect( + IndexedMesh::hit_result pinhead_mesh_intersect( const Vec3d& s, const Vec3d& dir, double r_pin, double r_back, - double width); - - template - inline double pinhead_mesh_distance(Args&&...args) { - return pinhead_mesh_intersect(std::forward(args)...).distance(); + double width, + double safety_d); + + IndexedMesh::hit_result pinhead_mesh_intersect( + const Vec3d& s, + const Vec3d& dir, + double r_pin, + double r_back, + double width) + { + return pinhead_mesh_intersect(s, dir, r_pin, r_back, width, + r_back * m_cfg.safety_distance_mm / + m_cfg.head_back_radius_mm); } // Checking bridge (pillar and stick as well) intersection with the model. @@ -243,11 +266,21 @@ class SupportTreeBuildsteps { // point was inside the model, an "invalid" hit_result will be returned // with a zero distance value instead of a NAN. This way the result can // be used safely for comparison with other distances. - EigenMesh3D::hit_result bridge_mesh_intersect( + IndexedMesh::hit_result bridge_mesh_intersect( const Vec3d& s, const Vec3d& dir, double r, - bool ins_check = false); + double safety_d); + + IndexedMesh::hit_result bridge_mesh_intersect( + const Vec3d& s, + const Vec3d& dir, + double r) + { + return bridge_mesh_intersect(s, dir, r, + r * m_cfg.safety_distance_mm / + m_cfg.head_back_radius_mm); + } template inline double bridge_mesh_distance(Args&&...args) { @@ -268,20 +301,29 @@ class SupportTreeBuildsteps { inline bool connect_to_ground(Head& head); bool connect_to_model_body(Head &head); - - bool search_pillar_and_connect(const Head& head); + + bool search_pillar_and_connect(const Head& source); // This is a proxy function for pillar creation which will mind the gap // between the pad and the model bottom in zero elevation mode. // jp is the starting junction point which needs to be routed down. // sourcedir is the allowed direction of an optional bridge between the // jp junction and the final pillar. - void create_ground_pillar(const Vec3d &jp, + bool create_ground_pillar(const Vec3d &jp, const Vec3d &sourcedir, double radius, - long head_id = ID_UNSET); - - + long head_id = SupportTreeNode::ID_UNSET); + + void add_pillar_base(long pid) + { + m_builder.add_pillar_base(pid, m_cfg.base_height_mm, m_cfg.base_radius_mm); + } + + std::optional search_widening_path(const Vec3d &jp, + const Vec3d &dir, + double radius, + double new_radius); + public: SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm); @@ -324,11 +366,6 @@ public: void interconnect_pillars(); - // Step: process the support points where there is not enough space for a - // full pinhead. In this case we will use a rounded sphere as a touching - // point and use a thinner bridge (let's call it a stick). - void routing_headless (); - inline void merge_result() { m_builder.merged_mesh(); } static bool execute(SupportTreeBuilder & builder, const SupportableMesh &sm); diff --git a/src/libslic3r/SLA/SupportTreeMesher.cpp b/src/libslic3r/SLA/SupportTreeMesher.cpp new file mode 100644 index 0000000000..15491775b4 --- /dev/null +++ b/src/libslic3r/SLA/SupportTreeMesher.cpp @@ -0,0 +1,266 @@ +#include "SupportTreeMesher.hpp" + +namespace Slic3r { namespace sla { + +Contour3D sphere(double rho, Portion portion, double fa) { + + Contour3D ret; + + // prohibit close to zero radius + if(rho <= 1e-6 && rho >= -1e-6) return ret; + + auto& vertices = ret.points; + auto& facets = ret.faces3; + + // Algorithm: + // Add points one-by-one to the sphere grid and form facets using relative + // coordinates. Sphere is composed effectively of a mesh of stacked circles. + + // adjust via rounding to get an even multiple for any provided angle. + double angle = (2*PI / floor(2*PI / fa)); + + // Ring to be scaled to generate the steps of the sphere + std::vector ring; + + for (double i = 0; i < 2*PI; i+=angle) ring.emplace_back(i); + + const auto sbegin = size_t(2*std::get<0>(portion)/angle); + const auto send = size_t(2*std::get<1>(portion)/angle); + + const size_t steps = ring.size(); + const double increment = 1.0 / double(steps); + + // special case: first ring connects to 0,0,0 + // insert and form facets. + if(sbegin == 0) + vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*sbegin*2.0*rho)); + + auto id = coord_t(vertices.size()); + for (size_t i = 0; i < ring.size(); i++) { + // Fixed scaling + const double z = -rho + increment*rho*2.0 * (sbegin + 1.0); + // radius of the circle for this step. + const double r = std::sqrt(std::abs(rho*rho - z*z)); + Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r); + vertices.emplace_back(Vec3d(b(0), b(1), z)); + + if (sbegin == 0) + (i == 0) ? facets.emplace_back(coord_t(ring.size()), 0, 1) : + facets.emplace_back(id - 1, 0, id); + ++id; + } + + // General case: insert and form facets for each step, + // joining it to the ring below it. + for (size_t s = sbegin + 2; s < send - 1; s++) { + const double z = -rho + increment*double(s*2.0*rho); + const double r = std::sqrt(std::abs(rho*rho - z*z)); + + for (size_t i = 0; i < ring.size(); i++) { + Vec2d b = Eigen::Rotation2Dd(ring[i]) * Eigen::Vector2d(0, r); + vertices.emplace_back(Vec3d(b(0), b(1), z)); + auto id_ringsize = coord_t(id - int(ring.size())); + if (i == 0) { + // wrap around + facets.emplace_back(id - 1, id, id + coord_t(ring.size() - 1) ); + facets.emplace_back(id - 1, id_ringsize, id); + } else { + facets.emplace_back(id_ringsize - 1, id_ringsize, id); + facets.emplace_back(id - 1, id_ringsize - 1, id); + } + id++; + } + } + + // special case: last ring connects to 0,0,rho*2.0 + // only form facets. + if(send >= size_t(2*PI / angle)) { + vertices.emplace_back(Vec3d(0.0, 0.0, -rho + increment*send*2.0*rho)); + for (size_t i = 0; i < ring.size(); i++) { + auto id_ringsize = coord_t(id - int(ring.size())); + if (i == 0) { + // third vertex is on the other side of the ring. + facets.emplace_back(id - 1, id_ringsize, id); + } else { + auto ci = coord_t(id_ringsize + coord_t(i)); + facets.emplace_back(ci - 1, ci, id); + } + } + } + id++; + + return ret; +} + +Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp) +{ + assert(ssteps > 0); + + Contour3D ret; + + auto steps = int(ssteps); + auto& points = ret.points; + auto& indices = ret.faces3; + points.reserve(2*ssteps); + double a = 2*PI/steps; + + Vec3d jp = sp; + Vec3d endp = {sp(X), sp(Y), sp(Z) + h}; + + // Upper circle points + for(int i = 0; i < steps; ++i) { + double phi = i*a; + double ex = endp(X) + r*std::cos(phi); + double ey = endp(Y) + r*std::sin(phi); + points.emplace_back(ex, ey, endp(Z)); + } + + // Lower circle points + for(int i = 0; i < steps; ++i) { + double phi = i*a; + double x = jp(X) + r*std::cos(phi); + double y = jp(Y) + r*std::sin(phi); + points.emplace_back(x, y, jp(Z)); + } + + // Now create long triangles connecting upper and lower circles + indices.reserve(2*ssteps); + auto offs = steps; + for(int i = 0; i < steps - 1; ++i) { + indices.emplace_back(i, i + offs, offs + i + 1); + indices.emplace_back(i, offs + i + 1, i + 1); + } + + // Last triangle connecting the first and last vertices + auto last = steps - 1; + indices.emplace_back(0, last, offs); + indices.emplace_back(last, offs + last, offs); + + // According to the slicing algorithms, we need to aid them with generating + // a watertight body. So we create a triangle fan for the upper and lower + // ending of the cylinder to close the geometry. + points.emplace_back(jp); int ci = int(points.size() - 1); + for(int i = 0; i < steps - 1; ++i) + indices.emplace_back(i + offs + 1, i + offs, ci); + + indices.emplace_back(offs, steps + offs - 1, ci); + + points.emplace_back(endp); ci = int(points.size() - 1); + for(int i = 0; i < steps - 1; ++i) + indices.emplace_back(ci, i, i + 1); + + indices.emplace_back(steps - 1, 0, ci); + + return ret; +} + +Contour3D pinhead(double r_pin, double r_back, double length, size_t steps) +{ + assert(steps > 0); + assert(length >= 0.); + assert(r_back > 0.); + assert(r_pin > 0.); + + Contour3D mesh; + + // We create two spheres which will be connected with a robe that fits + // both circles perfectly. + + // Set up the model detail level + const double detail = 2 * PI / steps; + + // We don't generate whole circles. Instead, we generate only the + // portions which are visible (not covered by the robe) To know the + // exact portion of the bottom and top circles we need to use some + // rules of tangent circles from which we can derive (using simple + // triangles the following relations: + + // The height of the whole mesh + const double h = r_back + r_pin + length; + double phi = PI / 2. - std::acos((r_back - r_pin) / h); + + // To generate a whole circle we would pass a portion of (0, Pi) + // To generate only a half horizontal circle we can pass (0, Pi/2) + // The calculated phi is an offset to the half circles needed to smooth + // the transition from the circle to the robe geometry + + auto &&s1 = sphere(r_back, make_portion(0, PI / 2 + phi), detail); + auto &&s2 = sphere(r_pin, make_portion(PI / 2 + phi, PI), detail); + + for (auto &p : s2.points) p.z() += h; + + mesh.merge(s1); + mesh.merge(s2); + + for (size_t idx1 = s1.points.size() - steps, idx2 = s1.points.size(); + idx1 < s1.points.size() - 1; idx1++, idx2++) { + coord_t i1s1 = coord_t(idx1), i1s2 = coord_t(idx2); + coord_t i2s1 = i1s1 + 1, i2s2 = i1s2 + 1; + + mesh.faces3.emplace_back(i1s1, i2s1, i2s2); + mesh.faces3.emplace_back(i1s1, i2s2, i1s2); + } + + auto i1s1 = coord_t(s1.points.size()) - coord_t(steps); + auto i2s1 = coord_t(s1.points.size()) - 1; + auto i1s2 = coord_t(s1.points.size()); + auto i2s2 = coord_t(s1.points.size()) + coord_t(steps) - 1; + + mesh.faces3.emplace_back(i2s2, i2s1, i1s1); + mesh.faces3.emplace_back(i1s2, i2s2, i1s1); + + return mesh; +} + +Contour3D halfcone(double baseheight, + double r_bottom, + double r_top, + const Vec3d &pos, + size_t steps) +{ + assert(steps > 0); + + if (baseheight <= 0 || steps <= 0) return {}; + + Contour3D base; + + double a = 2 * PI / steps; + auto last = int(steps - 1); + Vec3d ep{pos.x(), pos.y(), pos.z() + baseheight}; + for (size_t i = 0; i < steps; ++i) { + double phi = i * a; + double x = pos.x() + r_top * std::cos(phi); + double y = pos.y() + r_top * std::sin(phi); + base.points.emplace_back(x, y, ep.z()); + } + + for (size_t i = 0; i < steps; ++i) { + double phi = i * a; + double x = pos.x() + r_bottom * std::cos(phi); + double y = pos.y() + r_bottom * std::sin(phi); + base.points.emplace_back(x, y, pos.z()); + } + + base.points.emplace_back(pos); + base.points.emplace_back(ep); + + auto &indices = base.faces3; + auto hcenter = int(base.points.size() - 1); + auto lcenter = int(base.points.size() - 2); + auto offs = int(steps); + for (int i = 0; i < last; ++i) { + indices.emplace_back(i, i + offs, offs + i + 1); + indices.emplace_back(i, offs + i + 1, i + 1); + indices.emplace_back(i, i + 1, hcenter); + indices.emplace_back(lcenter, offs + i + 1, offs + i); + } + + indices.emplace_back(0, last, offs); + indices.emplace_back(last, offs + last, offs); + indices.emplace_back(hcenter, last, 0); + indices.emplace_back(offs, offs + last, lcenter); + + return base; +} + +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportTreeMesher.hpp b/src/libslic3r/SLA/SupportTreeMesher.hpp new file mode 100644 index 0000000000..63182745da --- /dev/null +++ b/src/libslic3r/SLA/SupportTreeMesher.hpp @@ -0,0 +1,117 @@ +#ifndef SUPPORTTREEMESHER_HPP +#define SUPPORTTREEMESHER_HPP + +#include "libslic3r/Point.hpp" + +#include "libslic3r/SLA/SupportTreeBuilder.hpp" +#include "libslic3r/SLA/Contour3D.hpp" + +namespace Slic3r { namespace sla { + +using Portion = std::tuple; + +inline Portion make_portion(double a, double b) +{ + return std::make_tuple(a, b); +} + +Contour3D sphere(double rho, + Portion portion = make_portion(0., 2. * PI), + double fa = (2. * PI / 360.)); + +// Down facing cylinder in Z direction with arguments: +// r: radius +// h: Height +// ssteps: how many edges will create the base circle +// sp: starting point +Contour3D cylinder(double r, + double h, + size_t steps = 45, + const Vec3d &sp = Vec3d::Zero()); + +Contour3D pinhead(double r_pin, double r_back, double length, size_t steps = 45); + +Contour3D halfcone(double baseheight, + double r_bottom, + double r_top, + const Vec3d &pt = Vec3d::Zero(), + size_t steps = 45); + +inline Contour3D get_mesh(const Head &h, size_t steps) +{ + Contour3D mesh = pinhead(h.r_pin_mm, h.r_back_mm, h.width_mm, steps); + + for(auto& p : mesh.points) p.z() -= (h.fullwidth() - h.r_back_mm); + + using Quaternion = Eigen::Quaternion; + + // We rotate the head to the specified direction. The head's pointing + // side is facing upwards so this means that it would hold a support + // point with a normal pointing straight down. This is the reason of + // the -1 z coordinate + auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, -1}, h.dir); + + for(auto& p : mesh.points) p = quatern * p + h.pos; + + return mesh; +} + +inline Contour3D get_mesh(const Pillar &p, size_t steps) +{ + if(p.height > EPSILON) { // Endpoint is below the starting point + // We just create a bridge geometry with the pillar parameters and + // move the data. + return cylinder(p.r, p.height, steps, p.endpoint()); + } + + return {}; +} + +inline Contour3D get_mesh(const Pedestal &p, size_t steps) +{ + return halfcone(p.height, p.r_bottom, p.r_top, p.pos, steps); +} + +inline Contour3D get_mesh(const Junction &j, size_t steps) +{ + Contour3D mesh = sphere(j.r, make_portion(0, PI), 2 *PI / steps); + for(auto& p : mesh.points) p += j.pos; + return mesh; +} + +inline Contour3D get_mesh(const Bridge &br, size_t steps) +{ + using Quaternion = Eigen::Quaternion; + Vec3d v = (br.endp - br.startp); + Vec3d dir = v.normalized(); + double d = v.norm(); + + Contour3D mesh = cylinder(br.r, d, steps); + + auto quater = Quaternion::FromTwoVectors(Vec3d{0,0,1}, dir); + for(auto& p : mesh.points) p = quater * p + br.startp; + + return mesh; +} + +inline Contour3D get_mesh(const DiffBridge &br, size_t steps) +{ + double h = br.get_length(); + Contour3D mesh = halfcone(h, br.r, br.end_r, Vec3d::Zero(), steps); + + using Quaternion = Eigen::Quaternion; + + // We rotate the head to the specified direction. The head's pointing + // side is facing upwards so this means that it would hold a support + // point with a normal pointing straight down. This is the reason of + // the -1 z coordinate + auto quatern = Quaternion::FromTwoVectors(Vec3d{0, 0, 1}, br.get_dir()); + + for(auto& p : mesh.points) p = quatern * p + br.startp; + + return mesh; +} + +}} // namespace Slic3r::sla + +#endif // SUPPORTTREEMESHER_HPP diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index 2402207a8a..07ec380160 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -35,13 +35,16 @@ bool is_zero_elevation(const SLAPrintObjectConfig &c) } // Compile the argument for support creation from the static print config. -sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) +sla::SupportTreeConfig make_support_cfg(const SLAPrintObjectConfig& c) { - sla::SupportConfig scfg; + sla::SupportTreeConfig scfg; scfg.enabled = c.supports_enable.getBool(); scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); - scfg.head_back_radius_mm = 0.5*c.support_pillar_diameter.getFloat(); + double pillar_r = 0.5 * c.support_pillar_diameter.getFloat(); + scfg.head_back_radius_mm = pillar_r; + scfg.head_fallback_radius_mm = + 0.01 * c.support_small_pillar_diameter_percent.getFloat() * pillar_r; scfg.head_penetration_mm = c.support_head_penetration.getFloat(); scfg.head_width_mm = c.support_head_width.getFloat(); scfg.object_elevation_mm = is_zero_elevation(c) ? @@ -616,7 +619,7 @@ std::string SLAPrint::validate() const return L("Cannot proceed without support points! " "Add support points or disable support generation."); - sla::SupportConfig cfg = make_support_cfg(po->config()); + sla::SupportTreeConfig cfg = make_support_cfg(po->config()); double elv = cfg.object_elevation_mm; @@ -925,6 +928,7 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector m_layers; virtual uqptr create_raster() const = 0; - virtual sla::EncodedRaster encode_raster(const sla::RasterBase &rst) const = 0; + virtual sla::RasterEncoder get_encoder() const = 0; public: virtual ~SLAPrinter() = default; @@ -385,12 +385,13 @@ public: template void draw_layers(size_t layer_num, Fn &&drawfn) { m_layers.resize(layer_num); - sla::ccr::enumerate(m_layers.begin(), m_layers.end(), - [this, &drawfn](sla::EncodedRaster& enc, size_t idx) { - auto rst = create_raster(); - drawfn(*rst, idx); - enc = encode_raster(*rst); - }); + sla::ccr::for_each(size_t(0), m_layers.size(), + [this, &drawfn] (size_t idx) { + sla::EncodedRaster& enc = m_layers[idx]; + auto rst = create_raster(); + drawfn(*rst, idx); + enc = rst->encode(get_encoder()); + }); } }; @@ -433,7 +434,7 @@ public: // in the notification center. const SLAPrintObject* get_object(ObjectID object_id) const { auto it = std::find_if(m_objects.begin(), m_objects.end(), - [object_id](const SLAPrintObject *obj) { return *static_cast(obj) == object_id; }); + [object_id](const SLAPrintObject *obj) { return obj->id() == object_id; }); return (it == m_objects.end()) ? nullptr : *it; } @@ -544,7 +545,7 @@ private: bool is_zero_elevation(const SLAPrintObjectConfig &c); -sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c); +sla::SupportTreeConfig make_support_cfg(const SLAPrintObjectConfig& c); sla::PadConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c); diff --git a/src/libslic3r/SLAPrintSteps.cpp b/src/libslic3r/SLAPrintSteps.cpp index e421e9c1dc..d94bc682b3 100644 --- a/src/libslic3r/SLAPrintSteps.cpp +++ b/src/libslic3r/SLAPrintSteps.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -187,7 +188,7 @@ void SLAPrint::Steps::drill_holes(SLAPrintObject &po) } if (MeshBoolean::cgal::does_self_intersect(*holes_mesh_cgal)) - throw std::runtime_error(L("Too much overlapping holes.")); + throw Slic3r::SlicingError(L("Too many overlapping holes.")); auto hollowed_mesh_cgal = MeshBoolean::cgal::triangle_mesh_to_cgal(hollowed_mesh); @@ -195,7 +196,7 @@ void SLAPrint::Steps::drill_holes(SLAPrintObject &po) MeshBoolean::cgal::minus(*hollowed_mesh_cgal, *holes_mesh_cgal); hollowed_mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*hollowed_mesh_cgal); } catch (const std::runtime_error &) { - throw std::runtime_error(L( + throw Slic3r::SlicingError(L( "Drilling holes into the mesh failed. " "This is usually caused by broken model. Try to fix it first.")); } @@ -241,7 +242,7 @@ void SLAPrint::Steps::slice_model(SLAPrintObject &po) if(slindex_it == po.m_slice_index.end()) //TRN To be shown at the status bar on SLA slicing error. - throw std::runtime_error( + throw Slic3r::RuntimeError( L("Slicing had to be stopped due to an internal error: " "Inconsistent slice index.")); @@ -264,11 +265,12 @@ void SLAPrint::Steps::slice_model(SLAPrintObject &po) std::vector interior_slices; interior_slicer.slice(slice_grid, SlicingMode::Regular, closing_r, &interior_slices, thr); - sla::ccr::enumerate(interior_slices.begin(), interior_slices.end(), - [&po](const ExPolygons &slice, size_t i) { - po.m_model_slices[i] = - diff_ex(po.m_model_slices[i], slice); - }); + sla::ccr::for_each(size_t(0), interior_slices.size(), + [&po, &interior_slices] (size_t i) { + const ExPolygons &slice = interior_slices[i]; + po.m_model_slices[i] = + diff_ex(po.m_model_slices[i], slice); + }); } auto mit = slindex_it; @@ -360,18 +362,6 @@ void SLAPrint::Steps::support_points(SLAPrintObject &po) // removed them on purpose. No calculation will be done. po.m_supportdata->pts = po.transformed_support_points(); } - - // If the zero elevation mode is engaged, we have to filter out all the - // points that are on the bottom of the object - if (is_zero_elevation(po.config())) { - double tolerance = po.config().pad_enable.getBool() ? - po.m_config.pad_wall_thickness.getFloat() : - po.m_config.support_base_height.getFloat(); - - remove_bottom_points(po.m_supportdata->pts, - po.m_supportdata->emesh.ground_level(), - tolerance); - } } void SLAPrint::Steps::support_tree(SLAPrintObject &po) @@ -382,6 +372,13 @@ void SLAPrint::Steps::support_tree(SLAPrintObject &po) if (pcfg.embed_object) po.m_supportdata->emesh.ground_level_offset(pcfg.wall_thickness_mm); + + // If the zero elevation mode is engaged, we have to filter out all the + // points that are on the bottom of the object + if (is_zero_elevation(po.config())) { + remove_bottom_points(po.m_supportdata->pts, + float(po.m_supportdata->emesh.ground_level() + EPSILON)); + } po.m_supportdata->cfg = make_support_cfg(po.m_config); // po.m_supportdata->emesh.load_holes(po.transformed_drainhole_points()); @@ -449,7 +446,7 @@ void SLAPrint::Steps::generate_pad(SLAPrintObject &po) { auto &pad_mesh = po.m_supportdata->support_tree_ptr->retrieve_mesh(sla::MeshType::Pad); if (!validate_pad(pad_mesh, pcfg)) - throw std::runtime_error( + throw Slic3r::SlicingError( L("No pad can be generated for this model with the " "current configuration")); @@ -617,7 +614,7 @@ void SLAPrint::Steps::initialize_printer_input() for(const SliceRecord& slicerecord : o->get_slice_index()) { if (!slicerecord.is_valid()) - throw std::runtime_error( + throw Slic3r::SlicingError( L("There are unprintable objects. Try to " "adjust support settings to make the " "objects printable.")); @@ -684,14 +681,16 @@ void SLAPrint::Steps::merge_slices_and_eval_stats() { using Lock = std::lock_guard; // Going to parallel: - auto printlayerfn = [ + auto printlayerfn = [this, // functions and read only vars areafn, area_fill, display_area, exp_time, init_exp_time, fast_tilt, slow_tilt, delta_fade_time, // write vars &mutex, &models_volume, &supports_volume, &estim_time, &slow_layers, - &fast_layers, &fade_layer_time](PrintLayer& layer, size_t sliced_layer_cnt) + &fast_layers, &fade_layer_time](size_t sliced_layer_cnt) { + PrintLayer &layer = m_print->m_printer_input[sliced_layer_cnt]; + // vector of slice record references auto& slicerecord_references = layer.slices(); @@ -794,7 +793,7 @@ void SLAPrint::Steps::merge_slices_and_eval_stats() { // sequential version for debugging: // for(size_t i = 0; i < m_printer_input.size(); ++i) printlayerfn(i); - sla::ccr::enumerate(printer_input.begin(), printer_input.end(), printlayerfn); + sla::ccr::for_each(size_t(0), printer_input.size(), printlayerfn); auto SCALING2 = SCALING_FACTOR * SCALING_FACTOR; print_statistics.support_used_material = supports_volume * SCALING2; diff --git a/src/libslic3r/Semver.hpp b/src/libslic3r/Semver.hpp index 24ca74f837..f55fa9f9f4 100644 --- a/src/libslic3r/Semver.hpp +++ b/src/libslic3r/Semver.hpp @@ -10,6 +10,8 @@ #include "semver/semver.h" +#include "Exception.hpp" + namespace Slic3r { @@ -38,7 +40,7 @@ public: { auto parsed = parse(str); if (! parsed) { - throw std::runtime_error(std::string("Could not parse version string: ") + str); + throw Slic3r::RuntimeError(std::string("Could not parse version string: ") + str); } ver = parsed->ver; parsed->ver = semver_zero(); diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index 43f582d5f9..1669f60d21 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -409,7 +409,7 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) export_print_z_polygons_and_extrusions_to_svg( debug_out_path("support-w-fills-%d-%lf.svg", iRun, layers_sorted[i]->print_z).c_str(), layers_sorted.data() + i, j - i, - *object.support_layers[layer_id]); + *object.support_layers()[layer_id]); ++layer_id; } i = j; @@ -597,8 +597,8 @@ public: ::fwrite(&n_points, 4, 1, file); for (uint32_t j = 0; j < n_points; ++ j) { const Point &pt = poly.points[j]; - ::fwrite(&pt.x, sizeof(coord_t), 1, file); - ::fwrite(&pt.y, sizeof(coord_t), 1, file); + ::fwrite(&pt.x(), sizeof(coord_t), 1, file); + ::fwrite(&pt.y(), sizeof(coord_t), 1, file); } } n_polygons = m_trimming_polygons->size(); @@ -609,8 +609,8 @@ public: ::fwrite(&n_points, 4, 1, file); for (uint32_t j = 0; j < n_points; ++ j) { const Point &pt = poly.points[j]; - ::fwrite(&pt.x, sizeof(coord_t), 1, file); - ::fwrite(&pt.y, sizeof(coord_t), 1, file); + ::fwrite(&pt.x(), sizeof(coord_t), 1, file); + ::fwrite(&pt.y(), sizeof(coord_t), 1, file); } } ::fclose(file); @@ -972,8 +972,8 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::top_contact_ std::vector blockers = object.slice_support_blockers(); // Append custom supports. - object.project_and_append_custom_enforcers(enforcers); - object.project_and_append_custom_blockers(blockers); + object.project_and_append_custom_facets(false, EnforcerBlockerType::ENFORCER, enforcers); + object.project_and_append_custom_facets(false, EnforcerBlockerType::BLOCKER, blockers); // Output layers, sorted by top Z. MyLayersPtr contact_out; @@ -1134,7 +1134,7 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::top_contact_ { ::Slic3r::SVG svg(debug_out_path("support-top-contacts-raw-run%d-layer%d-region%d.svg", iRun, layer_id, - std::find_if(layer.regions.begin(), layer.regions.end(), [layerm](const LayerRegion* other){return other == layerm;}) - layer.regions.begin()), + std::find_if(layer.regions().begin(), layer.regions().end(), [layerm](const LayerRegion* other){return other == layerm;}) - layer.regions().begin()), get_extents(diff_polygons)); Slic3r::ExPolygons expolys = union_ex(diff_polygons, false); svg.draw(expolys); @@ -1152,7 +1152,7 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::top_contact_ Slic3r::SVG::export_expolygons( debug_out_path("support-top-contacts-filtered-run%d-layer%d-region%d-z%f.svg", iRun, layer_id, - std::find_if(layer.regions.begin(), layer.regions.end(), [layerm](const LayerRegion* other){return other == layerm;}) - layer.regions.begin(), + std::find_if(layer.regions().begin(), layer.regions().end(), [layerm](const LayerRegion* other){return other == layerm;}) - layer.regions().begin(), layer.print_z), union_ex(diff_polygons, false)); #endif /* SLIC3R_DEBUG */ @@ -1482,7 +1482,7 @@ PrintObjectSupportMaterial::MyLayersPtr PrintObjectSupportMaterial::bottom_conta svg.draw(union_ex(top, false), "blue", 0.5f); svg.draw(union_ex(projection_raw, true), "red", 0.5f); svg.draw_outline(union_ex(projection_raw, true), "red", "blue", scale_(0.1f)); - svg.draw(layer.slices, "green", 0.5f); + svg.draw(layer.lslices, "green", 0.5f); } #endif /* SLIC3R_DEBUG */ diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index c6991c057b..a0484b259c 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -15,7 +15,7 @@ #define ENABLE_RENDER_STATISTICS 0 // Shows an imgui dialog with camera related data #define ENABLE_CAMERA_STATISTICS 0 -// Render the picking pass instead of the main scene (use [T] key to toggle between regular rendering and picking pass only rendering) +// Render the picking pass instead of the main scene (use [T] key to toggle between regular rendering and picking pass only rendering) #define ENABLE_RENDER_PICKING_PASS 0 // Enable extracting thumbnails from selected gcode and save them as png files #define ENABLE_THUMBNAIL_GENERATOR_DEBUG 0 @@ -54,8 +54,10 @@ // Enable built-in DPI changed event handler of wxWidgets 3.1.3 #define ENABLE_WX_3_1_3_DPI_CHANGED_EVENT (1 && ENABLE_2_3_0_ALPHA1) -// Enable changing application layout without the need to restart -#define ENABLE_LAYOUT_NO_RESTART (1 && ENABLE_2_3_0_ALPHA1) - +// Enable G-Code viewer +#define ENABLE_GCODE_VIEWER (1 && ENABLE_2_3_0_ALPHA1) +#define ENABLE_GCODE_VIEWER_STATISTICS (0 && ENABLE_GCODE_VIEWER) +#define ENABLE_GCODE_VIEWER_DATA_CHECKING (0 && ENABLE_GCODE_VIEWER) +#define ENABLE_GCODE_VIEWER_TASKBAR_ICON (0 && ENABLE_GCODE_VIEWER) #endif // _prusaslicer_technologies_h_ diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index 17edf1b5a8..8ba34e5160 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -1,3 +1,4 @@ +#include "Exception.hpp" #include "TriangleMesh.hpp" #include "ClipperUtils.hpp" #include "Geometry.hpp" @@ -70,7 +71,7 @@ TriangleMesh::TriangleMesh(const Pointf3s &points, const std::vector &fac stl_get_size(&stl); } -TriangleMesh::TriangleMesh(const indexed_triangle_set &M) +TriangleMesh::TriangleMesh(const indexed_triangle_set &M) : repaired(false) { stl.stats.type = inmemory; @@ -420,7 +421,7 @@ std::deque TriangleMesh::find_unvisited_neighbors(std::vectorrepaired) - throw std::runtime_error("find_unvisited_neighbors() requires repair()"); + throw Slic3r::RuntimeError("find_unvisited_neighbors() requires repair()"); // If the visited list is empty, populate it with false for every facet. if (facet_visited.empty()) @@ -683,7 +684,7 @@ void TriangleMeshSlicer::init(const TriangleMesh *_mesh, throw_on_cancel_callbac { mesh = _mesh; if (! mesh->has_shared_vertices()) - throw std::invalid_argument("TriangleMeshSlicer was passed a mesh without shared vertices."); + throw Slic3r::InvalidArgument("TriangleMeshSlicer was passed a mesh without shared vertices."); throw_on_cancel(); facets_edges.assign(_mesh->stl.stats.number_of_facets * 3, -1); diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp new file mode 100644 index 0000000000..9f04374fdc --- /dev/null +++ b/src/libslic3r/TriangleSelector.cpp @@ -0,0 +1,690 @@ +#include "TriangleSelector.hpp" +#include "Model.hpp" + + +namespace Slic3r { + + + +// sides_to_split==-1 : just restore previous split +void TriangleSelector::Triangle::set_division(int sides_to_split, int special_side_idx) +{ + assert(sides_to_split >=-1 && sides_to_split <= 3); + assert(special_side_idx >=-1 && special_side_idx < 3); + + // If splitting one or two sides, second argument must be provided. + assert(sides_to_split != 1 || special_side_idx != -1); + assert(sides_to_split != 2 || special_side_idx != -1); + + if (sides_to_split != -1) { + this->number_of_splits = sides_to_split; + if (sides_to_split != 0) { + assert(old_number_of_splits == 0); + this->special_side_idx = special_side_idx; + this->old_number_of_splits = sides_to_split; + } + } + else { + assert(old_number_of_splits != 0); + this->number_of_splits = old_number_of_splits; + // indices of children should still be there. + } +} + + + +void TriangleSelector::select_patch(const Vec3f& hit, int facet_start, + const Vec3f& source, const Vec3f& dir, + float radius, EnforcerBlockerType new_state) +{ + assert(facet_start < m_orig_size_indices); + assert(is_approx(dir.norm(), 1.f)); + + // Save current cursor center, squared radius and camera direction, + // so we don't have to pass it around. + m_cursor = {hit, source, dir, radius*radius}; + + // In case user changed cursor size since last time, update triangle edge limit. + if (m_old_cursor_radius != radius) { + set_edge_limit(radius / 5.f); + m_old_cursor_radius = radius; + } + + // Now start with the facet the pointer points to and check all adjacent facets. + std::vector facets_to_check{facet_start}; + std::vector visited(m_orig_size_indices, false); // keep track of facets we already processed + int facet_idx = 0; // index into facets_to_check + while (facet_idx < int(facets_to_check.size())) { + int facet = facets_to_check[facet_idx]; + if (! visited[facet]) { + if (select_triangle(facet, new_state)) { + // add neighboring facets to list to be proccessed later + for (int n=0; n<3; ++n) { + int neighbor_idx = m_mesh->stl.neighbors_start[facet].neighbor[n]; + if (neighbor_idx >=0 && faces_camera(neighbor_idx)) + facets_to_check.push_back(neighbor_idx); + } + } + } + visited[facet] = true; + ++facet_idx; + } +} + + + +// Selects either the whole triangle (discarding any children it had), or divides +// the triangle recursively, selecting just subtriangles truly inside the circle. +// This is done by an actual recursive call. Returns false if the triangle is +// outside the cursor. +bool TriangleSelector::select_triangle(int facet_idx, EnforcerBlockerType type, bool recursive_call) +{ + assert(facet_idx < int(m_triangles.size())); + + Triangle* tr = &m_triangles[facet_idx]; + if (! tr->valid) + return false; + + int num_of_inside_vertices = vertices_inside(facet_idx); + + if (num_of_inside_vertices == 0 + && ! is_pointer_in_triangle(facet_idx) + && ! is_edge_inside_cursor(facet_idx)) + return false; + + if (num_of_inside_vertices == 3) { + // dump any subdivision and select whole triangle + undivide_triangle(facet_idx); + tr->set_state(type); + } else { + // the triangle is partially inside, let's recursively divide it + // (if not already) and try selecting its children. + + if (! tr->is_split() && tr->get_state() == type) { + // This is leaf triangle that is already of correct type as a whole. + // No need to split, all children would end up selected anyway. + return true; + } + + split_triangle(facet_idx); + tr = &m_triangles[facet_idx]; // might have been invalidated + + + int num_of_children = tr->number_of_split_sides() + 1; + if (num_of_children != 1) { + for (int i=0; ichildren.size())); + assert(tr->children[i] < int(m_triangles.size())); + + select_triangle(tr->children[i], type, true); + tr = &m_triangles[facet_idx]; // might have been invalidated + } + } + } + + if (! recursive_call) { + // In case that all children are leafs and have the same state now, + // they may be removed and substituted by the parent triangle. + remove_useless_children(facet_idx); + + // Make sure that we did not lose track of invalid triangles. + assert(m_invalid_triangles == std::count_if(m_triangles.begin(), m_triangles.end(), + [](const Triangle& tr) { return ! tr.valid; })); + + // Do garbage collection maybe? + if (2*m_invalid_triangles > int(m_triangles.size())) + garbage_collect(); + } + return true; +} + + + +void TriangleSelector::set_facet(int facet_idx, EnforcerBlockerType state) +{ + assert(facet_idx < m_orig_size_indices); + undivide_triangle(facet_idx); + assert(! m_triangles[facet_idx].is_split()); + m_triangles[facet_idx].set_state(state); +} + +void TriangleSelector::split_triangle(int facet_idx) +{ + if (m_triangles[facet_idx].is_split()) { + // The triangle is divided already. + return; + } + + Triangle* tr = &m_triangles[facet_idx]; + + EnforcerBlockerType old_type = tr->get_state(); + + if (tr->was_split_before() != 0) { + // This triangle is not split at the moment, but was at one point + // in history. We can just restore it and resurrect its children. + tr->set_division(-1); + for (int i=0; i<=tr->number_of_split_sides(); ++i) { + m_triangles[tr->children[i]].set_state(old_type); + m_triangles[tr->children[i]].valid = true; + --m_invalid_triangles; + } + return; + } + + // If we got here, we are about to actually split the triangle. + const double limit_squared = m_edge_limit_sqr; + + std::array& facet = tr->verts_idxs; + const stl_vertex* pts[3] = { &m_vertices[facet[0]].v, &m_vertices[facet[1]].v, &m_vertices[facet[2]].v}; + double sides[3] = { (*pts[2]-*pts[1]).squaredNorm(), + (*pts[0]-*pts[2]).squaredNorm(), + (*pts[1]-*pts[0]).squaredNorm() }; + + std::vector sides_to_split; + int side_to_keep = -1; + for (int pt_idx = 0; pt_idx<3; ++pt_idx) { + if (sides[pt_idx] > limit_squared) + sides_to_split.push_back(pt_idx); + else + side_to_keep = pt_idx; + } + if (sides_to_split.empty()) { + // This shall be unselected. + tr->set_division(0); + return; + } + + // Save how the triangle will be split. Second argument makes sense only for one + // or two split sides, otherwise the value is ignored. + tr->set_division(sides_to_split.size(), + sides_to_split.size() == 2 ? side_to_keep : sides_to_split[0]); + + perform_split(facet_idx, old_type); +} + + +// Calculate distance of a point from a line. +bool TriangleSelector::is_point_inside_cursor(const Vec3f& point) const +{ + Vec3f diff = m_cursor.center - point; + return (diff - diff.dot(m_cursor.dir) * m_cursor.dir).squaredNorm() < m_cursor.radius_sqr; +} + + +// Is pointer in a triangle? +bool TriangleSelector::is_pointer_in_triangle(int facet_idx) const +{ + auto signed_volume_sign = [](const Vec3f& a, const Vec3f& b, + const Vec3f& c, const Vec3f& d) -> bool { + return ((b-a).cross(c-a)).dot(d-a) > 0.; + }; + + const Vec3f& p1 = m_vertices[m_triangles[facet_idx].verts_idxs[0]].v; + const Vec3f& p2 = m_vertices[m_triangles[facet_idx].verts_idxs[1]].v; + const Vec3f& p3 = m_vertices[m_triangles[facet_idx].verts_idxs[2]].v; + const Vec3f& q1 = m_cursor.center + m_cursor.dir; + const Vec3f q2 = m_cursor.center - m_cursor.dir; + + if (signed_volume_sign(q1,p1,p2,p3) != signed_volume_sign(q2,p1,p2,p3)) { + bool pos = signed_volume_sign(q1,q2,p1,p2); + if (signed_volume_sign(q1,q2,p2,p3) == pos && signed_volume_sign(q1,q2,p3,p1) == pos) + return true; + } + return false; +} + + + +// Determine whether this facet is potentially visible (still can be obscured). +bool TriangleSelector::faces_camera(int facet) const +{ + assert(facet < m_orig_size_indices); + // The normal is cached in mesh->stl, use it. + return (m_mesh->stl.facet_start[facet].normal.dot(m_cursor.dir) < 0.); +} + + +// How many vertices of a triangle are inside the circle? +int TriangleSelector::vertices_inside(int facet_idx) const +{ + int inside = 0; + for (size_t i=0; i<3; ++i) { + if (is_point_inside_cursor(m_vertices[m_triangles[facet_idx].verts_idxs[i]].v)) + ++inside; + } + return inside; +} + + +// Is edge inside cursor? +bool TriangleSelector::is_edge_inside_cursor(int facet_idx) const +{ + Vec3f pts[3]; + for (int i=0; i<3; ++i) + pts[i] = m_vertices[m_triangles[facet_idx].verts_idxs[i]].v; + + const Vec3f& p = m_cursor.center; + + for (int side = 0; side < 3; ++side) { + const Vec3f& a = pts[side]; + const Vec3f& b = pts[side<2 ? side+1 : 0]; + Vec3f s = (b-a).normalized(); + float t = (p-a).dot(s); + Vec3f vector = a+t*s - p; + + // vector is 3D vector from center to the intersection. What we want to + // measure is length of its projection onto plane perpendicular to dir. + float dist_sqr = vector.squaredNorm() - std::pow(vector.dot(m_cursor.dir), 2.f); + if (dist_sqr < m_cursor.radius_sqr && t>=0.f && t<=(b-a).norm()) + return true; + } + return false; +} + + + +// Recursively remove all subtriangles. +void TriangleSelector::undivide_triangle(int facet_idx) +{ + assert(facet_idx < int(m_triangles.size())); + Triangle& tr = m_triangles[facet_idx]; + + if (tr.is_split()) { + for (int i=0; i<=tr.number_of_split_sides(); ++i) { + undivide_triangle(tr.children[i]); + m_triangles[tr.children[i]].valid = false; + ++m_invalid_triangles; + } + tr.set_division(0); // not split + } +} + + +void TriangleSelector::remove_useless_children(int facet_idx) +{ + // Check that all children are leafs of the same type. If not, try to + // make them (recursive call). Remove them if sucessful. + + assert(facet_idx < int(m_triangles.size()) && m_triangles[facet_idx].valid); + Triangle& tr = m_triangles[facet_idx]; + + if (! tr.is_split()) { + // This is a leaf, there nothing to do. This can happen during the + // first (non-recursive call). Shouldn't otherwise. + return; + } + + // Call this for all non-leaf children. + for (int child_idx=0; child_idx<=tr.number_of_split_sides(); ++child_idx) { + assert(child_idx < int(m_triangles.size()) && m_triangles[child_idx].valid); + if (m_triangles[tr.children[child_idx]].is_split()) + remove_useless_children(tr.children[child_idx]); + } + + + // Return if a child is not leaf or two children differ in type. + EnforcerBlockerType first_child_type = EnforcerBlockerType::NONE; + for (int child_idx=0; child_idx<=tr.number_of_split_sides(); ++child_idx) { + if (m_triangles[tr.children[child_idx]].is_split()) + return; + if (child_idx == 0) + first_child_type = m_triangles[tr.children[0]].get_state(); + else if (m_triangles[tr.children[child_idx]].get_state() != first_child_type) + return; + } + + // If we got here, the children can be removed. + undivide_triangle(facet_idx); + tr.set_state(first_child_type); +} + + + +void TriangleSelector::garbage_collect() +{ + // First make a map from old to new triangle indices. + int new_idx = m_orig_size_indices; + std::vector new_triangle_indices(m_triangles.size(), -1); + for (int i = m_orig_size_indices; i new_vertices_indices(m_vertices.size(), -1); + for (int i=m_orig_size_vertices; i= 0); + if (m_vertices[i].ref_cnt != 0) { + new_vertices_indices[i] = new_idx; + ++new_idx; + } + } + + // We can remove all invalid triangles and vertices that are no longer referenced. + m_triangles.erase(std::remove_if(m_triangles.begin()+m_orig_size_indices, m_triangles.end(), + [](const Triangle& tr) { return ! tr.valid; }), + m_triangles.end()); + m_vertices.erase(std::remove_if(m_vertices.begin()+m_orig_size_vertices, m_vertices.end(), + [](const Vertex& vert) { return vert.ref_cnt == 0; }), + m_vertices.end()); + + // Now go through all remaining triangles and update changed indices. + for (Triangle& tr : m_triangles) { + assert(tr.valid); + + if (tr.is_split()) { + // There are children. Update their indices. + for (int j=0; j<=tr.number_of_split_sides(); ++j) { + assert(new_triangle_indices[tr.children[j]] != -1); + tr.children[j] = new_triangle_indices[tr.children[j]]; + } + } + + // Update indices into m_vertices. The original vertices are never + // touched and need not be reindexed. + for (int& idx : tr.verts_idxs) { + if (idx >= m_orig_size_vertices) { + assert(new_vertices_indices[idx] != -1); + idx = new_vertices_indices[idx]; + } + } + + // If this triangle was split before, forget it. + // Children referenced in the cache are dead by now. + tr.forget_history(); + } + + m_invalid_triangles = 0; +} + +TriangleSelector::TriangleSelector(const TriangleMesh& mesh) + : m_mesh{&mesh} +{ + reset(); +} + + +void TriangleSelector::reset() +{ + if (! m_orig_size_indices != 0) // unless this is run from constructor + garbage_collect(); + m_vertices.clear(); + m_triangles.clear(); + for (const stl_vertex& vert : m_mesh->its.vertices) + m_vertices.emplace_back(vert); + for (const stl_triangle_vertex_indices& ind : m_mesh->its.indices) + push_triangle(ind[0], ind[1], ind[2]); + m_orig_size_vertices = m_vertices.size(); + m_orig_size_indices = m_triangles.size(); + m_invalid_triangles = 0; +} + + + + + +void TriangleSelector::set_edge_limit(float edge_limit) +{ + float new_limit_sqr = std::pow(edge_limit, 2.f); + + if (new_limit_sqr != m_edge_limit_sqr) { + m_edge_limit_sqr = new_limit_sqr; + + // The way how triangles split may be different now, forget + // all cached splits. + garbage_collect(); + } +} + + + +void TriangleSelector::push_triangle(int a, int b, int c) +{ + for (int i : {a, b, c}) { + assert(i >= 0 && i < int(m_vertices.size())); + ++m_vertices[i].ref_cnt; + } + m_triangles.emplace_back(a, b, c); +} + + +void TriangleSelector::perform_split(int facet_idx, EnforcerBlockerType old_state) +{ + Triangle* tr = &m_triangles[facet_idx]; + + assert(tr->is_split()); + + // Read info about how to split this triangle. + int sides_to_split = tr->number_of_split_sides(); + + // indices of triangle vertices + std::vector verts_idxs; + int idx = tr->special_side(); + for (int j=0; j<3; ++j) { + verts_idxs.push_back(tr->verts_idxs[idx++]); + if (idx == 3) + idx = 0; + } + + if (sides_to_split == 1) { + m_vertices.emplace_back((m_vertices[verts_idxs[1]].v + m_vertices[verts_idxs[2]].v)/2.); + verts_idxs.insert(verts_idxs.begin()+2, m_vertices.size() - 1); + + push_triangle(verts_idxs[0], verts_idxs[1], verts_idxs[2]); + push_triangle(verts_idxs[2], verts_idxs[3], verts_idxs[0]); + } + + if (sides_to_split == 2) { + m_vertices.emplace_back((m_vertices[verts_idxs[0]].v + m_vertices[verts_idxs[1]].v)/2.); + verts_idxs.insert(verts_idxs.begin()+1, m_vertices.size() - 1); + + m_vertices.emplace_back((m_vertices[verts_idxs[0]].v + m_vertices[verts_idxs[3]].v)/2.); + verts_idxs.insert(verts_idxs.begin()+4, m_vertices.size() - 1); + + push_triangle(verts_idxs[0], verts_idxs[1], verts_idxs[4]); + push_triangle(verts_idxs[1], verts_idxs[2], verts_idxs[4]); + push_triangle(verts_idxs[2], verts_idxs[3], verts_idxs[4]); + } + + if (sides_to_split == 3) { + m_vertices.emplace_back((m_vertices[verts_idxs[0]].v + m_vertices[verts_idxs[1]].v)/2.); + verts_idxs.insert(verts_idxs.begin()+1, m_vertices.size() - 1); + m_vertices.emplace_back((m_vertices[verts_idxs[2]].v + m_vertices[verts_idxs[3]].v)/2.); + verts_idxs.insert(verts_idxs.begin()+3, m_vertices.size() - 1); + m_vertices.emplace_back((m_vertices[verts_idxs[4]].v + m_vertices[verts_idxs[0]].v)/2.); + verts_idxs.insert(verts_idxs.begin()+5, m_vertices.size() - 1); + + push_triangle(verts_idxs[0], verts_idxs[1], verts_idxs[5]); + push_triangle(verts_idxs[1], verts_idxs[2], verts_idxs[3]); + push_triangle(verts_idxs[3], verts_idxs[4], verts_idxs[5]); + push_triangle(verts_idxs[1], verts_idxs[3], verts_idxs[5]); + } + + tr = &m_triangles[facet_idx]; // may have been invalidated + + // And save the children. All children should start in the same state as the triangle we just split. + assert(sides_to_split <= 3); + for (int i=0; i<=sides_to_split; ++i) { + tr->children[i] = m_triangles.size()-1-i; + m_triangles[tr->children[i]].set_state(old_state); + } +} + + + +indexed_triangle_set TriangleSelector::get_facets(EnforcerBlockerType state) const +{ + indexed_triangle_set out; + for (const Triangle& tr : m_triangles) { + if (tr.valid && ! tr.is_split() && tr.get_state() == state) { + stl_triangle_vertex_indices indices; + for (int i=0; i<3; ++i) { + out.vertices.emplace_back(m_vertices[tr.verts_idxs[i]].v); + indices[i] = out.vertices.size() - 1; + } + out.indices.emplace_back(indices); + } + } + return out; +} + + + +std::map> TriangleSelector::serialize() const +{ + // Each original triangle of the mesh is assigned a number encoding its state + // or how it is split. Each triangle is encoded by 4 bits (xxyy): + // leaf triangle: xx = EnforcerBlockerType, yy = 0 + // non-leaf: xx = special side, yy = number of split sides + // These are bitwise appended and formed into one 64-bit integer. + + // The function returns a map from original triangle indices to + // stream of bits encoding state and offsprings. + + std::map> out; + for (int i=0; i data; // complete encoding of this mesh triangle + int stored_triangles = 0; // how many have been already encoded + + std::function serialize_recursive; + serialize_recursive = [this, &serialize_recursive, &stored_triangles, &data](int facet_idx) { + const Triangle& tr = m_triangles[facet_idx]; + + // Always save number of split sides. It is zero for unsplit triangles. + int split_sides = tr.number_of_split_sides(); + assert(split_sides >= 0 && split_sides <= 3); + + //data |= (split_sides << (stored_triangles * 4)); + data.push_back(split_sides & 0b01); + data.push_back(split_sides & 0b10); + + if (tr.is_split()) { + // If this triangle is split, save which side is split (in case + // of one split) or kept (in case of two splits). The value will + // be ignored for 3-side split. + assert(split_sides > 0); + assert(tr.special_side() >= 0 && tr.special_side() <= 3); + data.push_back(tr.special_side() & 0b01); + data.push_back(tr.special_side() & 0b10); + ++stored_triangles; + // Now save all children. + for (int child_idx=0; child_idx<=split_sides; ++child_idx) + serialize_recursive(tr.children[child_idx]); + } else { + // In case this is leaf, we better save information about its state. + assert(int(tr.get_state()) <= 3); + data.push_back(int(tr.get_state()) & 0b01); + data.push_back(int(tr.get_state()) & 0b10); + ++stored_triangles; + } + }; + + serialize_recursive(i); + out[i] = data; + } + + return out; +} + +void TriangleSelector::deserialize(const std::map> data) +{ + reset(); // dump any current state + for (const auto& [triangle_id, code] : data) { + assert(triangle_id < int(m_triangles.size())); + assert(! code.empty()); + int processed_triangles = 0; + struct ProcessingInfo { + int facet_id = 0; + int processed_children = 0; + int total_children = 0; + }; + + // Vector to store all parents that have offsprings. + std::vector parents; + + while (true) { + // Read next triangle info. + int next_code = 0; + for (int i=3; i>=0; --i) { + next_code = next_code << 1; + next_code |= int(code[4 * processed_triangles + i]); + } + ++processed_triangles; + + int num_of_split_sides = (next_code & 0b11); + int num_of_children = num_of_split_sides != 0 ? num_of_split_sides + 1 : 0; + bool is_split = num_of_children != 0; + EnforcerBlockerType state = EnforcerBlockerType(next_code >> 2); + int special_side = (next_code >> 2); + + // Take care of the first iteration separately, so handling of the others is simpler. + if (parents.empty()) { + if (! is_split) { + // root is not split. just set the state and that's it. + m_triangles[triangle_id].set_state(state); + break; + } else { + // root is split, add it into list of parents and split it. + // then go to the next. + parents.push_back({triangle_id, 0, num_of_children}); + m_triangles[triangle_id].set_division(num_of_children-1, special_side); + perform_split(triangle_id, EnforcerBlockerType::NONE); + continue; + } + } + + // This is not the first iteration. This triangle is a child of last seen parent. + assert(! parents.empty()); + assert(parents.back().processed_children < parents.back().total_children); + + if (is_split) { + // split the triangle and save it as parent of the next ones. + const ProcessingInfo& last = parents.back(); + int this_idx = m_triangles[last.facet_id].children[last.processed_children]; + m_triangles[this_idx].set_division(num_of_children-1, special_side); + perform_split(this_idx, EnforcerBlockerType::NONE); + parents.push_back({this_idx, 0, num_of_children}); + } else { + // this triangle belongs to last split one + m_triangles[m_triangles[parents.back().facet_id].children[parents.back().processed_children]].set_state(state); + ++parents.back().processed_children; + } + + + // If all children of the past parent triangle are claimed, move to grandparent. + while (parents.back().processed_children == parents.back().total_children) { + parents.pop_back(); + + if (parents.empty()) + break; + + // And increment the grandparent children counter, because + // we have just finished that branch and got back here. + ++parents.back().processed_children; + } + + // In case we popped back the root, we should be done. + if (parents.empty()) + break; + } + + } +} + + + + +} // namespace Slic3r diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp new file mode 100644 index 0000000000..be1b20ed40 --- /dev/null +++ b/src/libslic3r/TriangleSelector.hpp @@ -0,0 +1,155 @@ +#ifndef libslic3r_TriangleSelector_hpp_ +#define libslic3r_TriangleSelector_hpp_ + +// #define PRUSASLICER_TRIANGLE_SELECTOR_DEBUG + + +#include "Point.hpp" +#include "TriangleMesh.hpp" + +namespace Slic3r { + +enum class EnforcerBlockerType : int8_t; + + + +// Following class holds information about selected triangles. It also has power +// to recursively subdivide the triangles and make the selection finer. +class TriangleSelector { +public: + void set_edge_limit(float edge_limit); + + // Create new object on a TriangleMesh. The referenced mesh must + // stay valid, a ptr to it is saved and used. + explicit TriangleSelector(const TriangleMesh& mesh); + + // Select all triangles fully inside the circle, subdivide where needed. + void select_patch(const Vec3f& hit, // point where to start + int facet_start, // facet that point belongs to + const Vec3f& source, // camera position (mesh coords) + const Vec3f& dir, // direction of the ray (mesh coords) + float radius, // radius of the cursor + EnforcerBlockerType new_state); // enforcer or blocker? + + // Get facets currently in the given state. + indexed_triangle_set get_facets(EnforcerBlockerType state) const; + + // Set facet of the mesh to a given state. Only works for original triangles. + void set_facet(int facet_idx, EnforcerBlockerType state); + + // Clear everything and make the tree empty. + void reset(); + + // Remove all unnecessary data. + void garbage_collect(); + + // Store the division trees in compact form (a long stream of + // bits for each triangle of the original mesh). + std::map> serialize() const; + + // Load serialized data. Assumes that correct mesh is loaded. + void deserialize(const std::map> data); + + +protected: + // Triangle and info about how it's split. + class Triangle { + public: + // Use TriangleSelector::push_triangle to create a new triangle. + // It increments/decrements reference counter on vertices. + Triangle(int a, int b, int c) + : verts_idxs{a, b, c}, + state{EnforcerBlockerType(0)}, + number_of_splits{0}, + special_side_idx{0}, + old_number_of_splits{0} + {} + // Indices into m_vertices. + std::array verts_idxs; + + // Is this triangle valid or marked to be removed? + bool valid{true}; + + // Children triangles. + std::array children; + + // Set the division type. + void set_division(int sides_to_split, int special_side_idx = -1); + + // Get/set current state. + void set_state(EnforcerBlockerType type) { assert(! is_split()); state = type; } + EnforcerBlockerType get_state() const { assert(! is_split()); return state; } + + // Get info on how it's split. + bool is_split() const { return number_of_split_sides() != 0; } + int number_of_split_sides() const { return number_of_splits; } + int special_side() const { assert(is_split()); return special_side_idx; } + bool was_split_before() const { return old_number_of_splits != 0; } + void forget_history() { old_number_of_splits = 0; } + + private: + int number_of_splits; + int special_side_idx; + EnforcerBlockerType state; + + // How many children were spawned during last split? + // Is not reset on remerging the triangle. + int old_number_of_splits; + }; + + struct Vertex { + explicit Vertex(const stl_vertex& vert) + : v{vert}, + ref_cnt{0} + {} + stl_vertex v; + int ref_cnt; + }; + + // Lists of vertices and triangles, both original and new + std::vector m_vertices; + std::vector m_triangles; + const TriangleMesh* m_mesh; + + // Number of invalid triangles (to trigger garbage collection). + int m_invalid_triangles; + + // Limiting length of triangle side (squared). + float m_edge_limit_sqr = 1.f; + + // Number of original vertices and triangles. + int m_orig_size_vertices = 0; + int m_orig_size_indices = 0; + + // Cache for cursor position, radius and direction. + struct Cursor { + Vec3f center; + Vec3f source; + Vec3f dir; + float radius_sqr; + }; + + Cursor m_cursor; + float m_old_cursor_radius; + + // Private functions: + bool select_triangle(int facet_idx, EnforcerBlockerType type, + bool recursive_call = false); + bool is_point_inside_cursor(const Vec3f& point) const; + int vertices_inside(int facet_idx) const; + bool faces_camera(int facet) const; + void undivide_triangle(int facet_idx); + void split_triangle(int facet_idx); + void remove_useless_children(int facet_idx); // No hidden meaning. Triangles are meant. + bool is_pointer_in_triangle(int facet_idx) const; + bool is_edge_inside_cursor(int facet_idx) const; + void push_triangle(int a, int b, int c); + void perform_split(int facet_idx, EnforcerBlockerType old_state); +}; + + + + +} // namespace Slic3r + +#endif // libslic3r_TriangleSelector_hpp_ diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 5cdf75037c..99b105a6b1 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -110,19 +110,20 @@ std::string header_slic3r_generated(); // getpid platform wrapper extern unsigned get_current_pid(); +#if !ENABLE_GCODE_VIEWER template Real round_nearest(Real value, unsigned int decimals) { Real res = (Real)0; if (decimals == 0) res = ::round(value); - else - { + else { Real power = ::pow((Real)10, (int)decimals); res = ::round(value * power + (Real)0.5) / power; } return res; } +#endif // !ENABLE_GCODE_VIEWER // Compute the next highest power of 2 of 32-bit v // http://graphics.stanford.edu/~seander/bithacks.html @@ -337,6 +338,25 @@ inline std::string get_time_dhms(float time_in_secs) return buffer; } +inline std::string get_time_dhm(float time_in_secs) +{ + int days = (int)(time_in_secs / 86400.0f); + time_in_secs -= (float)days * 86400.0f; + int hours = (int)(time_in_secs / 3600.0f); + time_in_secs -= (float)hours * 3600.0f; + int minutes = (int)(time_in_secs / 60.0f); + + char buffer[64]; + if (days > 0) + ::sprintf(buffer, "%dd %dh %dm", days, hours, minutes); + else if (hours > 0) + ::sprintf(buffer, "%dh %dm", hours, minutes); + else if (minutes > 0) + ::sprintf(buffer, "%dm", minutes); + + return buffer; +} + } // namespace Slic3r #if WIN32 diff --git a/src/libslic3r/Zipper.cpp b/src/libslic3r/Zipper.cpp index 02f022083b..7a95829cd0 100644 --- a/src/libslic3r/Zipper.cpp +++ b/src/libslic3r/Zipper.cpp @@ -1,5 +1,6 @@ #include +#include "Exception.hpp" #include "Zipper.hpp" #include "miniz_extension.hpp" #include @@ -29,7 +30,7 @@ public: SLIC3R_NORETURN void blow_up() const { - throw std::runtime_error(formatted_errorstr()); + throw Slic3r::RuntimeError(formatted_errorstr()); } bool is_alive() diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index e3816b87f9..76ff271360 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -5,6 +5,7 @@ // this needs to be included early for MSVC (listing it in Build.PL is not enough) #include +#include #include #include #include @@ -260,6 +261,20 @@ using IntegerOnly = std::enable_if_t::value, O>; template using ArithmeticOnly = std::enable_if_t::value, O>; +template +using IteratorOnly = std::enable_if_t< + !std::is_same_v::value_type, void>, O +>; + +template // Arbitrary allocator can be used +IntegerOnly> reserve_vector(I capacity) +{ + std::vector ret; + if (capacity > I(0)) ret.reserve(size_t(capacity)); + + return ret; +} + } // namespace Slic3r #endif diff --git a/src/libslic3r/libslic3r_version.h.in b/src/libslic3r/libslic3r_version.h.in index 26a67362b7..90f0812aba 100644 --- a/src/libslic3r/libslic3r_version.h.in +++ b/src/libslic3r/libslic3r_version.h.in @@ -6,4 +6,7 @@ #define SLIC3R_VERSION "@SLIC3R_VERSION@" #define SLIC3R_BUILD_ID "@SLIC3R_BUILD_ID@" +#define GCODEVIEWER_APP_NAME "@GCODEVIEWER_APP_NAME@" +#define GCODEVIEWER_BUILD_ID "@GCODEVIEWER_BUILD_ID@" + #endif /* __SLIC3R_VERSION_H */ diff --git a/src/platform/msw/PrusaSlicer-gcodeviewer.rc.in b/src/platform/msw/PrusaSlicer-gcodeviewer.rc.in new file mode 100644 index 0000000000..7f4e5a15c3 --- /dev/null +++ b/src/platform/msw/PrusaSlicer-gcodeviewer.rc.in @@ -0,0 +1,25 @@ +1 VERSIONINFO +FILEVERSION @SLIC3R_RC_VERSION@ +PRODUCTVERSION @SLIC3R_RC_VERSION@ +{ + BLOCK "StringFileInfo" + { + BLOCK "040904E4" + { + VALUE "CompanyName", "Prusa Research" + VALUE "FileDescription", "@SLIC3R_APP_NAME@ G-code Viewer" + VALUE "FileVersion", "@SLIC3R_BUILD_ID@" + VALUE "ProductName", "@SLIC3R_APP_NAME@ G-code Viewer" + VALUE "ProductVersion", "@SLIC3R_BUILD_ID@" + VALUE "InternalName", "@SLIC3R_APP_NAME@ G-code Viewer" + VALUE "LegalCopyright", "Copyright \251 2016-2020 Prusa Research, \251 2011-2018 Alessandro Ranelucci" + VALUE "OriginalFilename", "prusa-gcodeviewer.exe" + } + } + BLOCK "VarFileInfo" + { + VALUE "Translation", 0x409, 1252 + } +} +2 ICON "@SLIC3R_RESOURCES_DIR@/icons/PrusaSlicer-gcodeviewer.ico" +1 24 "PrusaSlicer.manifest" diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 7e02c0fdd7..1c30078102 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -12,8 +12,6 @@ set(SLIC3R_GUI_SOURCES GUI/SysInfoDialog.hpp GUI/KBShortcutsDialog.cpp GUI/KBShortcutsDialog.hpp - GUI/AppConfig.cpp - GUI/AppConfig.hpp GUI/BackgroundSlicingProcess.cpp GUI/BackgroundSlicingProcess.hpp GUI/BitmapCache.cpp @@ -23,6 +21,8 @@ set(SLIC3R_GUI_SOURCES GUI/3DScene.cpp GUI/3DScene.hpp GUI/format.hpp + GUI/GLShadersManager.hpp + GUI/GLShadersManager.cpp GUI/GLShader.cpp GUI/GLShader.hpp GUI/GLCanvas3D.hpp @@ -53,18 +53,22 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoCut.hpp GUI/Gizmos/GLGizmoHollow.cpp GUI/Gizmos/GLGizmoHollow.hpp + GUI/Gizmos/GLGizmoPainterBase.cpp + GUI/Gizmos/GLGizmoPainterBase.hpp + GUI/Gizmos/GLGizmoSeam.cpp + GUI/Gizmos/GLGizmoSeam.hpp GUI/GLSelectionRectangle.cpp GUI/GLSelectionRectangle.hpp + GUI/GLModel.hpp + GUI/GLModel.cpp GUI/GLTexture.hpp GUI/GLTexture.cpp GUI/GLToolbar.hpp GUI/GLToolbar.cpp + GUI/GCodeViewer.hpp + GUI/GCodeViewer.cpp GUI/Preferences.cpp GUI/Preferences.hpp - GUI/Preset.cpp - GUI/Preset.hpp - GUI/PresetBundle.cpp - GUI/PresetBundle.hpp GUI/PresetHints.cpp GUI/PresetHints.hpp GUI/GUI.cpp @@ -81,6 +85,10 @@ set(SLIC3R_GUI_SOURCES GUI/MainFrame.hpp GUI/Plater.cpp GUI/Plater.hpp + GUI/PresetComboBoxes.hpp + GUI/PresetComboBoxes.cpp + GUI/PhysicalPrinterDialog.hpp + GUI/PhysicalPrinterDialog.cpp GUI/GUI_ObjectList.cpp GUI/GUI_ObjectList.hpp GUI/GUI_ObjectManipulation.cpp @@ -163,6 +171,12 @@ set(SLIC3R_GUI_SOURCES GUI/InstanceCheck.hpp GUI/Search.cpp GUI/Search.hpp + GUI/NotificationManager.cpp + GUI/NotificationManager.hpp + GUI/UnsavedChangesDialog.cpp + GUI/UnsavedChangesDialog.hpp + GUI/ExtraRenderers.cpp + GUI/ExtraRenderers.hpp Utils/Http.cpp Utils/Http.hpp Utils/FixModelByWin10.cpp @@ -181,13 +195,14 @@ set(SLIC3R_GUI_SOURCES Utils/Bonjour.hpp Utils/PresetUpdater.cpp Utils/PresetUpdater.hpp + Utils/Process.cpp + Utils/Process.hpp + Utils/Profile.hpp Utils/UndoRedo.cpp Utils/UndoRedo.hpp Utils/HexFile.cpp Utils/HexFile.hpp Utils/Thread.hpp - Utils/SLAImport.hpp - Utils/SLAImport.cpp ) if (APPLE) diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index 2264afa7d2..45dc998741 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -1,6 +1,4 @@ #include "Snapshot.hpp" -#include "../GUI/AppConfig.hpp" -#include "../GUI/PresetBundle.hpp" #include @@ -11,7 +9,7 @@ #include #include - +#include "libslic3r/PresetBundle.hpp" #include "libslic3r/libslic3r.h" #include "libslic3r/Time.hpp" #include "libslic3r/Config.hpp" @@ -317,7 +315,7 @@ size_t SnapshotDB::load_db() // Sort the snapshots by their date/time. std::sort(m_snapshots.begin(), m_snapshots.end(), [](const Snapshot &s1, const Snapshot &s2) { return s1.time_captured < s2.time_captured; }); if (! errors_cummulative.empty()) - throw std::runtime_error(errors_cummulative); + throw Slic3r::RuntimeError(errors_cummulative); return m_snapshots.size(); } @@ -341,7 +339,7 @@ static void copy_config_dir_single_level(const boost::filesystem::path &path_src { if (! boost::filesystem::is_directory(path_dst) && ! boost::filesystem::create_directory(path_dst)) - throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + path_dst.string()); + throw Slic3r::RuntimeError(std::string("Slic3r was unable to create a directory at ") + path_dst.string()); for (auto &dir_entry : boost::filesystem::directory_iterator(path_src)) if (Slic3r::is_ini_file(dir_entry)) @@ -431,7 +429,7 @@ const Snapshot& SnapshotDB::restore_snapshot(const std::string &id, AppConfig &a this->restore_snapshot(snapshot, app_config); return snapshot; } - throw std::runtime_error(std::string("Snapshot with id " + id + " was not found.")); + throw Slic3r::RuntimeError(std::string("Snapshot with id " + id + " was not found.")); } void SnapshotDB::restore_snapshot(const Snapshot &snapshot, AppConfig &app_config) @@ -503,7 +501,7 @@ boost::filesystem::path SnapshotDB::create_db_dir() subdir.make_preferred(); if (! boost::filesystem::is_directory(subdir) && ! boost::filesystem::create_directory(subdir)) - throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + subdir.string()); + throw Slic3r::RuntimeError(std::string("Slic3r was unable to create a directory at ") + subdir.string()); } return snapshots_dir; } diff --git a/src/slic3r/Config/Version.cpp b/src/slic3r/Config/Version.cpp index d00e4a2abf..04ce05ab5b 100644 --- a/src/slic3r/Config/Version.cpp +++ b/src/slic3r/Config/Version.cpp @@ -324,7 +324,7 @@ std::vector Index::load_db() } if (! errors_cummulative.empty()) - throw std::runtime_error(errors_cummulative); + throw Slic3r::RuntimeError(errors_cummulative); return index_db; } diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 6c070ca99a..8a29d08bd8 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -5,15 +5,24 @@ #include "libslic3r/Polygon.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/BoundingBox.hpp" +#if ENABLE_GCODE_VIEWER +#include "libslic3r/Geometry.hpp" +#endif // ENABLE_GCODE_VIEWER #include "GUI_App.hpp" -#include "PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" #include "GLCanvas3D.hpp" +#if ENABLE_GCODE_VIEWER +#include "3DScene.hpp" +#endif // ENABLE_GCODE_VIEWER #include #include #include +#if ENABLE_GCODE_VIEWER +#include +#endif // ENABLE_GCODE_VIEWER static const float GROUND_Z = -0.02f; @@ -36,10 +45,8 @@ bool GeometryBuffer::set_from_triangles(const Polygons& triangles, float z, bool float max_y = min_y; unsigned int v_count = 0; - for (const Polygon& t : triangles) - { - for (unsigned int i = 0; i < 3; ++i) - { + for (const Polygon& t : triangles) { + for (unsigned int i = 0; i < 3; ++i) { Vertex& v = m_vertices[v_count]; const Point& p = t.points[i]; @@ -50,8 +57,7 @@ bool GeometryBuffer::set_from_triangles(const Polygons& triangles, float z, bool v.position[1] = y; v.position[2] = z; - if (generate_tex_coords) - { + if (generate_tex_coords) { v.tex_coords[0] = x; v.tex_coords[1] = y; @@ -65,17 +71,14 @@ bool GeometryBuffer::set_from_triangles(const Polygons& triangles, float z, bool } } - if (generate_tex_coords) - { + if (generate_tex_coords) { float size_x = max_x - min_x; float size_y = max_y - min_y; - if ((size_x != 0.0f) && (size_y != 0.0f)) - { + if ((size_x != 0.0f) && (size_y != 0.0f)) { float inv_size_x = 1.0f / size_x; float inv_size_y = -1.0f / size_y; - for (Vertex& v : m_vertices) - { + for (Vertex& v : m_vertices) { v.tex_coords[0] = (v.tex_coords[0] - min_x) * inv_size_x; v.tex_coords[1] = (v.tex_coords[1] - min_y) * inv_size_y; } @@ -96,8 +99,7 @@ bool GeometryBuffer::set_from_lines(const Lines& lines, float z) m_vertices = std::vector(v_size, Vertex()); unsigned int v_count = 0; - for (const Line& l : lines) - { + for (const Line& l : lines) { Vertex& v1 = m_vertices[v_count]; v1.position[0] = unscale(l.a(0)); v1.position[1] = unscale(l.a(1)); @@ -119,10 +121,24 @@ const float* GeometryBuffer::get_vertices_data() const return (m_vertices.size() > 0) ? (const float*)m_vertices.data() : nullptr; } +#if ENABLE_GCODE_VIEWER +const float Bed3D::Axes::DefaultStemRadius = 0.5f; +const float Bed3D::Axes::DefaultStemLength = 25.0f; +const float Bed3D::Axes::DefaultTipRadius = 2.5f * Bed3D::Axes::DefaultStemRadius; +const float Bed3D::Axes::DefaultTipLength = 5.0f; +#else const double Bed3D::Axes::Radius = 0.5; const double Bed3D::Axes::ArrowBaseRadius = 2.5 * Bed3D::Axes::Radius; const double Bed3D::Axes::ArrowLength = 5.0; +#endif // ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER +void Bed3D::Axes::set_stem_length(float length) +{ + m_stem_length = length; + m_arrow.reset(); +} +#else Bed3D::Axes::Axes() : origin(Vec3d::Zero()) , length(25.0 * Vec3d::Ones()) @@ -137,9 +153,47 @@ Bed3D::Axes::~Axes() if (m_quadric != nullptr) ::gluDeleteQuadric(m_quadric); } +#endif // ENABLE_GCODE_VIEWER void Bed3D::Axes::render() const { +#if ENABLE_GCODE_VIEWER + auto render_axis = [this](const Transform3f& transform) { + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixf(transform.data())); + m_arrow.render(); + glsafe(::glPopMatrix()); + }; + + m_arrow.init_from(stilized_arrow(16, DefaultTipRadius, DefaultTipLength, DefaultStemRadius, m_stem_length)); + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + + shader->start_using(); + + // x axis + std::array color = { 0.75f, 0.0f, 0.0f, 1.0f }; + shader->set_uniform("uniform_color", color); + render_axis(Geometry::assemble_transform(m_origin, { 0.0, 0.5 * M_PI, 0.0f }).cast()); + + // y axis + color = { 0.0f, 0.75f, 0.0f, 1.0f }; + shader->set_uniform("uniform_color", color); + render_axis(Geometry::assemble_transform(m_origin, { -0.5 * M_PI, 0.0, 0.0f }).cast()); + + // z axis + color = { 0.0f, 0.0f, 0.75f, 1.0f }; + shader->set_uniform("uniform_color", color); + render_axis(Geometry::assemble_transform(m_origin).cast()); + + shader->stop_using(); + + glsafe(::glDisable(GL_DEPTH_TEST)); +#else if (m_quadric == nullptr) return; @@ -171,8 +225,10 @@ void Bed3D::Axes::render() const glsafe(::glDisable(GL_LIGHTING)); glsafe(::glDisable(GL_DEPTH_TEST)); +#endif // !ENABLE_GCODE_VIEWER } +#if !ENABLE_GCODE_VIEWER void Bed3D::Axes::render_axis(double length) const { ::gluQuadricOrientation(m_quadric, GLU_OUTSIDE); @@ -185,6 +241,7 @@ void Bed3D::Axes::render_axis(double length) const ::gluQuadricOrientation(m_quadric, GLU_INSIDE); ::gluDisk(m_quadric, 0.0, ArrowBaseRadius, 32, 1); } +#endif // !ENABLE_GCODE_VIEWER Bed3D::Bed3D() : m_type(Custom) @@ -193,7 +250,7 @@ Bed3D::Bed3D() { } -bool Bed3D::set_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model) +bool Bed3D::set_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) { auto check_texture = [](const std::string& texture) { return !texture.empty() && (boost::algorithm::iends_with(texture, ".png") || boost::algorithm::iends_with(texture, ".svg")) && boost::filesystem::exists(texture); @@ -203,30 +260,39 @@ bool Bed3D::set_shape(const Pointfs& shape, const std::string& custom_texture, c return !model.empty() && boost::algorithm::iends_with(model, ".stl") && boost::filesystem::exists(model); }; - auto [new_type, system_model, system_texture] = detect_type(shape); + EType type; + std::string model; + std::string texture; + if (force_as_custom) + type = Custom; + else { + auto [new_type, system_model, system_texture] = detect_type(shape); + type = new_type; + model = system_model; + texture = system_texture; + } - std::string texture_filename = custom_texture.empty() ? system_texture : custom_texture; + std::string texture_filename = custom_texture.empty() ? texture : custom_texture; if (!check_texture(texture_filename)) texture_filename.clear(); - std::string model_filename = custom_model.empty() ? system_model : custom_model; + std::string model_filename = custom_model.empty() ? model : custom_model; if (!check_model(model_filename)) model_filename.clear(); - if ((m_shape == shape) && (m_type == new_type) && (m_texture_filename == texture_filename) && (m_model_filename == model_filename)) + if (m_shape == shape && m_type == type && m_texture_filename == texture_filename && m_model_filename == model_filename) // No change, no need to update the UI. return false; m_shape = shape; m_texture_filename = texture_filename; m_model_filename = model_filename; - m_type = new_type; + m_type = type; calc_bounding_boxes(); ExPolygon poly; - for (const Vec2d& p : m_shape) - { + for (const Vec2d& p : m_shape) { poly.contour.append(Point(scale_(p(0)), scale_(p(1)))); } @@ -242,8 +308,13 @@ bool Bed3D::set_shape(const Pointfs& shape, const std::string& custom_texture, c m_model.reset(); // Set the origin and size for rendering the coordinate system axes. +#if ENABLE_GCODE_VIEWER + m_axes.set_origin({ 0.0, 0.0, static_cast(GROUND_Z) }); + m_axes.set_stem_length(0.1f * static_cast(m_bounding_box.max_size())); +#else m_axes.origin = Vec3d(0.0, 0.0, (double)GROUND_Z); m_axes.length = 0.1 * m_bounding_box.max_size() * Vec3d::Ones(); +#endif // ENABLE_GCODE_VIEWER // Let the calee to update the UI. return true; @@ -282,25 +353,35 @@ void Bed3D::render(GLCanvas3D& canvas, bool bottom, float scale_factor, void Bed3D::calc_bounding_boxes() const { m_bounding_box = BoundingBoxf3(); - for (const Vec2d& p : m_shape) - { + for (const Vec2d& p : m_shape) { m_bounding_box.merge(Vec3d(p(0), p(1), 0.0)); } m_extended_bounding_box = m_bounding_box; // extend to contain axes - m_extended_bounding_box.merge(m_axes.length + Axes::ArrowLength * Vec3d::Ones()); +#if ENABLE_GCODE_VIEWER + m_extended_bounding_box.merge(m_axes.get_origin() + m_axes.get_total_length() * Vec3d::Ones()); + m_extended_bounding_box.merge(m_extended_bounding_box.min + Vec3d(-Axes::DefaultTipRadius, -Axes::DefaultTipRadius, m_extended_bounding_box.max(2))); + // extend to contain model, if any + BoundingBoxf3 model_bb = m_model.get_bounding_box(); + if (model_bb.defined) { + model_bb.translate(m_model_offset); + m_extended_bounding_box.merge(model_bb); + } +#else + m_extended_bounding_box.merge(m_axes.length + Axes::ArrowLength * Vec3d::Ones()); // extend to contain model, if any if (!m_model.get_filename().empty()) m_extended_bounding_box.merge(m_model.get_transformed_bounding_box()); +#endif // ENABLE_GCODE_VIEWER } void Bed3D::calc_triangles(const ExPolygon& poly) { Polygons triangles; - poly.triangulate(&triangles); + poly.triangulate_p2t(&triangles); if (!m_triangles.set_from_triangles(triangles, GROUND_Z, true)) printf("Unable to create bed triangles\n"); @@ -309,15 +390,13 @@ void Bed3D::calc_triangles(const ExPolygon& poly) void Bed3D::calc_gridlines(const ExPolygon& poly, const BoundingBox& bed_bbox) { Polylines axes_lines; - for (coord_t x = bed_bbox.min(0); x <= bed_bbox.max(0); x += scale_(10.0)) - { + for (coord_t x = bed_bbox.min(0); x <= bed_bbox.max(0); x += scale_(10.0)) { Polyline line; line.append(Point(x, bed_bbox.min(1))); line.append(Point(x, bed_bbox.max(1))); axes_lines.push_back(line); } - for (coord_t y = bed_bbox.min(1); y <= bed_bbox.max(1); y += scale_(10.0)) - { + for (coord_t y = bed_bbox.min(1); y <= bed_bbox.max(1); y += scale_(10.0)) { Polyline line; line.append(Point(bed_bbox.min(0), y)); line.append(Point(bed_bbox.max(0), y)); @@ -335,6 +414,7 @@ void Bed3D::calc_gridlines(const ExPolygon& poly, const BoundingBox& bed_bbox) printf("Unable to create bed grid lines\n"); } +#if !ENABLE_GCODE_VIEWER static std::string system_print_bed_model(const Preset &preset) { std::string out; @@ -352,23 +432,25 @@ static std::string system_print_bed_texture(const Preset &preset) out = Slic3r::resources_dir() + "/profiles/" + preset.vendor->id + "/" + pm->bed_texture; return out; } +#endif // !ENABLE_GCODE_VIEWER std::tuple Bed3D::detect_type(const Pointfs& shape) const { auto bundle = wxGetApp().preset_bundle; - if (bundle != nullptr) - { + if (bundle != nullptr) { const Preset* curr = &bundle->printers.get_selected_preset(); - while (curr != nullptr) - { - if (curr->config.has("bed_shape")) - { - if (shape == dynamic_cast(curr->config.option("bed_shape"))->values) - { + while (curr != nullptr) { + if (curr->config.has("bed_shape")) { + if (shape == dynamic_cast(curr->config.option("bed_shape"))->values) { +#if ENABLE_GCODE_VIEWER + std::string model_filename = PresetUtils::system_printer_bed_model(*curr); + std::string texture_filename = PresetUtils::system_printer_bed_texture(*curr); +#else std::string model_filename = system_print_bed_model(*curr); std::string texture_filename = system_print_bed_texture(*curr); +#endif // ENABLE_GCODE_VIEWER if (!model_filename.empty() && !texture_filename.empty()) - return std::make_tuple(System, model_filename, texture_filename); + return { System, model_filename, texture_filename }; } } @@ -376,7 +458,7 @@ std::tuple Bed3D::detect_type(const Poin } } - return std::make_tuple(Custom, "", ""); + return { Custom, "", "" }; } void Bed3D::render_axes() const @@ -396,26 +478,21 @@ void Bed3D::render_system(GLCanvas3D& canvas, bool bottom, bool show_texture) co void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas) const { - if (m_texture_filename.empty()) - { + if (m_texture_filename.empty()) { m_texture.reset(); render_default(bottom); return; } - if ((m_texture.get_id() == 0) || (m_texture.get_source() != m_texture_filename)) - { + if ((m_texture.get_id() == 0) || (m_texture.get_source() != m_texture_filename)) { m_texture.reset(); - if (boost::algorithm::iends_with(m_texture_filename, ".svg")) - { + if (boost::algorithm::iends_with(m_texture_filename, ".svg")) { // use higher resolution images if graphic card and opengl version allow GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(); - if ((m_temp_texture.get_id() == 0) || (m_temp_texture.get_source() != m_texture_filename)) - { + if ((m_temp_texture.get_id() == 0) || (m_temp_texture.get_source() != m_texture_filename)) { // generate a temporary lower resolution texture to show while no main texture levels have been compressed - if (!m_temp_texture.load_from_svg_file(m_texture_filename, false, false, false, max_tex_size / 8)) - { + if (!m_temp_texture.load_from_svg_file(m_texture_filename, false, false, false, max_tex_size / 8)) { render_default(bottom); return; } @@ -423,19 +500,15 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas) const } // starts generating the main texture, compression will run asynchronously - if (!m_texture.load_from_svg_file(m_texture_filename, true, true, true, max_tex_size)) - { + if (!m_texture.load_from_svg_file(m_texture_filename, true, true, true, max_tex_size)) { render_default(bottom); return; } - } - else if (boost::algorithm::iends_with(m_texture_filename, ".png")) - { + } + else if (boost::algorithm::iends_with(m_texture_filename, ".png")) { // generate a temporary lower resolution texture to show while no main texture levels have been compressed - if ((m_temp_texture.get_id() == 0) || (m_temp_texture.get_source() != m_texture_filename)) - { - if (!m_temp_texture.load_from_file(m_texture_filename, false, GLTexture::None, false)) - { + if ((m_temp_texture.get_id() == 0) || (m_temp_texture.get_source() != m_texture_filename)) { + if (!m_temp_texture.load_from_file(m_texture_filename, false, GLTexture::None, false)) { render_default(bottom); return; } @@ -443,20 +516,17 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas) const } // starts generating the main texture, compression will run asynchronously - if (!m_texture.load_from_file(m_texture_filename, true, GLTexture::MultiThreaded, true)) - { + if (!m_texture.load_from_file(m_texture_filename, true, GLTexture::MultiThreaded, true)) { render_default(bottom); return; } } - else - { + else { render_default(bottom); return; } } - else if (m_texture.unsent_compressed_data_available()) - { + else if (m_texture.unsent_compressed_data_available()) { // sends to gpu the already available compressed levels of the main texture m_texture.send_compressed_data_to_gpu(); @@ -468,19 +538,14 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas) const } - if (m_triangles.get_vertices_count() > 0) - { - if (m_shader.get_shader_program_id() == 0) - m_shader.init("printbed.vs", "printbed.fs"); + if (m_triangles.get_vertices_count() > 0) { + GLShaderProgram* shader = wxGetApp().get_shader("printbed"); + if (shader != nullptr) { + shader->start_using(); + shader->set_uniform("transparent_background", bottom); + shader->set_uniform("svg_source", boost::algorithm::iends_with(m_texture.get_source(), ".svg")); - if (m_shader.is_initialized()) - { - m_shader.start_using(); - m_shader.set_uniform("transparent_background", bottom); - m_shader.set_uniform("svg_source", boost::algorithm::iends_with(m_texture.get_source(), ".svg")); - - if (m_vbo_id == 0) - { + if (m_vbo_id == 0) { glsafe(::glGenBuffers(1, &m_vbo_id)); glsafe(::glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id)); glsafe(::glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)m_triangles.get_vertices_data_size(), (const GLvoid*)m_triangles.get_vertices_data(), GL_STATIC_DRAW)); @@ -498,8 +563,8 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas) const unsigned int stride = m_triangles.get_vertex_data_size(); - GLint position_id = m_shader.get_attrib_location("v_position"); - GLint tex_coords_id = m_shader.get_attrib_location("v_tex_coords"); + GLint position_id = shader->get_attrib_location("v_position"); + GLint tex_coords_id = shader->get_attrib_location("v_tex_coords"); // show the temporary texture while no compressed data is available GLuint tex_id = (GLuint)m_temp_texture.get_id(); @@ -509,13 +574,11 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas) const glsafe(::glBindTexture(GL_TEXTURE_2D, tex_id)); glsafe(::glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id)); - if (position_id != -1) - { + if (position_id != -1) { glsafe(::glEnableVertexAttribArray(position_id)); glsafe(::glVertexAttribPointer(position_id, 3, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(intptr_t)m_triangles.get_position_offset())); } - if (tex_coords_id != -1) - { + if (tex_coords_id != -1) { glsafe(::glEnableVertexAttribArray(tex_coords_id)); glsafe(::glVertexAttribPointer(tex_coords_id, 2, GL_FLOAT, GL_FALSE, stride, (GLvoid*)(intptr_t)m_triangles.get_tex_coords_offset())); } @@ -537,7 +600,7 @@ void Bed3D::render_texture(bool bottom, GLCanvas3D& canvas) const glsafe(::glDisable(GL_BLEND)); glsafe(::glDepthMask(GL_TRUE)); - m_shader.stop_using(); + shader->stop_using(); } } } @@ -547,29 +610,41 @@ void Bed3D::render_model() const if (m_model_filename.empty()) return; - if ((m_model.get_filename() != m_model_filename) && m_model.init_from_file(m_model_filename)) - { + if ((m_model.get_filename() != m_model_filename) && m_model.init_from_file(m_model_filename)) { // move the model so that its origin (0.0, 0.0, 0.0) goes into the bed shape center and a bit down to avoid z-fighting with the texture quad Vec3d shift = m_bounding_box.center(); shift(2) = -0.03; +#if ENABLE_GCODE_VIEWER + m_model_offset = shift; +#else m_model.set_offset(shift); +#endif // ENABLE_GCODE_VIEWER // update extended bounding box calc_bounding_boxes(); } - if (!m_model.get_filename().empty()) - { - glsafe(::glEnable(GL_LIGHTING)); - m_model.render(); - glsafe(::glDisable(GL_LIGHTING)); + if (!m_model.get_filename().empty()) { + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader != nullptr) { + shader->start_using(); +#if ENABLE_GCODE_VIEWER + shader->set_uniform("uniform_color", m_model_color); + ::glPushMatrix(); + ::glTranslated(m_model_offset(0), m_model_offset(1), m_model_offset(2)); +#endif // ENABLE_GCODE_VIEWER + m_model.render(); +#if ENABLE_GCODE_VIEWER + ::glPopMatrix(); +#endif // ENABLE_GCODE_VIEWER + shader->stop_using(); + } } } void Bed3D::render_custom(GLCanvas3D& canvas, bool bottom, bool show_texture) const { - if (m_texture_filename.empty() && m_model_filename.empty()) - { + if (m_texture_filename.empty() && m_model_filename.empty()) { render_default(bottom); return; } @@ -586,8 +661,7 @@ void Bed3D::render_default(bool bottom) const m_texture.reset(); unsigned int triangles_vcount = m_triangles.get_vertices_count(); - if (triangles_vcount > 0) - { + if (triangles_vcount > 0) { bool has_model = !m_model.get_filename().empty(); glsafe(::glEnable(GL_DEPTH_TEST)); @@ -596,11 +670,14 @@ void Bed3D::render_default(bool bottom) const glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); - if (!has_model && !bottom) - { + if (!has_model && !bottom) { // draw background glsafe(::glDepthMask(GL_FALSE)); +#if ENABLE_GCODE_VIEWER + glsafe(::glColor4fv(m_model_color.data())); +#else glsafe(::glColor4f(0.35f, 0.35f, 0.35f, 0.4f)); +#endif // ENABLE_GCODE_VIEWER glsafe(::glNormal3d(0.0f, 0.0f, 1.0f)); glsafe(::glVertexPointer(3, GL_FLOAT, m_triangles.get_vertex_data_size(), (GLvoid*)m_triangles.get_vertices_data())); glsafe(::glDrawArrays(GL_TRIANGLES, 0, (GLsizei)triangles_vcount)); @@ -608,11 +685,11 @@ void Bed3D::render_default(bool bottom) const } // draw grid - glsafe(::glLineWidth(3.0f * m_scale_factor)); + glsafe(::glLineWidth(1.5f * m_scale_factor)); if (has_model && !bottom) - glsafe(::glColor4f(0.75f, 0.75f, 0.75f, 1.0f)); + glsafe(::glColor4f(0.9f, 0.9f, 0.9f, 1.0f)); else - glsafe(::glColor4f(0.2f, 0.2f, 0.2f, 0.4f)); + glsafe(::glColor4f(0.9f, 0.9f, 0.9f, 0.6f)); glsafe(::glVertexPointer(3, GL_FLOAT, m_triangles.get_vertex_data_size(), (GLvoid*)m_gridlines.get_vertices_data())); glsafe(::glDrawArrays(GL_LINES, 0, (GLsizei)m_gridlines.get_vertices_count())); @@ -624,8 +701,7 @@ void Bed3D::render_default(bool bottom) const void Bed3D::reset() { - if (m_vbo_id > 0) - { + if (m_vbo_id > 0) { glsafe(::glDeleteBuffers(1, &m_vbo_id)); m_vbo_id = 0; } diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index abdfca1fe0..fbfc3078c1 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -3,12 +3,19 @@ #include "GLTexture.hpp" #include "3DScene.hpp" -#include "GLShader.hpp" +#if ENABLE_GCODE_VIEWER +#include "GLModel.hpp" +#endif // ENABLE_GCODE_VIEWER #include +#if ENABLE_GCODE_VIEWER +#include +#endif // ENABLE_GCODE_VIEWER +#if !ENABLE_GCODE_VIEWER class GLUquadric; typedef class GLUquadric GLUquadricObj; +#endif // !ENABLE_GCODE_VIEWER namespace Slic3r { namespace GUI { @@ -45,22 +52,53 @@ public: class Bed3D { +#if ENABLE_GCODE_VIEWER + class Axes + { + public: + static const float DefaultStemRadius; + static const float DefaultStemLength; + static const float DefaultTipRadius; + static const float DefaultTipLength; + + private: +#else struct Axes { static const double Radius; static const double ArrowBaseRadius; static const double ArrowLength; +#endif // ENABLE_GCODE_VIEWER + +#if ENABLE_GCODE_VIEWER + Vec3d m_origin{ Vec3d::Zero() }; + float m_stem_length{ DefaultStemLength }; + mutable GLModel m_arrow; + + public: +#else Vec3d origin; Vec3d length; GLUquadricObj* m_quadric; +#endif // ENABLE_GCODE_VIEWER +#if !ENABLE_GCODE_VIEWER Axes(); ~Axes(); +#endif // !ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER + const Vec3d& get_origin() const { return m_origin; } + void set_origin(const Vec3d& origin) { m_origin = origin; } + void set_stem_length(float length); + float get_total_length() const { return m_stem_length + DefaultTipLength; } +#endif // ENABLE_GCODE_VIEWER void render() const; +#if !ENABLE_GCODE_VIEWER private: void render_axis(double length) const; +#endif // !ENABLE_GCODE_VIEWER }; public: @@ -82,10 +120,15 @@ private: GeometryBuffer m_triangles; GeometryBuffer m_gridlines; mutable GLTexture m_texture; +#if ENABLE_GCODE_VIEWER + mutable GLModel m_model; + mutable Vec3d m_model_offset{ Vec3d::Zero() }; + std::array m_model_color{ 0.235f, 0.235f, 0.235f, 1.0f }; +#else mutable GLBed m_model; +#endif // ENABLE_GCODE_VIEWER // temporary texture shown until the main texture has still no levels compressed mutable GLTexture m_temp_texture; - mutable Shader m_shader; mutable unsigned int m_vbo_id; Axes m_axes; @@ -101,7 +144,7 @@ public: const Pointfs& get_shape() const { return m_shape; } // Return true if the bed shape changed, so the calee will update the UI. - bool set_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model); + bool set_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom = false); const BoundingBoxf3& get_bounding_box(bool extended) const { return extended ? m_extended_bounding_box : m_bounding_box; diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 5dc0d430f1..ca96af49c1 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -7,23 +7,28 @@ #endif // ENABLE_SMOOTH_NORMALS #include "3DScene.hpp" -#if ENABLE_ENVIRONMENT_MAP +#include "GLShader.hpp" #include "GUI_App.hpp" +#if ENABLE_ENVIRONMENT_MAP #include "Plater.hpp" -#include "AppConfig.hpp" #endif // ENABLE_ENVIRONMENT_MAP #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/ExtrusionEntityCollection.hpp" #include "libslic3r/Geometry.hpp" +#if !ENABLE_GCODE_VIEWER #include "libslic3r/GCode/PreviewData.hpp" +#endif // !ENABLE_GCODE_VIEWER #include "libslic3r/Print.hpp" #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Slicing.hpp" +#if !ENABLE_GCODE_VIEWER #include "libslic3r/GCode/Analyzer.hpp" +#endif // !ENABLE_GCODE_VIEWER #include "slic3r/GUI/BitmapCache.hpp" #include "libslic3r/Format/STL.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/AppConfig.hpp" #include #include @@ -442,7 +447,6 @@ BoundingBoxf3 GLVolume::transformed_convex_hull_bounding_box(const Transform3d & bounding_box().transformed(trafo); } - void GLVolume::set_range(double min_z, double max_z) { this->qverts_range.first = 0; @@ -747,6 +751,10 @@ GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCo void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disable_cullface, const Transform3d& view_matrix, std::function filter_func) const { + GLShaderProgram* shader = GUI::wxGetApp().get_current_shader(); + if (shader == nullptr) + return; + glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); @@ -757,80 +765,32 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disab glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); - GLint current_program_id; - glsafe(::glGetIntegerv(GL_CURRENT_PROGRAM, ¤t_program_id)); - GLint color_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "uniform_color") : -1; - GLint z_range_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "z_range") : -1; - GLint clipping_plane_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "clipping_plane") : -1; - - GLint print_box_min_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "print_box.min") : -1; - GLint print_box_max_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "print_box.max") : -1; - GLint print_box_active_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "print_box.actived") : -1; - GLint print_box_worldmatrix_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "print_box.volume_world_matrix") : -1; - + shader->set_uniform("print_box.min", m_print_box_min, 3); + shader->set_uniform("print_box.max", m_print_box_max, 3); + shader->set_uniform("z_range", m_z_range, 2); + shader->set_uniform("clipping_plane", m_clipping_plane, 4); #if ENABLE_SLOPE_RENDERING - GLint slope_active_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "slope.actived") : -1; - GLint slope_normal_matrix_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "slope.volume_world_normal_matrix") : -1; - GLint slope_z_range_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "slope.z_range") : -1; -#endif // ENABLE_SLOPE_RENDERING - -#if ENABLE_ENVIRONMENT_MAP - GLint use_environment_tex_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "use_environment_tex") : -1; -#endif // ENABLE_ENVIRONMENT_MAP - glcheck(); - - if (print_box_min_id != -1) - glsafe(::glUniform3fv(print_box_min_id, 1, (const GLfloat*)m_print_box_min)); - - if (print_box_max_id != -1) - glsafe(::glUniform3fv(print_box_max_id, 1, (const GLfloat*)m_print_box_max)); - - if (z_range_id != -1) - glsafe(::glUniform2fv(z_range_id, 1, (const GLfloat*)m_z_range)); - - if (clipping_plane_id != -1) - glsafe(::glUniform4fv(clipping_plane_id, 1, (const GLfloat*)m_clipping_plane)); - -#if ENABLE_SLOPE_RENDERING - if (slope_z_range_id != -1) - glsafe(::glUniform2fv(slope_z_range_id, 1, (const GLfloat*)m_slope.z_range.data())); + shader->set_uniform("slope.z_range", m_slope.z_range); #endif // ENABLE_SLOPE_RENDERING #if ENABLE_ENVIRONMENT_MAP unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id(); - bool use_environment_texture = current_program_id > 0 && environment_texture_id > 0 && GUI::wxGetApp().app_config->get("use_environment_map") == "1"; - - if (use_environment_tex_id != -1) - { - glsafe(::glUniform1i(use_environment_tex_id, use_environment_texture ? 1 : 0)); - if (use_environment_texture) - glsafe(::glBindTexture(GL_TEXTURE_2D, environment_texture_id)); - } + bool use_environment_texture = environment_texture_id > 0 && GUI::wxGetApp().app_config->get("use_environment_map") == "1"; + shader->set_uniform("use_environment_tex", use_environment_texture); + if (use_environment_texture) + glsafe(::glBindTexture(GL_TEXTURE_2D, environment_texture_id)); #endif // ENABLE_ENVIRONMENT_MAP + glcheck(); GLVolumeWithIdAndZList to_render = volumes_to_render(this->volumes, type, view_matrix, filter_func); for (GLVolumeWithIdAndZ& volume : to_render) { volume.first->set_render_color(); #if ENABLE_SLOPE_RENDERING - if (color_id >= 0) - glsafe(::glUniform4fv(color_id, 1, (const GLfloat*)volume.first->render_color)); - else - glsafe(::glColor4fv(volume.first->render_color)); - - if (print_box_active_id != -1) - glsafe(::glUniform1i(print_box_active_id, volume.first->shader_outside_printer_detection_enabled ? 1 : 0)); - - if (print_box_worldmatrix_id != -1) - glsafe(::glUniformMatrix4fv(print_box_worldmatrix_id, 1, GL_FALSE, (const GLfloat*)volume.first->world_matrix().cast().data())); - - if (slope_active_id != -1) - glsafe(::glUniform1i(slope_active_id, m_slope.active && !volume.first->is_modifier && !volume.first->is_wipe_tower ? 1 : 0)); - - if (slope_normal_matrix_id != -1) - { - Matrix3f normal_matrix = volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast(); - glsafe(::glUniformMatrix3fv(slope_normal_matrix_id, 1, GL_FALSE, (const GLfloat*)normal_matrix.data())); - } + shader->set_uniform("uniform_color", volume.first->render_color, 4); + shader->set_uniform("print_box.actived", volume.first->shader_outside_printer_detection_enabled); + shader->set_uniform("print_box.volume_world_matrix", volume.first->world_matrix()); + shader->set_uniform("slope.actived", m_slope.active && !volume.first->is_modifier && !volume.first->is_wipe_tower); + shader->set_uniform("slope.volume_world_normal_matrix", static_cast(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast())); volume.first->render(); #else @@ -839,7 +799,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disab } #if ENABLE_ENVIRONMENT_MAP - if (use_environment_tex_id != -1 && use_environment_texture) + if (use_environment_texture) glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); #endif // ENABLE_ENVIRONMENT_MAP @@ -1057,6 +1017,7 @@ bool GLVolumeCollection::has_toolpaths_to_export() const return false; } +#if !ENABLE_GCODE_VIEWER void GLVolumeCollection::export_toolpaths_to_obj(const char* filename) const { if (filename == nullptr) @@ -1338,6 +1299,7 @@ void GLVolumeCollection::export_toolpaths_to_obj(const char* filename) const fclose(fp); } +#endif // !ENABLE_GCODE_VIEWER // caller is responsible for supplying NO lines with zero length static void thick_lines_to_indexed_vertex_array( @@ -1964,7 +1926,7 @@ void _3DScene::extrusionentity_to_verts(const ExtrusionEntity *extrusion_entity, if (extrusion_entity_collection != nullptr) extrusionentity_to_verts(*extrusion_entity_collection, print_z, copy, volume); else { - throw std::runtime_error("Unexpected extrusion_entity type in to_verts()"); + throw Slic3r::RuntimeError("Unexpected extrusion_entity type in to_verts()"); } } } @@ -1985,6 +1947,7 @@ void _3DScene::point3_to_verts(const Vec3crd& point, double width, double height thick_point_to_verts(point, width, height, volume); } +#if !ENABLE_GCODE_VIEWER GLModel::GLModel() : m_filename("") { @@ -2040,6 +2003,10 @@ void GLModel::reset() void GLModel::render() const { + GLShaderProgram* shader = GUI::wxGetApp().get_current_shader(); + if (shader == nullptr) + return; + glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); @@ -2047,17 +2014,8 @@ void GLModel::render() const glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); - GLint current_program_id; - glsafe(::glGetIntegerv(GL_CURRENT_PROGRAM, ¤t_program_id)); - GLint color_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "uniform_color") : -1; - glcheck(); - #if ENABLE_SLOPE_RENDERING - if (color_id >= 0) - glsafe(::glUniform4fv(color_id, 1, (const GLfloat*)m_volume.render_color)); - else - glsafe(::glColor4fv(m_volume.render_color)); - + shader->set_uniform("uniform_color", m_volume.render_color, 4); m_volume.render(); #else m_volume.render(color_id, -1, -1); @@ -2274,5 +2232,6 @@ bool GLBed::on_init_from_file(const std::string& filename) return true; } +#endif // !ENABLE_GCODE_VIEWER } // namespace Slic3r diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index f935f0fb47..7e8ae6fe33 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -26,12 +26,6 @@ #endif // HAS_GLSAFE namespace Slic3r { -namespace GUI { -class Bed3D; -struct Camera; -class GLToolbar; -} // namespace GUI - class SLAPrintObject; enum SLAPrintObjectStep : unsigned int; class DynamicPrintConfig; @@ -603,8 +597,10 @@ public: std::string log_memory_info() const; bool has_toolpaths_to_export() const; +#if !ENABLE_GCODE_VIEWER // Export the geometry of the GLVolumes toolpaths of this collection into the file with the given path, in obj format void export_toolpaths_to_obj(const char* filename) const; +#endif // !ENABLE_GCODE_VIEWER private: GLVolumeCollection(const GLVolumeCollection &other); @@ -613,6 +609,7 @@ private: GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCollection::ERenderType type, const Transform3d& view_matrix, std::function filter_func = nullptr); +#if !ENABLE_GCODE_VIEWER class GLModel { protected: @@ -672,6 +669,7 @@ class GLBed : public GLModel protected: bool on_init_from_file(const std::string& filename) override; }; +#endif // !ENABLE_GCODE_VIEWER struct _3DScene { diff --git a/src/slic3r/GUI/AboutDialog.cpp b/src/slic3r/GUI/AboutDialog.cpp index 92f8d1fdbd..8d9ea97b98 100644 --- a/src/slic3r/GUI/AboutDialog.cpp +++ b/src/slic3r/GUI/AboutDialog.cpp @@ -37,10 +37,17 @@ void AboutDialogLogo::onRepaint(wxEvent &event) // CopyrightsDialog // ----------------------------------------- CopyrightsDialog::CopyrightsDialog() +#if ENABLE_GCODE_VIEWER + : DPIDialog(NULL, wxID_ANY, from_u8((boost::format("%1% - %2%") + % (wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME) + % _utf8(L("Portions copyright"))).str()), + wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +#else : DPIDialog(NULL, wxID_ANY, from_u8((boost::format("%1% - %2%") % SLIC3R_APP_NAME % _utf8(L("Portions copyright"))).str()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +#endif // ENABLE_GCODE_VIEWER { this->SetFont(wxGetApp().normal_font()); this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); @@ -52,7 +59,7 @@ CopyrightsDialog::CopyrightsDialog() m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxSize(40 * em_unit(), 20 * em_unit()), wxHW_SCROLLBAR_AUTO); - wxFont font = GetFont(); + wxFont font = get_default_font_for_dpi(get_dpi_for_window(this));// GetFont(); const int fs = font.GetPointSize(); const int fs2 = static_cast(1.2f*fs); int size[] = { fs, fs, fs, fs, fs2, fs2, fs2 }; @@ -201,8 +208,13 @@ void CopyrightsDialog::onCloseDialog(wxEvent &) } AboutDialog::AboutDialog() +#if ENABLE_GCODE_VIEWER + : DPIDialog(NULL, wxID_ANY, from_u8((boost::format(_utf8(L("About %s"))) % (wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME)).str()), wxDefaultPosition, + wxDefaultSize, /*wxCAPTION*/wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +#else : DPIDialog(NULL, wxID_ANY, from_u8((boost::format(_utf8(L("About %s"))) % SLIC3R_APP_NAME).str()), wxDefaultPosition, wxDefaultSize, /*wxCAPTION*/wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +#endif // ENABLE_GCODE_VIEWER { SetFont(wxGetApp().normal_font()); @@ -214,7 +226,11 @@ AboutDialog::AboutDialog() main_sizer->Add(hsizer, 0, wxEXPAND | wxALL, 20); // logo +#if ENABLE_GCODE_VIEWER + m_logo_bitmap = ScalableBitmap(this, wxGetApp().is_editor() ? "PrusaSlicer_192px.png" : "PrusaSlicer-gcodeviewer_192px.png", 192); +#else m_logo_bitmap = ScalableBitmap(this, "PrusaSlicer_192px.png", 192); +#endif // ENABLE_GCODE_VIEWER m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bitmap.bmp()); hsizer->Add(m_logo, 1, wxALIGN_CENTER_VERTICAL); @@ -223,7 +239,11 @@ AboutDialog::AboutDialog() // title { +#if ENABLE_GCODE_VIEWER + wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize); +#else wxStaticText* title = new wxStaticText(this, wxID_ANY, SLIC3R_APP_NAME, wxDefaultPosition, wxDefaultSize); +#endif // ENABLE_GCODE_VIEWER wxFont title_font = GUI::wxGetApp().bold_font(); title_font.SetFamily(wxFONTFAMILY_ROMAN); title_font.SetPointSize(24); @@ -233,7 +253,7 @@ AboutDialog::AboutDialog() // version { - auto version_string = _(L("Version"))+ " " + std::string(SLIC3R_VERSION); + auto version_string = _L("Version")+ " " + std::string(SLIC3R_VERSION); wxStaticText* version = new wxStaticText(this, wxID_ANY, version_string.c_str(), wxDefaultPosition, wxDefaultSize); wxFont version_font = GetFont(); #ifdef __WXMSW__ @@ -249,7 +269,7 @@ AboutDialog::AboutDialog() m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO/*NEVER*/); { m_html->SetMinSize(wxSize(-1, 16 * wxGetApp().em_unit())); - wxFont font = GetFont(); + wxFont font = get_default_font_for_dpi(get_dpi_for_window(this));// GetFont(); const auto text_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); auto text_clr_str = wxString::Format(wxT("#%02X%02X%02X"), text_clr.Red(), text_clr.Green(), text_clr.Blue()); auto bgr_clr_str = wxString::Format(wxT("#%02X%02X%02X"), bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()); @@ -294,7 +314,7 @@ AboutDialog::AboutDialog() wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxCLOSE); m_copy_rights_btn_id = NewControlId(); - auto copy_rights_btn = new wxButton(this, m_copy_rights_btn_id, _(L("Portions copyright"))+dots); + auto copy_rights_btn = new wxButton(this, m_copy_rights_btn_id, _L("Portions copyright")+dots); buttons->Insert(0, copy_rights_btn, 0, wxLEFT, 5); copy_rights_btn->Bind(wxEVT_BUTTON, &AboutDialog::onCopyrightBtn, this); diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 8d50998c48..605a98eea0 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -19,7 +19,9 @@ #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/GCode/PostProcessor.hpp" +#if !ENABLE_GCODE_VIEWER #include "libslic3r/GCode/PreviewData.hpp" +#endif // !ENABLE_GCODE_VIEWER #include "libslic3r/Format/SL1.hpp" #include "libslic3r/libslic3r.h" @@ -39,6 +41,36 @@ namespace Slic3r { +bool SlicingProcessCompletedEvent::critical_error() const +{ + try { + this->rethrow_exception(); + } catch (const Slic3r::SlicingError &ex) { + // Exception derived from SlicingError is non-critical. + return false; + } catch (...) { + } + return true; +} + +std::string SlicingProcessCompletedEvent::format_error_message() const +{ + std::string error; + try { + this->rethrow_exception(); + } catch (const std::bad_alloc& ex) { + wxString errmsg = GUI::from_u8((boost::format(_utf8(L("%s has encountered an error. It was likely caused by running out of memory. " + "If you are sure you have enough RAM on your system, this may also be a bug and we would " + "be glad if you reported it."))) % SLIC3R_APP_NAME).str()); + error = std::string(errmsg.ToUTF8()) + "\n\n" + std::string(ex.what()); + } catch (std::exception &ex) { + error = ex.what(); + } catch (...) { + error = "Unknown C++ exception."; + } + return error; +} + BackgroundSlicingProcess::BackgroundSlicingProcess() { boost::filesystem::path temp_path(wxStandardPaths::Get().GetTempDir().utf8_str().data()); @@ -89,11 +121,17 @@ void BackgroundSlicingProcess::process_fff() { assert(m_print == m_fff_print); m_print->process(); - wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_slicing_completed_id)); - m_fff_print->export_gcode(m_temp_output_path, m_gcode_preview_data, m_thumbnail_cb); - + wxCommandEvent evt(m_event_slicing_completed_id); + evt.SetInt((int)(m_fff_print->step_state_with_timestamp(PrintStep::psBrim).timestamp)); + wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt.Clone()); +#if ENABLE_GCODE_VIEWER + m_fff_print->export_gcode(m_temp_output_path, m_gcode_result, m_thumbnail_cb); +#else + m_fff_print->export_gcode(m_temp_output_path, m_gcode_preview_data, m_thumbnail_cb); +#endif // ENABLE_GCODE_VIEWER if (this->set_step_started(bspsGCodeFinalize)) { if (! m_export_path.empty()) { + wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); //FIXME localize the messages // Perform the final post-processing of the export path by applying the print statistics over the file name. std::string export_path = m_fff_print->print_statistics().finalize_output_path(m_export_path); @@ -101,19 +139,19 @@ void BackgroundSlicingProcess::process_fff() switch (copy_ret_val) { case SUCCESS: break; // no error case FAIL_COPY_FILE: - throw std::runtime_error(_utf8(L("Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?"))); + throw Slic3r::RuntimeError(_utf8(L("Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?"))); break; case FAIL_FILES_DIFFERENT: - throw std::runtime_error((boost::format(_utf8(L("Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp."))) % export_path).str()); + throw Slic3r::RuntimeError((boost::format(_utf8(L("Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp."))) % export_path).str()); break; case FAIL_RENAMING: - throw std::runtime_error((boost::format(_utf8(L("Renaming of the G-code after copying to the selected destination folder has failed. Current path is %1%.tmp. Please try exporting again."))) % export_path).str()); + throw Slic3r::RuntimeError((boost::format(_utf8(L("Renaming of the G-code after copying to the selected destination folder has failed. Current path is %1%.tmp. Please try exporting again."))) % export_path).str()); break; case FAIL_CHECK_ORIGIN_NOT_OPENED: - throw std::runtime_error((boost::format(_utf8(L("Copying of the temporary G-code has finished but the original code at %1% couldn't be opened during copy check. The output G-code is at %2%.tmp."))) % m_temp_output_path % export_path).str()); + throw Slic3r::RuntimeError((boost::format(_utf8(L("Copying of the temporary G-code has finished but the original code at %1% couldn't be opened during copy check. The output G-code is at %2%.tmp."))) % m_temp_output_path % export_path).str()); break; case FAIL_CHECK_TARGET_NOT_OPENED: - throw std::runtime_error((boost::format(_utf8(L("Copying of the temporary G-code has finished but the exported code couldn't be opened during copy check. The output G-code is at %1%.tmp."))) % export_path).str()); + throw Slic3r::RuntimeError((boost::format(_utf8(L("Copying of the temporary G-code has finished but the exported code couldn't be opened during copy check. The output G-code is at %1%.tmp."))) % export_path).str()); break; default: BOOST_LOG_TRIVIAL(warning) << "Unexpected fail code(" << (int)copy_ret_val << ") durring copy_file() to " << export_path << "."; @@ -124,6 +162,7 @@ void BackgroundSlicingProcess::process_fff() run_post_process_scripts(export_path, m_fff_print->config()); m_print->set_status(100, (boost::format(_utf8(L("G-code file exported to %1%"))) % export_path).str()); } else if (! m_upload_job.empty()) { + wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); prepare_upload(); } else { m_print->set_status(100, _utf8(L("Slicing complete"))); @@ -149,6 +188,8 @@ void BackgroundSlicingProcess::process_sla() m_print->process(); if (this->set_step_started(bspsGCodeFinalize)) { if (! m_export_path.empty()) { + wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); + const std::string export_path = m_sla_print->print_statistics().finalize_output_path(m_export_path); Zipper zipper(export_path); @@ -170,6 +211,7 @@ void BackgroundSlicingProcess::process_sla() m_print->set_status(100, (boost::format(_utf8(L("Masked SLA file exported to %1%"))) % export_path).str()); } else if (! m_upload_job.empty()) { + wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); prepare_upload(); } else { m_print->set_status(100, _utf8(L("Slicing complete"))); @@ -198,7 +240,7 @@ void BackgroundSlicingProcess::thread_proc() // Process the background slicing task. m_state = STATE_RUNNING; lck.unlock(); - std::string error; + std::exception_ptr exception; try { assert(m_print != nullptr); switch(m_print->technology()) { @@ -209,15 +251,8 @@ void BackgroundSlicingProcess::thread_proc() } catch (CanceledException & /* ex */) { // Canceled, this is all right. assert(m_print->canceled()); - } catch (const std::bad_alloc& ex) { - wxString errmsg = GUI::from_u8((boost::format(_utf8(L("%s has encountered an error. It was likely caused by running out of memory. " - "If you are sure you have enough RAM on your system, this may also be a bug and we would " - "be glad if you reported it."))) % SLIC3R_APP_NAME).str()); - error = std::string(errmsg.ToUTF8()) + "\n\n" + std::string(ex.what()); - } catch (std::exception &ex) { - error = ex.what(); } catch (...) { - error = "Unknown C++ exception."; + exception = std::current_exception(); } m_print->finalize(); lck.lock(); @@ -225,9 +260,9 @@ void BackgroundSlicingProcess::thread_proc() if (m_print->cancel_status() != Print::CANCELED_INTERNAL) { // Only post the canceled event, if canceled by user. // Don't post the canceled event, if canceled from Print::apply(). - wxCommandEvent evt(m_event_finished_id); - evt.SetString(GUI::from_u8(error)); - evt.SetInt(m_print->canceled() ? -1 : (error.empty() ? 1 : 0)); + SlicingProcessCompletedEvent evt(m_event_finished_id, 0, + (m_state == STATE_CANCELED) ? SlicingProcessCompletedEvent::Cancelled : + exception ? SlicingProcessCompletedEvent::Error : SlicingProcessCompletedEvent::Finished, exception); wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt.Clone()); } m_print->restart(); @@ -287,7 +322,7 @@ bool BackgroundSlicingProcess::start() // The background processing thread is already running. return false; if (! this->idle()) - throw std::runtime_error("Cannot start a background task, the worker thread is not idle."); + throw Slic3r::RuntimeError("Cannot start a background task, the worker thread is not idle."); m_state = STATE_STARTED; m_print->set_cancel_callback([this](){ this->stop_internal(); }); lck.unlock(); @@ -376,6 +411,17 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model &model, const Dyn assert(m_print != nullptr); assert(config.opt_enum("printer_technology") == m_print->technology()); Print::ApplyStatus invalidated = m_print->apply(model, config); +#if ENABLE_GCODE_VIEWER + if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) != 0 && m_print->technology() == ptFFF && + !this->m_fff_print->is_step_done(psGCodeExport)) + { + // Some FFF status was invalidated, and the G-code was not exported yet. + // Let the G-code preview UI know that the final G-code preview is not valid. + // In addition, this early memory deallocation reduces memory footprint. + if (m_gcode_result != nullptr) + m_gcode_result->reset(); + } +#else if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) != 0 && m_print->technology() == ptFFF && m_gcode_preview_data != nullptr && ! this->m_fff_print->is_step_done(psGCodeExport)) { // Some FFF status was invalidated, and the G-code was not exported yet. @@ -383,6 +429,7 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model &model, const Dyn // In addition, this early memory deallocation reduces memory footprint. m_gcode_preview_data->reset(); } +#endif // ENABLE_GCODE_VIEWER return invalidated; } @@ -470,7 +517,7 @@ void BackgroundSlicingProcess::prepare_upload() if (m_print == m_fff_print) { m_print->set_status(95, _utf8(L("Running post-processing scripts"))); if (copy_file(m_temp_output_path, source_path.string()) != SUCCESS) { - throw std::runtime_error(_utf8(L("Copying of the temporary G-code to the output G-code failed"))); + throw Slic3r::RuntimeError(_utf8(L("Copying of the temporary G-code to the output G-code failed"))); } run_post_process_scripts(source_path.string(), m_fff_print->config()); m_upload_job.upload_data.upload_path = m_fff_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string()); diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp index 38e9e10755..1b2687e63a 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp @@ -11,6 +11,9 @@ #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Format/SL1.hpp" #include "slic3r/Utils/PrintHost.hpp" +#if ENABLE_GCODE_VIEWER +#include "libslic3r/GCode/GCodeProcessor.hpp" +#endif // ENABLE_GCODE_VIEWER namespace boost { namespace filesystem { class path; } } @@ -18,7 +21,9 @@ namespace boost { namespace filesystem { class path; } } namespace Slic3r { class DynamicPrintConfig; +#if !ENABLE_GCODE_VIEWER class GCodePreviewData; +#endif // !ENABLE_GCODE_VIEWER class Model; class SLAPrint; @@ -32,6 +37,36 @@ public: PrintBase::SlicingStatus status; }; +class SlicingProcessCompletedEvent : public wxEvent +{ +public: + enum StatusType { + Finished, + Cancelled, + Error + }; + + SlicingProcessCompletedEvent(wxEventType eventType, int winid, StatusType status, std::exception_ptr exception) : + wxEvent(winid, eventType), m_status(status), m_exception(exception) {} + virtual wxEvent* Clone() const { return new SlicingProcessCompletedEvent(*this); } + + StatusType status() const { return m_status; } + bool finished() const { return m_status == Finished; } + bool success() const { return m_status == Finished; } + bool cancelled() const { return m_status == Cancelled; } + bool error() const { return m_status == Error; } + // Unhandled error produced by stdlib or a Win32 structured exception, or unhandled Slic3r's own critical exception. + bool critical_error() const; + // Only valid if error() + void rethrow_exception() const { assert(this->error()); assert(m_exception); std::rethrow_exception(m_exception); } + // Produce a human readable message to be displayed by a notification or a message box. + std::string format_error_message() const; + +private: + StatusType m_status; + std::exception_ptr m_exception; +}; + wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); // Print step IDs for keeping track of the print state. @@ -50,8 +85,12 @@ public: void set_fff_print(Print *print) { m_fff_print = print; } void set_sla_print(SLAPrint *print) { m_sla_print = print; m_sla_print->set_printer(&m_sla_archive); } - void set_gcode_preview_data(GCodePreviewData *gpd) { m_gcode_preview_data = gpd; } - void set_thumbnail_cb(ThumbnailsGeneratorCallback cb) { m_thumbnail_cb = cb; } + void set_thumbnail_cb(ThumbnailsGeneratorCallback cb) { m_thumbnail_cb = cb; } +#if ENABLE_GCODE_VIEWER + void set_gcode_result(GCodeProcessor::Result* result) { m_gcode_result = result; } +#else + void set_gcode_preview_data(GCodePreviewData* gpd) { m_gcode_preview_data = gpd; } +#endif // ENABLE_GCODE_VIEWER // The following wxCommandEvent will be sent to the UI thread / Plater window, when the slicing is finished // and the background processing will transition into G-code export. @@ -60,6 +99,10 @@ public: // The following wxCommandEvent will be sent to the UI thread / Plater window, when the G-code export is finished. // The wxCommandEvent is sent to the UI thread asynchronously without waiting for the event to be processed. void set_finished_event(int event_id) { m_event_finished_id = event_id; } + // The following wxCommandEvent will be sent to the UI thread / Plater window, when the G-code is being exported to + // specified path or uploaded. + // The wxCommandEvent is sent to the UI thread asynchronously without waiting for the event to be processed. + void set_export_began_event(int event_id) { m_event_export_began_id = event_id; } // Activate either m_fff_print or m_sla_print. // Return true if changed. @@ -153,12 +196,17 @@ private: // Non-owned pointers to Print instances. Print *m_fff_print = nullptr; SLAPrint *m_sla_print = nullptr; +#if ENABLE_GCODE_VIEWER + // Data structure, to which the G-code export writes its annotations. + GCodeProcessor::Result *m_gcode_result = nullptr; +#else // Data structure, to which the G-code export writes its annotations. GCodePreviewData *m_gcode_preview_data = nullptr; - // Callback function, used to write thumbnails into gcode. - ThumbnailsGeneratorCallback m_thumbnail_cb = nullptr; - SL1Archive m_sla_archive; - // Temporary G-code, there is one defined for the BackgroundSlicingProcess, differentiated from the other processes by a process ID. +#endif // ENABLE_GCODE_VIEWER + // Callback function, used to write thumbnails into gcode. + ThumbnailsGeneratorCallback m_thumbnail_cb = nullptr; + SL1Archive m_sla_archive; + // Temporary G-code, there is one defined for the BackgroundSlicingProcess, differentiated from the other processes by a process ID. std::string m_temp_output_path; // Output path provided by the user. The output path may be set even if the slicing is running, // but once set, it cannot be re-set. @@ -190,6 +238,9 @@ private: int m_event_slicing_completed_id = 0; // wxWidgets command ID to be sent to the plater to inform that the task finished. int m_event_finished_id = 0; + // wxWidgets command ID to be sent to the plater to inform that the G-code is being exported. + int m_event_export_began_id = 0; + }; }; // namespace Slic3r diff --git a/src/slic3r/GUI/BedShapeDialog.cpp b/src/slic3r/GUI/BedShapeDialog.cpp index f08b1a3794..2fc7d10366 100644 --- a/src/slic3r/GUI/BedShapeDialog.cpp +++ b/src/slic3r/GUI/BedShapeDialog.cpp @@ -21,6 +21,160 @@ namespace Slic3r { namespace GUI { +BedShape::BedShape(const ConfigOptionPoints& points) +{ + auto polygon = Polygon::new_scale(points.values); + + // is this a rectangle ? + if (points.size() == 4) { + auto lines = polygon.lines(); + if (lines[0].parallel_to(lines[2]) && lines[1].parallel_to(lines[3])) { + // okay, it's a rectangle + // find origin + coordf_t x_min, x_max, y_min, y_max; + x_max = x_min = points.values[0](0); + y_max = y_min = points.values[0](1); + for (auto pt : points.values) + { + x_min = std::min(x_min, pt(0)); + x_max = std::max(x_max, pt(0)); + y_min = std::min(y_min, pt(1)); + y_max = std::max(y_max, pt(1)); + } + + m_type = Type::Rectangular; + m_rectSize = Vec2d(x_max - x_min, y_max - y_min); + m_rectOrigin = Vec2d(-x_min, -y_min); + + return; + } + } + + // is this a circle ? + { + // Analyze the array of points.Do they reside on a circle ? + auto center = polygon.bounding_box().center(); + std::vector vertex_distances; + double avg_dist = 0; + for (auto pt : polygon.points) + { + double distance = (pt - center).cast().norm(); + vertex_distances.push_back(distance); + avg_dist += distance; + } + + avg_dist /= vertex_distances.size(); + bool defined_value = true; + for (auto el : vertex_distances) + { + if (abs(el - avg_dist) > 10 * SCALED_EPSILON) + defined_value = false; + break; + } + if (defined_value) { + // all vertices are equidistant to center + m_type = Type::Circular; + m_diameter = unscale(avg_dist * 2); + + return; + } + } + + if (points.size() < 3) + return; + + // This is a custom bed shape, use the polygon provided. + m_type = Type::Custom; +} + +static std::string get_option_label(BedShape::Parameter param) +{ + switch (param) { + case BedShape::Parameter::RectSize : return L("Size"); + case BedShape::Parameter::RectOrigin: return L("Origin"); + case BedShape::Parameter::Diameter : return L("Diameter"); + default: return ""; + } +} + +void BedShape::append_option_line(ConfigOptionsGroupShp optgroup, Parameter param) +{ + ConfigOptionDef def; + + if (param == Parameter::RectSize) { + def.type = coPoints; + def.set_default_value(new ConfigOptionPoints{ Vec2d(200, 200) }); + def.min = 0; + def.max = 1200; + def.label = get_option_label(param); + def.tooltip = L("Size in X and Y of the rectangular plate."); + + Option option(def, "rect_size"); + optgroup->append_single_option_line(option); + } + else if (param == Parameter::RectOrigin) { + def.type = coPoints; + def.set_default_value(new ConfigOptionPoints{ Vec2d(0, 0) }); + def.min = -600; + def.max = 600; + def.label = get_option_label(param); + def.tooltip = L("Distance of the 0,0 G-code coordinate from the front left corner of the rectangle."); + + Option option(def, "rect_origin"); + optgroup->append_single_option_line(option); + } + else if (param == Parameter::Diameter) { + def.type = coFloat; + def.set_default_value(new ConfigOptionFloat(200)); + def.sidetext = L("mm"); + def.label = get_option_label(param); + def.tooltip = L("Diameter of the print bed. It is assumed that origin (0,0) is located in the center."); + + Option option(def, "diameter"); + optgroup->append_single_option_line(option); + } +} + +wxString BedShape::get_name(Type type) +{ + switch (type) { + case Type::Rectangular : return _L("Rectangular"); + case Type::Circular : return _L("Circular"); + case Type::Custom : return _L("Custom"); + case Type::Invalid : + default : return _L("Invalid"); + } +} + +size_t BedShape::get_type() +{ + return static_cast(m_type == Type::Invalid ? Type::Rectangular : m_type); +} + +wxString BedShape::get_full_name_with_params() +{ + wxString out = _L("Shape") + ": " + get_name(m_type); + + if (m_type == Type::Rectangular) { + out += "\n" + _(get_option_label(Parameter::RectSize)) + ": [" + ConfigOptionPoint(m_rectSize).serialize() + "]"; + out += "\n" + _(get_option_label(Parameter::RectOrigin))+ ": [" + ConfigOptionPoint(m_rectOrigin).serialize() + "]"; + } + else if (m_type == Type::Circular) + out += "\n" + _L(get_option_label(Parameter::Diameter)) + ": [" + double_to_string(m_diameter) + "]"; + + return out; +} + +void BedShape::apply_optgroup_values(ConfigOptionsGroupShp optgroup) +{ + if (m_type == Type::Rectangular || m_type == Type::Invalid) { + optgroup->set_value("rect_size" , new ConfigOptionPoints{ m_rectSize }); + optgroup->set_value("rect_origin" , new ConfigOptionPoints{ m_rectOrigin }); + } + else if (m_type == Type::Circular) + optgroup->set_value("diameter", double_to_string(m_diameter)); +} + void BedShapeDialog::build_dialog(const ConfigOptionPoints& default_pt, const ConfigOptionString& custom_texture, const ConfigOptionString& custom_model) { SetFont(wxGetApp().normal_font()); @@ -72,50 +226,28 @@ void BedShapePanel::build_panel(const ConfigOptionPoints& default_pt, const Conf m_shape_options_book = new wxChoicebook(this, wxID_ANY, wxDefaultPosition, wxSize(25*wxGetApp().em_unit(), -1), wxCHB_TOP); sbsizer->Add(m_shape_options_book); - auto optgroup = init_shape_options_page(_(L("Rectangular"))); - ConfigOptionDef def; - def.type = coPoints; - def.set_default_value(new ConfigOptionPoints{ Vec2d(200, 200) }); - def.min = 0; - def.max = 1200; - def.label = L("Size"); - def.tooltip = L("Size in X and Y of the rectangular plate."); - Option option(def, "rect_size"); - optgroup->append_single_option_line(option); + auto optgroup = init_shape_options_page(BedShape::get_name(BedShape::Type::Rectangular)); + BedShape::append_option_line(optgroup, BedShape::Parameter::RectSize); + BedShape::append_option_line(optgroup, BedShape::Parameter::RectOrigin); - def.type = coPoints; - def.set_default_value(new ConfigOptionPoints{ Vec2d(0, 0) }); - def.min = -600; - def.max = 600; - def.label = L("Origin"); - def.tooltip = L("Distance of the 0,0 G-code coordinate from the front left corner of the rectangle."); - option = Option(def, "rect_origin"); - optgroup->append_single_option_line(option); + optgroup = init_shape_options_page(BedShape::get_name(BedShape::Type::Circular)); + BedShape::append_option_line(optgroup, BedShape::Parameter::Diameter); - optgroup = init_shape_options_page(_(L("Circular"))); - def.type = coFloat; - def.set_default_value(new ConfigOptionFloat(200)); - def.sidetext = L("mm"); - def.label = L("Diameter"); - def.tooltip = L("Diameter of the print bed. It is assumed that origin (0,0) is located in the center."); - option = Option(def, "diameter"); - optgroup->append_single_option_line(option); + optgroup = init_shape_options_page(BedShape::get_name(BedShape::Type::Custom)); - optgroup = init_shape_options_page(_(L("Custom"))); Line line{ "", "" }; line.full_width = 1; line.widget = [this](wxWindow* parent) { - wxButton* shape_btn = new wxButton(parent, wxID_ANY, _(L("Load shape from STL..."))); + wxButton* shape_btn = new wxButton(parent, wxID_ANY, _L("Load shape from STL...")); wxSizer* shape_sizer = new wxBoxSizer(wxHORIZONTAL); shape_sizer->Add(shape_btn, 1, wxEXPAND); wxSizer* sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(shape_sizer, 1, wxEXPAND); - shape_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent& e) - { + shape_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { load_stl(); - })); + }); return sizer; }; @@ -149,10 +281,6 @@ void BedShapePanel::build_panel(const ConfigOptionPoints& default_pt, const Conf update_preview(); } -#define SHAPE_RECTANGULAR 0 -#define SHAPE_CIRCULAR 1 -#define SHAPE_CUSTOM 2 - // Called from the constructor. // Create a panel for a rectangular / circular / custom bed shape. ConfigOptionsGroupShp BedShapePanel::init_shape_options_page(const wxString& title) @@ -337,83 +465,18 @@ wxPanel* BedShapePanel::init_model_panel() // with the list of points in the ini file directly. void BedShapePanel::set_shape(const ConfigOptionPoints& points) { - auto polygon = Polygon::new_scale(points.values); + BedShape shape(points); - // is this a rectangle ? - if (points.size() == 4) { - auto lines = polygon.lines(); - if (lines[0].parallel_to(lines[2]) && lines[1].parallel_to(lines[3])) { - // okay, it's a rectangle - // find origin - coordf_t x_min, x_max, y_min, y_max; - x_max = x_min = points.values[0](0); - y_max = y_min = points.values[0](1); - for (auto pt : points.values) - { - x_min = std::min(x_min, pt(0)); - x_max = std::max(x_max, pt(0)); - y_min = std::min(y_min, pt(1)); - y_max = std::max(y_max, pt(1)); - } + m_shape_options_book->SetSelection(shape.get_type()); + shape.apply_optgroup_values(m_optgroups[shape.get_type()]); - auto origin = new ConfigOptionPoints{ Vec2d(-x_min, -y_min) }; + // Copy the polygon to the canvas, make a copy of the array, if custom shape is selected + if (shape.is_custom()) + m_loaded_shape = points.values; - m_shape_options_book->SetSelection(SHAPE_RECTANGULAR); - auto optgroup = m_optgroups[SHAPE_RECTANGULAR]; - optgroup->set_value("rect_size", new ConfigOptionPoints{ Vec2d(x_max - x_min, y_max - y_min) });//[x_max - x_min, y_max - y_min]); - optgroup->set_value("rect_origin", origin); - update_shape(); - return; - } - } - - // is this a circle ? - { - // Analyze the array of points.Do they reside on a circle ? - auto center = polygon.bounding_box().center(); - std::vector vertex_distances; - double avg_dist = 0; - for (auto pt: polygon.points) - { - double distance = (pt - center).cast().norm(); - vertex_distances.push_back(distance); - avg_dist += distance; - } - - avg_dist /= vertex_distances.size(); - bool defined_value = true; - for (auto el: vertex_distances) - { - if (abs(el - avg_dist) > 10 * SCALED_EPSILON) - defined_value = false; - break; - } - if (defined_value) { - // all vertices are equidistant to center - m_shape_options_book->SetSelection(SHAPE_CIRCULAR); - auto optgroup = m_optgroups[SHAPE_CIRCULAR]; - boost::any ret = wxNumberFormatter::ToString(unscale(avg_dist * 2), 0); - optgroup->set_value("diameter", ret); - update_shape(); - return; - } - } - - if (points.size() < 3) { - // Invalid polygon.Revert to default bed dimensions. - m_shape_options_book->SetSelection(SHAPE_RECTANGULAR); - auto optgroup = m_optgroups[SHAPE_RECTANGULAR]; - optgroup->set_value("rect_size", new ConfigOptionPoints{ Vec2d(200, 200) }); - optgroup->set_value("rect_origin", new ConfigOptionPoints{ Vec2d(0, 0) }); - update_shape(); - return; - } - - // This is a custom bed shape, use the polygon provided. - m_shape_options_book->SetSelection(SHAPE_CUSTOM); - // Copy the polygon to the canvas, make a copy of the array. - m_loaded_shape = points.values; update_shape(); + + return; } void BedShapePanel::update_preview() @@ -426,21 +489,20 @@ void BedShapePanel::update_preview() void BedShapePanel::update_shape() { auto page_idx = m_shape_options_book->GetSelection(); - if (page_idx == SHAPE_RECTANGULAR) { + auto opt_group = m_optgroups[page_idx]; + + BedShape::Type page_type = static_cast(page_idx); + + if (page_type == BedShape::Type::Rectangular) { Vec2d rect_size(Vec2d::Zero()); Vec2d rect_origin(Vec2d::Zero()); - try{ - rect_size = boost::any_cast(m_optgroups[SHAPE_RECTANGULAR]->get_value("rect_size")); } - catch (const std::exception & /* e */) { - return; - } - try { - rect_origin = boost::any_cast(m_optgroups[SHAPE_RECTANGULAR]->get_value("rect_origin")); - } - catch (const std::exception & /* e */) { - return; - } - + + try { rect_size = boost::any_cast(opt_group->get_value("rect_size")); } + catch (const std::exception& /* e */) { return; } + + try { rect_origin = boost::any_cast(opt_group->get_value("rect_origin")); } + catch (const std::exception & /* e */) { return; } + auto x = rect_size(0); auto y = rect_size(1); // empty strings or '-' or other things @@ -462,14 +524,11 @@ void BedShapePanel::update_shape() Vec2d(x1, y1), Vec2d(x0, y1) }; } - else if(page_idx == SHAPE_CIRCULAR) { + else if (page_type == BedShape::Type::Circular) { double diameter; - try{ - diameter = boost::any_cast(m_optgroups[SHAPE_CIRCULAR]->get_value("diameter")); - } - catch (const std::exception & /* e */) { - return; - } + try { diameter = boost::any_cast(opt_group->get_value("diameter")); } + catch (const std::exception & /* e */) { return; } + if (diameter == 0.0) return ; auto r = diameter / 2; auto twopi = 2 * PI; @@ -481,7 +540,7 @@ void BedShapePanel::update_shape() } m_shape = points; } - else if (page_idx == SHAPE_CUSTOM) + else if (page_type == BedShape::Type::Custom) m_shape = m_loaded_shape; update_preview(); diff --git a/src/slic3r/GUI/BedShapeDialog.hpp b/src/slic3r/GUI/BedShapeDialog.hpp index b583eca4a1..2cfbc73aec 100644 --- a/src/slic3r/GUI/BedShapeDialog.hpp +++ b/src/slic3r/GUI/BedShapeDialog.hpp @@ -16,6 +16,42 @@ namespace GUI { class ConfigOptionsGroup; using ConfigOptionsGroupShp = std::shared_ptr; + +struct BedShape +{ + enum class Type { + Rectangular = 0, + Circular, + Custom, + Invalid + }; + + enum class Parameter { + RectSize, + RectOrigin, + Diameter + }; + + BedShape(const ConfigOptionPoints& points); + + bool is_custom() { return m_type == Type::Custom; } + + static void append_option_line(ConfigOptionsGroupShp optgroup, Parameter param); + static wxString get_name(Type type); + + // convert Type to size_t + size_t get_type(); + + wxString get_full_name_with_params(); + void apply_optgroup_values(ConfigOptionsGroupShp optgroup); + +private: + Type m_type {Type::Invalid}; + Vec2d m_rectSize {200, 200}; + Vec2d m_rectOrigin {0, 0}; + double m_diameter {0}; +}; + class BedShapePanel : public wxPanel { static const std::string NONE; diff --git a/src/slic3r/GUI/BitmapCache.cpp b/src/slic3r/GUI/BitmapCache.cpp index 74a4bc1c87..cc4c831ae9 100644 --- a/src/slic3r/GUI/BitmapCache.cpp +++ b/src/slic3r/GUI/BitmapCache.cpp @@ -3,6 +3,9 @@ #include "libslic3r/Utils.hpp" #include "../Utils/MacDarkMode.hpp" #include "GUI.hpp" +#if ENABLE_GCODE_VIEWER +#include "GUI_Utils.hpp" +#endif // ENABLE_GCODE_VIEWER #include @@ -355,6 +358,7 @@ wxBitmap BitmapCache::mksolid(size_t width, size_t height, unsigned char r, unsi } +#if !ENABLE_GCODE_VIEWER static inline int hex_digit_to_int(const char c) { return @@ -362,6 +366,7 @@ static inline int hex_digit_to_int(const char c) (c >= 'A' && c <= 'F') ? int(c - 'A') + 10 : (c >= 'a' && c <= 'f') ? int(c - 'a') + 10 : -1; } +#endif // !ENABLE_GCODE_VIEWER bool BitmapCache::parse_color(const std::string& scolor, unsigned char* rgb_out) { diff --git a/src/slic3r/GUI/Camera.cpp b/src/slic3r/GUI/Camera.cpp index ac32767c4b..3bd22590f5 100644 --- a/src/slic3r/GUI/Camera.cpp +++ b/src/slic3r/GUI/Camera.cpp @@ -1,10 +1,11 @@ #include "libslic3r/libslic3r.h" +#include "libslic3r/AppConfig.hpp" #include "Camera.hpp" #include "GUI_App.hpp" -#include "AppConfig.hpp" #if ENABLE_CAMERA_STATISTICS #include "Mouse3DController.hpp" +#include "Plater.hpp" #endif // ENABLE_CAMERA_STATISTICS #include diff --git a/src/slic3r/GUI/Camera.hpp b/src/slic3r/GUI/Camera.hpp index ece999c078..6e42562351 100644 --- a/src/slic3r/GUI/Camera.hpp +++ b/src/slic3r/GUI/Camera.hpp @@ -84,6 +84,7 @@ public: double get_near_z() const { return m_frustrum_zs.first; } double get_far_z() const { return m_frustrum_zs.second; } + const std::pair& get_z_range() const { return m_frustrum_zs; } double get_fov() const; diff --git a/src/slic3r/GUI/ConfigExceptions.hpp b/src/slic3r/GUI/ConfigExceptions.hpp index 9038d3445e..181442d4e3 100644 --- a/src/slic3r/GUI/ConfigExceptions.hpp +++ b/src/slic3r/GUI/ConfigExceptions.hpp @@ -1,15 +1,15 @@ #include namespace Slic3r { -class ConfigError : public std::runtime_error { -using std::runtime_error::runtime_error; +class ConfigError : public Slic3r::RuntimeError { + using Slic3r::RuntimeError::RuntimeError; }; namespace GUI { class ConfigGUITypeError : public ConfigError { -using ConfigError::ConfigError; + using ConfigError::ConfigError; }; -} -} +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index a0df4c6598..5836b8a2c0 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -2,7 +2,7 @@ #include "ConfigManipulation.hpp" #include "I18N.hpp" #include "GUI_App.hpp" -#include "PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" #include @@ -353,6 +353,7 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) toggle_field("support_head_penetration", supports_en); toggle_field("support_head_width", supports_en); toggle_field("support_pillar_diameter", supports_en); + toggle_field("support_small_pillar_diameter_percent", supports_en); toggle_field("support_max_bridges_on_pillar", supports_en); toggle_field("support_pillar_connection_mode", supports_en); toggle_field("support_buildplate_only", supports_en); diff --git a/src/slic3r/GUI/ConfigSnapshotDialog.cpp b/src/slic3r/GUI/ConfigSnapshotDialog.cpp index 6a44b96dc6..4855bea81e 100644 --- a/src/slic3r/GUI/ConfigSnapshotDialog.cpp +++ b/src/slic3r/GUI/ConfigSnapshotDialog.cpp @@ -114,7 +114,7 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db // text html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO); { - wxFont font = wxGetApp().normal_font();//wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + wxFont font = get_default_font_for_dpi(get_dpi_for_window(this));// wxGetApp().normal_font();//wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); #ifdef __WXMSW__ const int fs = font.GetPointSize(); const int fs1 = static_cast(0.8f*fs); @@ -140,7 +140,7 @@ ConfigSnapshotDialog::ConfigSnapshotDialog(const Config::SnapshotDB &snapshot_db void ConfigSnapshotDialog::on_dpi_changed(const wxRect &suggested_rect) { - wxFont font = GetFont(); + wxFont font = get_default_font_for_dpi(get_dpi_for_window(this));// GetFont(); const int fs = font.GetPointSize(); const int fs1 = static_cast(0.8f*fs); const int fs2 = static_cast(1.1f*fs); diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 60323976c8..c98b736b7c 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -122,7 +123,7 @@ Bundle& BundleMap::prusa_bundle() { auto it = find(PresetBundle::PRUSA_BUNDLE); if (it == end()) { - throw std::runtime_error("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); + throw Slic3r::RuntimeError("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); } return it->second; @@ -561,30 +562,37 @@ const std::string PageMaterials::EMPTY; PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) : ConfigWizardPage(parent, std::move(title), std::move(shortname)) , materials(materials) - , list_l1(new StringList(this)) - , list_l2(new StringList(this)) - , list_l3(new PresetList(this)) + , list_printer(new StringList(this, wxLB_MULTIPLE)) + , list_type(new StringList(this)) + , list_vendor(new StringList(this)) + , list_profile(new PresetList(this)) + , compatible_printers(new wxStaticText(this, wxID_ANY, _(L("")))) { append_spacer(VERTICAL_SPACING); const int em = parent->em_unit(); const int list_h = 30*em; - list_l1->SetMinSize(wxSize(8*em, list_h)); - list_l2->SetMinSize(wxSize(13*em, list_h)); - list_l3->SetMinSize(wxSize(25*em, list_h)); + list_printer->SetWindowStyle(wxLB_EXTENDED); - auto *grid = new wxFlexGridSizer(3, em/2, em); - grid->AddGrowableCol(2, 1); + list_printer->SetMinSize(wxSize(23*em, list_h)); + list_type->SetMinSize(wxSize(8*em, list_h)); + list_vendor->SetMinSize(wxSize(13*em, list_h)); + list_profile->SetMinSize(wxSize(23*em, list_h)); + + grid = new wxFlexGridSizer(4, em/2, em); + grid->AddGrowableCol(3, 1); grid->AddGrowableRow(1, 1); + grid->Add(new wxStaticText(this, wxID_ANY, _(L("Printer:")))); grid->Add(new wxStaticText(this, wxID_ANY, list1name)); grid->Add(new wxStaticText(this, wxID_ANY, _(L("Vendor:")))); grid->Add(new wxStaticText(this, wxID_ANY, _(L("Profile:")))); - grid->Add(list_l1, 0, wxEXPAND); - grid->Add(list_l2, 0, wxEXPAND); - grid->Add(list_l3, 1, wxEXPAND); + grid->Add(list_printer, 0, wxEXPAND); + grid->Add(list_type, 0, wxEXPAND); + grid->Add(list_vendor, 0, wxEXPAND); + grid->Add(list_profile, 1, wxEXPAND); auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); auto *sel_all = new wxButton(this, wxID_ANY, _(L("All"))); @@ -592,121 +600,342 @@ PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxStrin btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); btn_sizer->Add(sel_none); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); grid->Add(new wxBoxSizer(wxHORIZONTAL)); grid->Add(btn_sizer, 0, wxALIGN_RIGHT); + + auto* notes_sizer = new wxBoxSizer(wxHORIZONTAL); + notes_sizer->Add(compatible_printers); + grid->Add(notes_sizer); + append(grid, 1, wxEXPAND); - list_l1->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_l1->GetSelection(), list_l2->GetSelection()); + list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { + update_lists(evt.GetInt(), list_type->GetSelection(), list_vendor->GetSelection()); + }); + list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_printer->GetSelection(), list_type->GetSelection(), list_vendor->GetSelection()); }); - list_l2->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_l1->GetSelection(), list_l2->GetSelection()); + list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_printer->GetSelection(), list_type->GetSelection(), list_vendor->GetSelection()); }); - list_l3->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); + list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); + list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); + Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); + + list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); + list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); + list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); + reload_presets(); } - +void PageMaterials::on_paint() +{ + if (first_paint) { + first_paint = false; + prepare_compatible_printers_label(); + } +} +void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) +{ + const wxClientDC dc(list_profile); + const wxPoint pos = evt.GetLogicalPosition(dc); + int item = list_profile->HitTest(pos); + BOOST_LOG_TRIVIAL(error) << "hit test: " << item; + on_material_hovered(item); +} +void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) +{} +void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) +{ + on_material_hovered(-1); +} void PageMaterials::reload_presets() { clear(); - list_l1->append(_(L("(All)")), &EMPTY); + list_printer->append(_(L("(All)")), &EMPTY); + list_printer->SetLabelMarkup("bald"); + for (const Preset* printer : materials->printers) { + list_printer->append(printer->name, &printer->name); + } - for (const std::string &type : materials->types) { - list_l1->append(type, &type); - } - - if (list_l1->GetCount() > 0) { - list_l1->SetSelection(0); - sel1_prev = wxNOT_FOUND; - sel2_prev = wxNOT_FOUND; - update_lists(0, 0); + if (list_printer->GetCount() > 0) { + list_printer->SetSelection(0); + sel_printer_prev = wxNOT_FOUND; + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + update_lists(0, 0, 0); } presets_loaded = true; } -void PageMaterials::update_lists(int sel1, int sel2) +void PageMaterials::prepare_compatible_printers_label() { - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - if (sel1 != sel1_prev) { - // Refresh the second list - - // XXX: The vendor list is created with quadratic complexity here, - // but the number of vendors is going to be very small this shouldn't be a problem. - - list_l2->Clear(); - list_l2->append(_(L("(All)")), &EMPTY); - if (sel1 != wxNOT_FOUND) { - const std::string &type = list_l1->get_data(sel1); - - materials->filter_presets(type, EMPTY, [this](const Preset *p) { - const std::string &vendor = this->materials->get_vendor(p); - - if (list_l2->find(vendor) == wxNOT_FOUND) { - list_l2->append(vendor, &vendor); - } - }); - } - - sel1_prev = sel1; - sel2 = 0; - sel2_prev = wxNOT_FOUND; - list_l2->SetSelection(sel2); - list_l3->Clear(); + assert(grid->GetColWidths().size() == 4); + compatible_printers_width = grid->GetColWidths()[3]; + empty_printers_label = "Compatible printers:"; + for (const Preset* printer : materials->printers) { + empty_printers_label += "\n"; } + clear_compatible_printers_label(); +} - if (sel2 != sel2_prev) { - // Refresh the third list +void PageMaterials::clear_compatible_printers_label() +{ + compatible_printers->SetLabel(boost::nowide::widen(empty_printers_label)); + compatible_printers->Wrap(compatible_printers_width); + Layout(); +} - list_l3->Clear(); - if (sel1 != wxNOT_FOUND && sel2 != wxNOT_FOUND) { - const std::string &type = list_l1->get_data(sel1); - const std::string &vendor = list_l2->get_data(sel2); - - materials->filter_presets(type, vendor, [this](const Preset *p) { - bool was_checked = false; - - int cur_i = list_l3->find(p->alias); - if (cur_i == wxNOT_FOUND) - cur_i = list_l3->append(p->alias, &p->alias); +void PageMaterials::on_material_hovered(int sel_material) +{ + if ( sel_material == last_hovered_item) + return; + if (sel_material == -1) { + clear_compatible_printers_label(); + return; + } + last_hovered_item = sel_material; + std::string compatible_printers_label = "compatible printers:\n"; + //selected material string + std::string material_name = list_profile->get_data(sel_material); + // get material preset + const std::vector matching_materials = materials->get_presets_by_alias(material_name); + if (matching_materials.empty()) + { + clear_compatible_printers_label(); + return; + } + //find matching printers + bool first = true; + for (const Preset* printer : materials->printers) { + bool compatible = false; + for (const Preset* material : matching_materials) { + if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { + if (first) + first = false; else - was_checked = list_l3->IsChecked(cur_i); - - const std::string& section = materials->appconfig_section(); - - const bool checked = wizard_p()->appconfig_new.has(section, p->name); - list_l3->Check(cur_i, checked | was_checked); - - /* Update preset selection in config. - * If one preset from aliases bundle is selected, - * than mark all presets with this aliases as selected - * */ - if (checked && !was_checked) - wizard_p()->update_presets_in_config(section, p->alias, true); - else if (!checked && was_checked) - wizard_p()->appconfig_new.set(section, p->name, "1"); - } ); + compatible_printers_label += "\n";//", "; + compatible_printers_label += printer->name; + compatible = true; + break; + } } - - sel2_prev = sel2; } + this->compatible_printers->SetLabel(boost::nowide::widen(compatible_printers_label)); + this->compatible_printers->Wrap(compatible_printers_width); +} + +void PageMaterials::on_material_highlighted(int sel_material) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + //std::string compatible_printers_label = "compatible printers:\n"; + //std::string empty_suplement = std::string(); + //unselect all printers + list_printer->SetSelection(wxNOT_FOUND); + //selected material string + std::string material_name = list_profile->get_data(sel_material); + // get material preset + const std::vector matching_materials = materials->get_presets_by_alias(material_name); + if (matching_materials.empty()) + return; + //find matching printers + //bool first = true; + for (const Preset* printer : materials->printers) { + bool compatible = false; + for (const Preset* material : matching_materials) { + if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { + //select printer + int index = list_printer->find(printer->name); + list_printer->SetSelection(index); + /*if (first) + first = false; + else + compatible_printers_label += "\n";//", "; + compatible_printers_label += printer->name; + compatible = true; + break;*/ + } + } + //if(!compatible) + // empty_suplement += std::string(printer->name.length() + 2, ' '); + } + // fill rest of label with blanks so it maintains legth + //compatible_printers_label += empty_suplement; + + update_lists(0,0,0); + list_profile->SetSelection(list_profile->find(material_name)); + + //this->compatible_printers->SetLabel(boost::nowide::widen(compatible_printers_label)); + //this->compatible_printers->Wrap(compatible_printers_width); + //Refresh(); +} + +void PageMaterials::update_lists(int sel_printer, int sel_type, int sel_vendor) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + wxArrayInt sel_printers; + int sel_printers_count = list_printer->GetSelections(sel_printers); + + if (sel_printers_count != sel_printer_prev) { + // Refresh type list + list_type->Clear(); + list_type->append(_(L("(All)")), &EMPTY); + if (sel_printers_count > 0) { + // If all is selected with other printers + // unselect "all" or all printers depending on last value + if (sel_printers[0] == 0 && sel_printers_count > 1) { + if (sel_printer == 0) { + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + } else { + list_printer->SetSelection(0, false); + sel_printers_count = list_printer->GetSelections(sel_printers); + } + } + if (sel_printers[0] != 0) { + for (size_t i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + } else { + //clear selection except "ALL" + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + + materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + + } + + sel_printer_prev = sel_printers_count; + sel_type = 0; + sel_type_prev = wxNOT_FOUND; + list_type->SetSelection(sel_type); + list_profile->Clear(); + } + + if (sel_type != sel_type_prev) { + // Refresh vendor list + + // XXX: The vendor list is created with quadratic complexity here, + // but the number of vendors is going to be very small this shouldn't be a problem. + + list_vendor->Clear(); + list_vendor->append(_(L("(All)")), &EMPTY); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + // find printer preset + for (size_t i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) { + const std::string& vendor = this->materials->get_vendor(p); + if (list_vendor->find(vendor) == wxNOT_FOUND) { + list_vendor->append(vendor, &vendor); + } + }); + } + } + + sel_type_prev = sel_type; + sel_vendor = 0; + sel_vendor_prev = wxNOT_FOUND; + list_vendor->SetSelection(sel_vendor); + list_profile->Clear(); + } + + if (sel_vendor != sel_vendor_prev) { + // Refresh material list + + list_profile->Clear(); + clear_compatible_printers_label(); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + const std::string& vendor = list_vendor->get_data(sel_vendor); + // finst printer preset + for (size_t i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + + materials->filter_presets(printer, type, vendor, [this](const Preset* p) { + bool was_checked = false; + //size_t printer_counter = materials->get_printer_counter(p); + int cur_i = list_profile->find(p->alias); + if (cur_i == wxNOT_FOUND) + //cur_i = list_profile->append(p->alias + " " + std::to_string(printer_counter)/*+ (omnipresent ? "" : " ONLY SOME PRINTERS")*/, &p->alias); + cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias); + else + was_checked = list_profile->IsChecked(cur_i); + + const std::string& section = materials->appconfig_section(); + + const bool checked = wizard_p()->appconfig_new.has(section, p->name); + list_profile->Check(cur_i, checked | was_checked); + + /* Update preset selection in config. + * If one preset from aliases bundle is selected, + * than mark all presets with this aliases as selected + * */ + if (checked && !was_checked) + wizard_p()->update_presets_in_config(section, p->alias, true); + else if (!checked && was_checked) + wizard_p()->appconfig_new.set(section, p->name, "1"); + }); + } + } + + sel_vendor_prev = sel_vendor; + } } void PageMaterials::select_material(int i) { - const bool checked = list_l3->IsChecked(i); + const bool checked = list_profile->IsChecked(i); - const std::string& alias_key = list_l3->get_data(i); + const std::string& alias_key = list_profile->get_data(i); wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); } @@ -715,10 +944,10 @@ void PageMaterials::select_all(bool select) wxWindowUpdateLocker freeze_guard(this); (void)freeze_guard; - for (unsigned i = 0; i < list_l3->GetCount(); i++) { - const bool current = list_l3->IsChecked(i); + for (unsigned i = 0; i < list_profile->GetCount(); i++) { + const bool current = list_profile->IsChecked(i); if (current != select) { - list_l3->Check(i, select); + list_profile->Check(i, select); select_material(i); } } @@ -726,11 +955,13 @@ void PageMaterials::select_all(bool select) void PageMaterials::clear() { - list_l1->Clear(); - list_l2->Clear(); - list_l3->Clear(); - sel1_prev = wxNOT_FOUND; - sel2_prev = wxNOT_FOUND; + list_printer->Clear(); + list_type->Clear(); + list_vendor->Clear(); + list_profile->Clear(); + sel_printer_prev = wxNOT_FOUND; + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; presets_loaded = false; } @@ -740,6 +971,7 @@ void PageMaterials::on_activate() wizard_p()->update_materials(materials->technology); reload_presets(); } + first_paint = true; } @@ -1314,16 +1546,22 @@ const std::string Materials::UNKNOWN = "(Unknown)"; void Materials::push(const Preset *preset) { - presets.push_back(preset); + presets.emplace_back(preset, 0); types.insert(technology & T_FFF ? Materials::get_filament_type(preset) : Materials::get_material_type(preset)); } +void Materials::add_printer(const Preset* preset) +{ + printers.insert(preset); +} + void Materials::clear() { presets.clear(); types.clear(); + printers.clear(); } const std::string& Materials::appconfig_section() const @@ -1373,7 +1611,6 @@ const std::string& Materials::get_material_vendor(const Preset *preset) return opt != nullptr ? opt->value : UNKNOWN; } - // priv static const std::unordered_map> legacy_preset_map {{ @@ -1601,26 +1838,28 @@ void ConfigWizard::priv::update_materials(Technology technology) if (any_fff_selected && (technology & T_FFF)) { filaments.clear(); aliases_fff.clear(); - // Iterate filaments in all bundles for (const auto &pair : bundles) { for (const auto &filament : pair.second.preset_bundle->filaments) { // Check if filament is already added - if (filaments.containts(&filament)) - continue; + if (filaments.containts(&filament)) + continue; // Iterate printers in all bundles - // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. -// for (const auto &pair : bundles) - for (const auto &printer : pair.second.preset_bundle->printers) - // Filter out inapplicable printers - if (printer.is_visible && printer.printer_technology() == ptFFF && - is_compatible_with_printer(PresetWithVendorProfile(filament, nullptr), PresetWithVendorProfile(printer, nullptr)) && - // Check if filament is already added - ! filaments.containts(&filament)) { - filaments.push(&filament); - if (!filament.alias.empty()) - aliases_fff[filament.alias].insert(filament.name); - } + for (const auto &printer : pair.second.preset_bundle->printers) { + if (!printer.is_visible || printer.printer_technology() != ptFFF) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { + if (!filaments.containts(&filament)) { + filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); + } + filaments.add_printer_counter(&filament); + filaments.add_printer(&printer); + } + } + } } } @@ -1637,17 +1876,21 @@ void ConfigWizard::priv::update_materials(Technology technology) continue; // Iterate printers in all bundles // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. -// for (const auto &pair : bundles) - for (const auto &printer : pair.second.preset_bundle->printers) - // Filter out inapplicable printers - if (printer.is_visible && printer.printer_technology() == ptSLA && - is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr)) && - // Check if material is already added - ! sla_materials.containts(&material)) { + for (const auto& printer : pair.second.preset_bundle->printers) { + if(!printer.is_visible || printer.printer_technology() != ptSLA) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + // Check if material is already added + if(!sla_materials.containts(&material)) { sla_materials.push(&material); if (!material.alias.empty()) aliases_sla[material.alias].insert(material.name); } + sla_materials.add_printer_counter(&material); + sla_materials.add_printer(&printer); + } + } } } } diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index c99c5952b9..260eeb22cb 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -20,9 +21,8 @@ #include #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/PresetBundle.hpp" #include "slic3r/Utils/PresetUpdater.hpp" -#include "AppConfig.hpp" -#include "PresetBundle.hpp" #include "BedShapeDialog.hpp" #include "GUI.hpp" #include "wxExtensions.hpp" @@ -58,32 +58,98 @@ enum Technology { T_ANY = ~0, }; +struct Bundle +{ + std::unique_ptr preset_bundle; + VendorProfile* vendor_profile{ nullptr }; + bool is_in_resources{ false }; + bool is_prusa_bundle{ false }; + + Bundle() = default; + Bundle(Bundle&& other); + + // Returns false if not loaded. Reason for that is logged as boost::log error. + bool load(fs::path source_path, bool is_in_resources, bool is_prusa_bundle = false); + + const std::string& vendor_id() const { return vendor_profile->id; } +}; + +struct BundleMap : std::unordered_map +{ + static BundleMap load(); + + Bundle& prusa_bundle(); + const Bundle& prusa_bundle() const; +}; + struct Materials { Technology technology; // use vector for the presets to purpose of save of presets sorting in the bundle - std::vector presets; + // bool is true if material is present in all printers (omnipresent) + // size_t is counter of printers compatible with material + std::vector> presets; std::set types; + std::set printers; Materials(Technology technology) : technology(technology) {} void push(const Preset *preset); + void add_printer(const Preset* preset); void clear(); bool containts(const Preset *preset) const { - return std::find(presets.begin(), presets.end(), preset) != presets.end(); + //return std::find(presets.begin(), presets.end(), preset) != presets.end(); + return std::find_if(presets.begin(), presets.end(), + [preset](const std::pair& element) { return element.first == preset; }) != presets.end(); + } + + bool get_omnipresent(const Preset* preset) { + return get_printer_counter(preset) == printers.size(); + } + + const std::vector get_presets_by_alias(const std::string name) { + std::vector ret_vec; + for (auto it = presets.begin(); it != presets.end(); ++it) { + if ((*it).first->alias == name) + ret_vec.push_back((*it).first); + } + return ret_vec; + } + + void add_printer_counter(const Preset* preset) { + for (auto it = presets.begin(); it != presets.end(); ++it) { + if ((*it).first->alias == preset->alias) + (*it).second += 1; + } + } + + size_t get_printer_counter(const Preset* preset) { + size_t highest = 0; + for (auto it : presets) { + if (it.first->alias == preset->alias && it.second > highest) + highest = it.second; + } + return highest; + } const std::string& appconfig_section() const; const std::string& get_type(const Preset *preset) const; const std::string& get_vendor(const Preset *preset) const; + - template void filter_presets(const std::string &type, const std::string &vendor, F cb) { - for (const Preset *preset : presets) { - if ((type.empty() || get_type(preset) == type) && (vendor.empty() || get_vendor(preset) == vendor)) { - cb(preset); - } - } - } + template void filter_presets(const Preset* printer, const std::string& type, const std::string& vendor, F cb) { + for (auto preset : presets) { + const Preset& prst = *(preset.first); + const Preset& prntr = *printer; + if ((printer == nullptr || is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) && + (type.empty() || get_type(preset.first) == type) && + (vendor.empty() || get_vendor(preset.first) == vendor)) { + + cb(preset.first); + } + } + } static const std::string UNKNOWN; static const std::string& get_filament_type(const Preset *preset); @@ -92,33 +158,9 @@ struct Materials static const std::string& get_material_vendor(const Preset *preset); }; -struct Bundle -{ - std::unique_ptr preset_bundle; - VendorProfile *vendor_profile { nullptr }; - bool is_in_resources { false }; - bool is_prusa_bundle { false }; - - Bundle() = default; - Bundle(Bundle &&other); - - // Returns false if not loaded. Reason for that is logged as boost::log error. - bool load(fs::path source_path, bool is_in_resources, bool is_prusa_bundle = false); - - const std::string& vendor_id() const { return vendor_profile->id; } -}; - -struct BundleMap: std::unordered_map -{ - static BundleMap load(); - - Bundle& prusa_bundle(); - const Bundle& prusa_bundle() const; -}; struct PrinterPickerEvent; - // GUI elements typedef std::function ModelFilter; @@ -226,6 +268,7 @@ struct PagePrinters: ConfigWizardPage template struct DataList : public T { DataList(wxWindow *parent) : T(parent, wxID_ANY) {} + DataList(wxWindow* parent, int style) : T(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, NULL, style) {} // Note: We're _not_ using wxLB_SORT here because it doesn't do the right thing, // eg. "ABS" is sorted before "(All)" @@ -253,6 +296,25 @@ template struct DataList : public T } int size() { return this->GetCount(); } + + void on_mouse_move(const wxPoint& position) { + int item = T::HitTest(position); + + if(item == wxHitTest::wxHT_WINDOW_INSIDE) + BOOST_LOG_TRIVIAL(error) << "hit test wxHT_WINDOW_INSIDE"; + else if (item == wxHitTest::wxHT_WINDOW_OUTSIDE) + BOOST_LOG_TRIVIAL(error) << "hit test wxHT_WINDOW_OUTSIDE"; + else if(item == wxHitTest::wxHT_WINDOW_CORNER) + BOOST_LOG_TRIVIAL(error) << "hit test wxHT_WINDOW_CORNER"; + else if (item == wxHitTest::wxHT_WINDOW_VERT_SCROLLBAR) + BOOST_LOG_TRIVIAL(error) << "hit test wxHT_WINDOW_VERT_SCROLLBAR"; + else if (item == wxHitTest::wxHT_NOWHERE) + BOOST_LOG_TRIVIAL(error) << "hit test wxHT_NOWHERE"; + else if (item == wxHitTest::wxHT_MAX) + BOOST_LOG_TRIVIAL(error) << "hit test wxHT_MAX"; + else + BOOST_LOG_TRIVIAL(error) << "hit test: " << item; + } }; typedef DataList StringList; @@ -261,21 +323,35 @@ typedef DataList PresetList; struct PageMaterials: ConfigWizardPage { Materials *materials; - StringList *list_l1, *list_l2; - PresetList *list_l3; - int sel1_prev, sel2_prev; + StringList *list_printer, *list_type, *list_vendor; + PresetList *list_profile; + int sel_printer_prev, sel_type_prev, sel_vendor_prev; bool presets_loaded; + wxFlexGridSizer *grid; + wxStaticText *compatible_printers; + int compatible_printers_width = { 100 }; + std::string empty_printers_label; + bool first_paint = { false }; static const std::string EMPTY; + int last_hovered_item = { -1 } ; PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name); void reload_presets(); - void update_lists(int sel1, int sel2); + void update_lists(int sel1, int sel2, int sel3); + void on_material_highlighted(int sel_material); + void on_material_hovered(int sel_material); void select_material(int i); void select_all(bool select); void clear(); + void prepare_compatible_printers_label(); + void clear_compatible_printers_label(); + void on_paint(); + void on_mouse_move_on_profiles(wxMouseEvent& evt); + void on_mouse_enter_profiles(wxMouseEvent& evt); + void on_mouse_leave_profiles(wxMouseEvent& evt); virtual void on_activate() override; }; diff --git a/src/slic3r/GUI/DoubleSlider.cpp b/src/slic3r/GUI/DoubleSlider.cpp index a4b65c1b82..8a9ac34ea1 100644 --- a/src/slic3r/GUI/DoubleSlider.cpp +++ b/src/slic3r/GUI/DoubleSlider.cpp @@ -1,5 +1,11 @@ +#include "libslic3r/libslic3r.h" +#if ENABLE_GCODE_VIEWER +#include "DoubleSlider.hpp" +#include "libslic3r/GCode.hpp" +#else #include "wxExtensions.hpp" #include "libslic3r/GCode/PreviewData.hpp" +#endif // ENABLE_GCODE_VIEWER #include "GUI.hpp" #include "GUI_App.hpp" #include "Plater.hpp" @@ -15,7 +21,9 @@ #include #include #include +#if !ENABLE_GCODE_VIEWER #include +#endif // !ENABLE_GCODE_VIEWER #include #include @@ -72,8 +80,13 @@ Control::Control( wxWindow *parent, if (!is_osx) SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX +#if ENABLE_GCODE_VIEWER + m_bmp_thumb_higher = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "thumb_right") : ScalableBitmap(this, "thumb_up")); + m_bmp_thumb_lower = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "thumb_left") : ScalableBitmap(this, "thumb_down")); +#else m_bmp_thumb_higher = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "right_half_circle.png") : ScalableBitmap(this, "thumb_up")); m_bmp_thumb_lower = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "left_half_circle.png" ) : ScalableBitmap(this, "thumb_down")); +#endif // ENABLE_GCODE_VIEWER m_thumb_size = m_bmp_thumb_lower.GetBmpSize(); m_bmp_add_tick_on = ScalableBitmap(this, "colorchange_add"); @@ -275,14 +288,14 @@ wxCoord Control::get_position_from_value(const int value) return wxCoord(SLIDER_MARGIN + int(val*step + 0.5)); } -wxSize Control::get_size() +wxSize Control::get_size() const { int w, h; get_size(&w, &h); return wxSize(w, h); } -void Control::get_size(int *w, int *h) +void Control::get_size(int* w, int* h) const { GetSize(w, h); is_horizontal() ? *w -= m_lock_icon_dim : *h -= m_lock_icon_dim; @@ -302,14 +315,22 @@ double Control::get_double_value(const SelectedSlider& selection) Info Control::GetTicksValues() const { Info custom_gcode_per_print_z; +#if ENABLE_GCODE_VIEWER + std::vector& values = custom_gcode_per_print_z.gcodes; +#else std::vector& values = custom_gcode_per_print_z.gcodes; +#endif // ENABLE_GCODE_VIEWER const int val_size = m_values.size(); if (!m_values.empty()) for (const TickCode& tick : m_ticks.ticks) { if (tick.tick > val_size) break; +#if ENABLE_GCODE_VIEWER + values.emplace_back(CustomGCode::Item{ m_values[tick.tick], tick.type, tick.extruder, tick.color, tick.extra }); +#else values.emplace_back(Item{m_values[tick.tick], tick.type, tick.extruder, tick.color, tick.extra}); +#endif // ENABLE_GCODE_VIEWER } if (m_force_mode_apply) @@ -329,7 +350,11 @@ void Control::SetTicksValues(const Info& custom_gcode_per_print_z) const bool was_empty = m_ticks.empty(); m_ticks.ticks.clear(); +#if ENABLE_GCODE_VIEWER + const std::vector& heights = custom_gcode_per_print_z.gcodes; +#else const std::vector& heights = custom_gcode_per_print_z.gcodes; +#endif // ENABLE_GCODE_VIEWER for (auto h : heights) { auto it = std::lower_bound(m_values.begin(), m_values.end(), h.print_z - epsilon()); @@ -401,7 +426,15 @@ void Control::draw_focus_rect() void Control::render() { +#if ENABLE_GCODE_VIEWER +#ifdef _WIN32 + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#else SetBackgroundColour(GetParent()->GetBackgroundColour()); +#endif // _WIN32 +#else + SetBackgroundColour(GetParent()->GetBackgroundColour()); +#endif // ENABLE_GCODE_VIEWER draw_focus_rect(); wxPaintDC dc(this); @@ -417,22 +450,22 @@ void Control::render() // draw line draw_scroll_line(dc, lower_pos, higher_pos); - //draw color print ticks + // draw color print ticks draw_ticks(dc); // draw both sliders draw_thumbs(dc, lower_pos, higher_pos); - //draw lock/unlock + // draw lock/unlock draw_one_layer_icon(dc); - //draw revert bitmap (if it's shown) + // draw revert bitmap (if it's shown) draw_revert_icon(dc); - //draw cog bitmap (if it's shown) + // draw cog bitmap (if it's shown) draw_cog_icon(dc); - //draw mouse position + // draw mouse position draw_tick_on_mouse_position(dc); } @@ -544,10 +577,21 @@ wxString Control::get_label(int tick) const if (value >= m_values.size()) return "ErrVal"; - const wxString str = m_values.empty() ? - wxNumberFormatter::ToString(m_label_koef*value, 2, wxNumberFormatter::Style_None) : - wxNumberFormatter::ToString(m_values[value], 2, wxNumberFormatter::Style_None); - return format_wxstr("%1%\n(%2%)", str, m_values.empty() ? value : value+1); +#if ENABLE_GCODE_VIEWER + if (m_draw_mode == dmSequentialGCodeView) + return wxString::Format("%d", static_cast(m_values[value])); + else { + const wxString str = m_values.empty() ? + wxString::Format("%.*f", 2, m_label_koef * value) : + wxString::Format("%.*f", 2, m_values[value]); + return format_wxstr("%1%\n(%2%)", str, m_values.empty() ? value : value + 1); + } +#else + const wxString str = m_values.empty() ? + wxNumberFormatter::ToString(m_label_koef * value, 2, wxNumberFormatter::Style_None) : + wxNumberFormatter::ToString(m_values[value], 2, wxNumberFormatter::Style_None); + return format_wxstr("%1%\n(%2%)", str, m_values.empty() ? value : value + 1); +#endif // ENABLE_GCODE_VIEWER } void Control::draw_tick_text(wxDC& dc, const wxPoint& pos, int tick, bool right_side/*=true*/) const @@ -556,13 +600,36 @@ void Control::draw_tick_text(wxDC& dc, const wxPoint& pos, int tick, bool right_ const wxString label = get_label(tick); dc.GetMultiLineTextExtent(label, &text_width, &text_height); wxPoint text_pos; - if (right_side) - text_pos = is_horizontal() ? wxPoint(pos.x + 1, pos.y + m_thumb_size.x) : - wxPoint(pos.x + m_thumb_size.x+1, pos.y - 0.5*text_height - 1); - else - text_pos = is_horizontal() ? wxPoint(pos.x - text_width - 1, pos.y - m_thumb_size.x - text_height) : - wxPoint(pos.x - text_width - 1 - m_thumb_size.x, pos.y - 0.5*text_height + 1); - dc.DrawText(label, text_pos); + if (right_side) { + if (is_horizontal()) { + int width; + int height; + get_size(&width, &height); + + int x_right = pos.x + 1 + text_width; + int xx = (x_right < width) ? pos.x + 1 : pos.x - text_width - 1; + text_pos = wxPoint(xx, pos.y + m_thumb_size.x / 2 + 1); + } + else + text_pos = wxPoint(pos.x + m_thumb_size.x + 1, pos.y - 0.5 * text_height - 1); + + // update text rectangle + m_rect_lower_thumb_text = wxRect(text_pos, wxSize(text_width, text_height)); + } + else { + if (is_horizontal()) { + int x = pos.x - text_width - 1; + int xx = (x > 0) ? x : pos.x + 1; + text_pos = wxPoint(xx, pos.y - m_thumb_size.x / 2 - text_height - 1); + } + else + text_pos = wxPoint(pos.x - text_width - 1 - m_thumb_size.x, pos.y - 0.5 * text_height + 1); + + // update text rectangle + m_rect_higher_thumb_text = wxRect(text_pos, wxSize(text_width, text_height)); + } + + dc.DrawText(label, text_pos); } void Control::draw_thumb_text(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection) const @@ -572,6 +639,10 @@ void Control::draw_thumb_text(wxDC& dc, const wxPoint& pos, const SelectedSlider void Control::draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection) { +#if ENABLE_GCODE_VIEWER + wxCoord x_draw = pos.x - int(0.5 * m_thumb_size.x); + wxCoord y_draw = pos.y - int(0.5 * m_thumb_size.y); +#else wxCoord x_draw, y_draw; if (selection == ssLower) { if (is_horizontal()) { @@ -583,7 +654,7 @@ void Control::draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedSlider y_draw = pos.y - int(0.5*m_thumb_size.y); } } - else{ + else { if (is_horizontal()) { x_draw = pos.x; y_draw = pos.y - int(0.5*m_thumb_size.y); @@ -593,6 +664,7 @@ void Control::draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedSlider y_draw = pos.y - int(0.5*m_thumb_size.y); } } +#endif // ENABLE_GCODE_VIEWER dc.DrawBitmap(selection == ssLower ? m_bmp_thumb_lower.bmp() : m_bmp_thumb_higher.bmp(), x_draw, y_draw); // Update thumb rect @@ -756,7 +828,15 @@ void Control::draw_colored_band(wxDC& dc) // don't color a band for MultiExtruder mode if (m_ticks.empty() || m_mode == MultiExtruder) { +#if ENABLE_GCODE_VIEWER +#ifdef _WIN32 + draw_band(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), main_band); +#else draw_band(dc, GetParent()->GetBackgroundColour(), main_band); +#endif // _WIN32 +#else + draw_band(dc, GetParent()->GetBackgroundColour(), main_band); +#endif // ENABLE_GCODE_VIEWER return; } @@ -788,6 +868,11 @@ void Control::draw_colored_band(wxDC& dc) void Control::draw_one_layer_icon(wxDC& dc) { +#if ENABLE_GCODE_VIEWER + if (m_draw_mode == dmSequentialGCodeView) + return; +#endif // ENABLE_GCODE_VIEWER + const wxBitmap& icon = m_is_one_layer ? m_focus == fiOneLayerIcon ? m_bmp_one_layer_lock_off.bmp() : m_bmp_one_layer_lock_on.bmp() : m_focus == fiOneLayerIcon ? m_bmp_one_layer_unlock_off.bmp() : m_bmp_one_layer_unlock_on.bmp(); @@ -829,8 +914,20 @@ void Control::draw_cog_icon(wxDC& dc) get_size(&width, &height); wxCoord x_draw, y_draw; - is_horizontal() ? x_draw = width-2 : x_draw = width - m_cog_icon_dim - 2; - is_horizontal() ? y_draw = height - m_cog_icon_dim - 2 : y_draw = height-2; +#if ENABLE_GCODE_VIEWER + if (m_draw_mode == dmSequentialGCodeView) + { + is_horizontal() ? x_draw = width - 2 : x_draw = 0.5 * width - 0.5 * m_cog_icon_dim; + is_horizontal() ? y_draw = 0.5 * height - 0.5 * m_cog_icon_dim : y_draw = height - 2; + } + else + { +#endif // ENABLE_GCODE_VIEWER + is_horizontal() ? x_draw = width - 2 : x_draw = width - m_cog_icon_dim - 2; + is_horizontal() ? y_draw = height - m_cog_icon_dim - 2 : y_draw = height - 2; +#if ENABLE_GCODE_VIEWER + } +#endif // ENABLE_GCODE_VIEWER dc.DrawBitmap(m_bmp_cog.bmp(), x_draw, y_draw); @@ -838,9 +935,12 @@ void Control::draw_cog_icon(wxDC& dc) m_rect_cog_icon = wxRect(x_draw, y_draw, m_cog_icon_dim, m_cog_icon_dim); } -void Control::update_thumb_rect(const wxCoord& begin_x, const wxCoord& begin_y, const SelectedSlider& selection) +void Control::update_thumb_rect(const wxCoord begin_x, const wxCoord begin_y, const SelectedSlider& selection) { - const wxRect& rect = wxRect(begin_x, begin_y + (selection == ssLower ? int(m_thumb_size.y * 0.5) : 0), m_thumb_size.x, int(m_thumb_size.y*0.5)); + const wxRect rect = is_horizontal() ? + wxRect(begin_x + (selection == ssHigher ? m_thumb_size.x / 2 : 0), begin_y, m_thumb_size.x / 2, m_thumb_size.y) : + wxRect(begin_x, begin_y + (selection == ssLower ? m_thumb_size.y / 2 : 0), m_thumb_size.x, m_thumb_size.y / 2); + if (selection == ssLower) m_rect_lower_thumb = rect; else @@ -968,10 +1068,19 @@ wxString Control::get_tooltip(int tick/*=-1*/) if (m_focus == fiRevertIcon) return _L("Discard all custom changes"); if (m_focus == fiCogIcon) - return m_mode == MultiAsSingle ? +#if ENABLE_GCODE_VIEWER + { + if (m_draw_mode == dmSequentialGCodeView) + return _L("Jump to move") + " (Shift + G)"; + else +#endif // ENABLE_GCODE_VIEWER + return m_mode == MultiAsSingle ? GUI::from_u8((boost::format(_u8L("Jump to height %s or " - "Set extruder sequence for the entire print")) % " (Shift + G)\n").str()) : - _L("Jump to height") + " (Shift + G)"; + "Set extruder sequence for the entire print")) % " (Shift + G)\n").str()) : + _L("Jump to height") + " (Shift + G)"; +#if ENABLE_GCODE_VIEWER + } +#endif // ENABLE_GCODE_VIEWER if (m_focus == fiColorBand) return m_mode != SingleExtruder ? "" : _L("Edit current color - Right click the colored slider segment"); @@ -1099,6 +1208,14 @@ void Control::OnMotion(wxMouseEvent& event) else if (m_mode == SingleExtruder && is_point_in_rect(pos, get_colored_band_rect()) && get_edited_tick_for_position(pos) >= 0 ) m_focus = fiColorBand; + else if (is_point_in_rect(pos, m_rect_lower_thumb)) + m_focus = fiLowerThumb; + else if (is_point_in_rect(pos, m_rect_higher_thumb)) + m_focus = fiHigherThumb; + else if (is_point_in_rect(pos, m_rect_lower_thumb_text)) + m_focus = fiLowerThumbText; + else if (is_point_in_rect(pos, m_rect_higher_thumb_text)) + m_focus = fiHigherThumbText; else { m_focus = fiTick; tick = get_tick_near_point(pos); @@ -1223,7 +1340,11 @@ void Control::OnLeftUp(wxMouseEvent& event) if (m_mode == MultiAsSingle && m_draw_mode == dmRegular) show_cog_icon_context_menu(); else +#if ENABLE_GCODE_VIEWER + jump_to_value(); +#else jump_to_print_z(); +#endif // ENABLE_GCODE_VIEWER break; case maOneLayerIconClick: switch_one_layer_mode(); @@ -1262,6 +1383,15 @@ void Control::move_current_thumb(const bool condition) if (is_horizontal()) delta *= -1; + // accelerators + int accelerator = 0; + if (wxGetKeyState(WXK_SHIFT)) + accelerator += 5; + if (wxGetKeyState(WXK_CONTROL)) + accelerator += 5; + if (accelerator > 0) + delta *= accelerator; + if (m_selection == ssLower) { m_lower_value -= delta; correct_lower_value(); @@ -1295,12 +1425,32 @@ void Control::OnWheel(wxMouseEvent& event) ssLower : ssHigher; } +#if ENABLE_GCODE_VIEWER + move_current_thumb((m_draw_mode == dmSequentialGCodeView) ? event.GetWheelRotation() < 0 : event.GetWheelRotation() > 0); +#else move_current_thumb(event.GetWheelRotation() > 0); +#endif // ENABLE_GCODE_VIEWER } void Control::OnKeyDown(wxKeyEvent &event) { const int key = event.GetKeyCode(); +#if ENABLE_GCODE_VIEWER + if (m_draw_mode != dmSequentialGCodeView && key == WXK_NUMPAD_ADD) { + // OnChar() is called immediately after OnKeyDown(), which can cause call of add_tick() twice. + // To avoid this case we should suppress second add_tick() call. + m_ticks.suppress_plus(true); + add_current_tick(true); + } + else if (m_draw_mode != dmSequentialGCodeView && (key == WXK_NUMPAD_SUBTRACT || key == WXK_DELETE || key == WXK_BACK)) { + // OnChar() is called immediately after OnKeyDown(), which can cause call of delete_tick() twice. + // To avoid this case we should suppress second delete_tick() call. + m_ticks.suppress_minus(true); + delete_current_tick(); + } + else if (m_draw_mode != dmSequentialGCodeView && event.GetKeyCode() == WXK_SHIFT) + UseDefaultColors(false); +#else if (key == WXK_NUMPAD_ADD) { // OnChar() is called immediately after OnKeyDown(), which can cause call of add_tick() twice. // To avoid this case we should suppress second add_tick() call. @@ -1315,22 +1465,37 @@ void Control::OnKeyDown(wxKeyEvent &event) } else if (event.GetKeyCode() == WXK_SHIFT) UseDefaultColors(false); +#endif // ENABLE_GCODE_VIEWER else if (is_horizontal()) { - if (key == WXK_LEFT || key == WXK_RIGHT) - move_current_thumb(key == WXK_LEFT); - else if (key == WXK_UP || key == WXK_DOWN) { - m_selection = key == WXK_UP ? ssHigher : ssLower; - Refresh(); +#if ENABLE_GCODE_VIEWER + if (m_is_focused) + { +#endif // ENABLE_GCODE_VIEWER + if (key == WXK_LEFT || key == WXK_RIGHT) + move_current_thumb(key == WXK_LEFT); + else if (key == WXK_UP || key == WXK_DOWN) { + m_selection = key == WXK_UP ? ssHigher : ssLower; + Refresh(); + } +#if ENABLE_GCODE_VIEWER + } +#endif // ENABLE_GCODE_VIEWER } - } else { - if (key == WXK_LEFT || key == WXK_RIGHT) { - m_selection = key == WXK_LEFT ? ssHigher : ssLower; - Refresh(); +#if ENABLE_GCODE_VIEWER + if (m_is_focused) + { +#endif // ENABLE_GCODE_VIEWER + if (key == WXK_LEFT || key == WXK_RIGHT) { + m_selection = key == WXK_LEFT ? ssHigher : ssLower; + Refresh(); + } + else if (key == WXK_UP || key == WXK_DOWN) + move_current_thumb(key == WXK_UP); +#if ENABLE_GCODE_VIEWER } - else if (key == WXK_UP || key == WXK_DOWN) - move_current_thumb(key == WXK_UP); +#endif // ENABLE_GCODE_VIEWER } event.Skip(); // !Needed to have EVT_CHAR generated as well @@ -1351,16 +1516,27 @@ void Control::OnKeyUp(wxKeyEvent &event) void Control::OnChar(wxKeyEvent& event) { const int key = event.GetKeyCode(); - if (key == '+' && !m_ticks.suppressed_plus()) { - add_current_tick(true); - m_ticks.suppress_plus(false); - } - else if (key == '-' && !m_ticks.suppressed_minus()) { - delete_current_tick(); - m_ticks.suppress_minus(false); +#if ENABLE_GCODE_VIEWER + if (m_draw_mode != dmSequentialGCodeView) + { +#endif // ENABLE_GCODE_VIEWER + if (key == '+' && !m_ticks.suppressed_plus()) { + add_current_tick(true); + m_ticks.suppress_plus(false); + } + else if (key == '-' && !m_ticks.suppressed_minus()) { + delete_current_tick(); + m_ticks.suppress_minus(false); + } +#if ENABLE_GCODE_VIEWER } +#endif // ENABLE_GCODE_VIEWER if (key == 'G') +#if ENABLE_GCODE_VIEWER + jump_to_value(); +#else jump_to_print_z(); +#endif // ENABLE_GCODE_VIEWER } void Control::OnRightDown(wxMouseEvent& event) @@ -1550,7 +1726,11 @@ void Control::show_cog_icon_context_menu() wxMenu menu; append_menu_item(&menu, wxID_ANY, _(L("Jump to height")) + " (Shift+G)", "", - [this](wxCommandEvent&) { jump_to_print_z(); }, "", &menu); +#if ENABLE_GCODE_VIEWER + [this](wxCommandEvent&) { jump_to_value(); }, "", & menu); +#else + [this](wxCommandEvent&) { jump_to_print_z(); }, "", &menu); +#endif // ENABLE_GCODE_VIEWER append_menu_item(&menu, wxID_ANY, _(L("Set extruder sequence for the entire print")), "", [this](wxCommandEvent&) { edit_extruder_sequence(); }, "", &menu); @@ -1670,11 +1850,21 @@ static std::string get_pause_print_msg(const std::string& msg_in, double height) return into_u8(dlg.GetValue()); } +#if ENABLE_GCODE_VIEWER +static double get_value_to_jump(double active_value, double min_z, double max_z, DrawMode mode) +#else static double get_print_z_to_jump(double active_print_z, double min_z, double max_z) +#endif // ENABLE_GCODE_VIEWER { +#if ENABLE_GCODE_VIEWER + wxString msg_text = (mode == dmSequentialGCodeView) ? _L("Enter the move you want to jump to") + ":" : _L("Enter the height you want to jump to") + ":"; + wxString msg_header = (mode == dmSequentialGCodeView) ? _L("Jump to move") : _L("Jump to height"); + wxString msg_in = GUI::double_to_string(active_value); +#else wxString msg_text = _(L("Enter the height you want to jump to")) + ":"; wxString msg_header = _(L("Jump to height")); wxString msg_in = GUI::double_to_string(active_print_z); +#endif // ENABLE_GCODE_VIEWER // get custom gcode wxTextEntryDialog dlg(nullptr, msg_text, msg_header, msg_in, wxTextEntryDialogStyle); @@ -1883,6 +2073,23 @@ void Control::edit_extruder_sequence() post_ticks_changed_event(ToolChange); } +#if ENABLE_GCODE_VIEWER +void Control::jump_to_value() +{ + double value = get_value_to_jump(m_values[m_selection == ssLower ? m_lower_value : m_higher_value], + m_values[m_min_value], m_values[m_max_value], m_draw_mode); + if (value < 0.0) + return; + + auto it = std::lower_bound(m_values.begin(), m_values.end(), value - epsilon()); + int tick_value = it - m_values.begin(); + + if (m_selection == ssLower) + SetLowerValue(tick_value); + else + SetHigherValue(tick_value); +} +#else void Control::jump_to_print_z() { double print_z = get_print_z_to_jump(m_values[m_selection == ssLower ? m_lower_value : m_higher_value], @@ -1898,6 +2105,7 @@ void Control::jump_to_print_z() else SetHigherValue(tick_value); } +#endif // ENABLE_GCODE_VIEWER void Control::post_ticks_changed_event(Type type /*= Custom*/) { @@ -1968,7 +2176,11 @@ std::string TickCodeInfo::get_color_for_tick(TickCode tick, Type type, const int { if (mode == SingleExtruder && type == ColorChange && m_use_default_colors) { +#if ENABLE_GCODE_VIEWER + const std::vector& colors = ColorPrintColors::get(); +#else const std::vector& colors = GCodePreviewData::ColorPrintColors(); +#endif // ENABLE_GCODE_VIEWER if (ticks.empty()) return colors[0]; m_default_color_idx++; diff --git a/src/slic3r/GUI/DoubleSlider.hpp b/src/slic3r/GUI/DoubleSlider.hpp index e39db6fb43..fb87ac4a97 100644 --- a/src/slic3r/GUI/DoubleSlider.hpp +++ b/src/slic3r/GUI/DoubleSlider.hpp @@ -4,7 +4,9 @@ #include "libslic3r/CustomGCode.hpp" #include "wxExtensions.hpp" +#if !ENABLE_GCODE_VIEWER #include +#endif // !ENABLE_GCODE_VIEWER #include #include #include @@ -42,6 +44,10 @@ enum FocusedItem { fiCogIcon, fiColorBand, fiActionIcon, + fiLowerThumb, + fiHigherThumb, + fiLowerThumbText, + fiHigherThumbText, fiTick }; @@ -73,6 +79,9 @@ enum DrawMode dmRegular, dmSlaPrint, dmSequentialFffPrint, +#if ENABLE_GCODE_VIEWER + dmSequentialGCodeView, +#endif // ENABLE_GCODE_VIEWER }; struct TickCode @@ -210,6 +219,9 @@ public: void SetTicksValues(const Info &custom_gcode_per_print_z); void SetDrawMode(bool is_sla_print, bool is_sequential_print); +#if ENABLE_GCODE_VIEWER + void SetDrawMode(DrawMode mode) { m_draw_mode = mode; } +#endif // ENABLE_GCODE_VIEWER void SetManipulationMode(Mode mode) { m_mode = mode; } Mode GetManipulationMode() const { return m_mode; } @@ -222,7 +234,7 @@ public: bool is_higher_at_max() const { return m_higher_value == m_max_value; } bool is_full_span() const { return this->is_lower_at_min() && this->is_higher_at_max(); } - void OnPaint(wxPaintEvent& ) { render();} + void OnPaint(wxPaintEvent& ) { render(); } void OnLeftDown(wxMouseEvent& event); void OnMotion(wxMouseEvent& event); void OnLeftUp(wxMouseEvent& event); @@ -246,7 +258,11 @@ public: void discard_all_thicks(); void move_current_thumb_to_pos(wxPoint pos); void edit_extruder_sequence(); +#if ENABLE_GCODE_VIEWER + void jump_to_value(); +#else void jump_to_print_z(); +#endif // ENABLE_GCODE_VIEWER void show_add_context_menu(); void show_edit_context_menu(); void show_cog_icon_context_menu(); @@ -272,7 +288,7 @@ protected: void draw_tick_text(wxDC& dc, const wxPoint& pos, int tick, bool right_side = true) const; void draw_thumb_text(wxDC& dc, const wxPoint& pos, const SelectedSlider& selection) const; - void update_thumb_rect(const wxCoord& begin_x, const wxCoord& begin_y, const SelectedSlider& selection); + void update_thumb_rect(const wxCoord begin_x, const wxCoord begin_y, const SelectedSlider& selection); bool detect_selected_slider(const wxPoint& pt); void correct_lower_value(); void correct_higher_value(); @@ -290,8 +306,8 @@ private: int get_value_from_position(const wxCoord x, const wxCoord y); int get_value_from_position(const wxPoint pos) { return get_value_from_position(pos.x, pos.y); } wxCoord get_position_from_value(const int value); - wxSize get_size(); - void get_size(int *w, int *h); + wxSize get_size() const; + void get_size(int* w, int* h) const; double get_double_value(const SelectedSlider& selection); wxString get_tooltip(int tick = -1); int get_edited_tick_for_position(wxPoint pos, Type type = ColorChange); @@ -348,6 +364,8 @@ private: wxRect m_rect_lower_thumb; wxRect m_rect_higher_thumb; + mutable wxRect m_rect_lower_thumb_text; + mutable wxRect m_rect_higher_thumb_text; wxRect m_rect_tick_action; wxRect m_rect_one_layer_icon; wxRect m_rect_revert_icon; diff --git a/src/slic3r/GUI/ExtraRenderers.cpp b/src/slic3r/GUI/ExtraRenderers.cpp new file mode 100644 index 0000000000..2915d498c0 --- /dev/null +++ b/src/slic3r/GUI/ExtraRenderers.cpp @@ -0,0 +1,333 @@ +#include "ExtraRenderers.hpp" +#include "wxExtensions.hpp" +#include "GUI.hpp" +#include "I18N.hpp" + +#include +#ifdef wxHAS_GENERIC_DATAVIEWCTRL +#include "wx/generic/private/markuptext.h" +#include "wx/generic/private/rowheightcache.h" +#include "wx/generic/private/widthcalc.h" +#endif +/* +#ifdef __WXGTK__ +#include "wx/gtk/private.h" +#include "wx/gtk/private/value.h" +#endif +*/ +#if wxUSE_ACCESSIBILITY +#include "wx/private/markupparser.h" +#endif // wxUSE_ACCESSIBILITY + +using Slic3r::GUI::from_u8; +using Slic3r::GUI::into_u8; + + +//----------------------------------------------------------------------------- +// DataViewBitmapText +//----------------------------------------------------------------------------- + +wxIMPLEMENT_DYNAMIC_CLASS(DataViewBitmapText, wxObject) + +IMPLEMENT_VARIANT_OBJECT(DataViewBitmapText) + +// --------------------------------------------------------- +// BitmapTextRenderer +// --------------------------------------------------------- + +#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING +BitmapTextRenderer::BitmapTextRenderer(wxDataViewCellMode mode /*= wxDATAVIEW_CELL_EDITABLE*/, + int align /*= wxDVR_DEFAULT_ALIGNMENT*/): +wxDataViewRenderer(wxT("PrusaDataViewBitmapText"), mode, align) +{ + SetMode(mode); + SetAlignment(align); +} +#endif // ENABLE_NONCUSTOM_DATA_VIEW_RENDERING + +BitmapTextRenderer::~BitmapTextRenderer() +{ +#ifdef SUPPORTS_MARKUP + #ifdef wxHAS_GENERIC_DATAVIEWCTRL + if (m_markupText) + delete m_markupText; + #endif //wxHAS_GENERIC_DATAVIEWCTRL +#endif // SUPPORTS_MARKUP +} + +void BitmapTextRenderer::EnableMarkup(bool enable) +{ +#ifdef SUPPORTS_MARKUP +#ifdef wxHAS_GENERIC_DATAVIEWCTRL + if (enable) { + if (!m_markupText) + m_markupText = new wxItemMarkupText(wxString()); + } + else { + if (m_markupText) { + delete m_markupText; + m_markupText = nullptr; + } + } +#else + is_markupText = enable; +#endif //wxHAS_GENERIC_DATAVIEWCTRL +#endif // SUPPORTS_MARKUP +} + +bool BitmapTextRenderer::SetValue(const wxVariant &value) +{ + m_value << value; + +#ifdef SUPPORTS_MARKUP +#ifdef wxHAS_GENERIC_DATAVIEWCTRL + if (m_markupText) + m_markupText->SetMarkup(m_value.GetText()); + /* +#else +#if defined(__WXGTK__) + GValue gvalue = G_VALUE_INIT; + g_value_init(&gvalue, G_TYPE_STRING); + g_value_set_string(&gvalue, wxGTK_CONV_FONT(str.GetText(), GetOwner()->GetOwner()->GetFont())); + g_object_set_property(G_OBJECT(m_renderer/ *.GetText()* /), is_markupText ? "markup" : "text", &gvalue); + g_value_unset(&gvalue); +#endif // __WXGTK__ + */ +#endif // wxHAS_GENERIC_DATAVIEWCTRL +#endif // SUPPORTS_MARKUP + + return true; +} + +bool BitmapTextRenderer::GetValue(wxVariant& WXUNUSED(value)) const +{ + return false; +} + +#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING && wxUSE_ACCESSIBILITY +wxString BitmapTextRenderer::GetAccessibleDescription() const +{ +#ifdef SUPPORTS_MARKUP + if (m_markupText) + return wxMarkupParser::Strip(m_text); +#endif // SUPPORTS_MARKUP + + return m_value.GetText(); +} +#endif // wxUSE_ACCESSIBILITY && ENABLE_NONCUSTOM_DATA_VIEW_RENDERING + +bool BitmapTextRenderer::Render(wxRect rect, wxDC *dc, int state) +{ + int xoffset = 0; + + const wxBitmap& icon = m_value.GetBitmap(); + if (icon.IsOk()) + { +#ifdef __APPLE__ + wxSize icon_sz = icon.GetScaledSize(); +#else + wxSize icon_sz = icon.GetSize(); +#endif + dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon_sz.y) / 2); + xoffset = icon_sz.x + 4; + } + +#if defined(SUPPORTS_MARKUP) && defined(wxHAS_GENERIC_DATAVIEWCTRL) + if (m_markupText) + { + rect.x += xoffset; + m_markupText->Render(GetView(), *dc, rect, 0, GetEllipsizeMode()); + } + else +#endif // SUPPORTS_MARKUP && wxHAS_GENERIC_DATAVIEWCTRL + RenderText(m_value.GetText(), xoffset, rect, dc, state); + + return true; +} + +wxSize BitmapTextRenderer::GetSize() const +{ + if (!m_value.GetText().empty()) + { + wxSize size; +#if defined(SUPPORTS_MARKUP) && defined(wxHAS_GENERIC_DATAVIEWCTRL) + if (m_markupText) + { + wxDataViewCtrl* const view = GetView(); + wxClientDC dc(view); + if (GetAttr().HasFont()) + dc.SetFont(GetAttr().GetEffectiveFont(view->GetFont())); + + size = m_markupText->Measure(dc); + + int lines = m_value.GetText().Freq('\n') + 1; + size.SetHeight(size.GetHeight() * lines); + } + else +#endif // SUPPORTS_MARKUP && wxHAS_GENERIC_DATAVIEWCTRL + size = GetTextExtent(m_value.GetText()); + + if (m_value.GetBitmap().IsOk()) + size.x += m_value.GetBitmap().GetWidth() + 4; + return size; + } + return wxSize(80, 20); +} + + +wxWindow* BitmapTextRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelRect, const wxVariant& value) +{ + if (can_create_editor_ctrl && !can_create_editor_ctrl()) + return nullptr; + + DataViewBitmapText data; + data << value; + + m_was_unusable_symbol = false; + + wxPoint position = labelRect.GetPosition(); + if (data.GetBitmap().IsOk()) { + const int bmp_width = data.GetBitmap().GetWidth(); + position.x += bmp_width; + labelRect.SetWidth(labelRect.GetWidth() - bmp_width); + } + + wxTextCtrl* text_editor = new wxTextCtrl(parent, wxID_ANY, data.GetText(), + position, labelRect.GetSize(), wxTE_PROCESS_ENTER); + text_editor->SetInsertionPointEnd(); + text_editor->SelectAll(); + + return text_editor; +} + +bool BitmapTextRenderer::GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) +{ + wxTextCtrl* text_editor = wxDynamicCast(ctrl, wxTextCtrl); + if (!text_editor || text_editor->GetValue().IsEmpty()) + return false; + + std::string chosen_name = into_u8(text_editor->GetValue()); + const char* unusable_symbols = "<>:/\\|?*\""; + for (size_t i = 0; i < std::strlen(unusable_symbols); i++) { + if (chosen_name.find_first_of(unusable_symbols[i]) != std::string::npos) { + m_was_unusable_symbol = true; + return false; + } + } + + // The icon can't be edited so get its old value and reuse it. + wxVariant valueOld; + GetView()->GetModel()->GetValue(valueOld, m_item, /*colName*/0); + + DataViewBitmapText bmpText; + bmpText << valueOld; + + // But replace the text with the value entered by user. + bmpText.SetText(text_editor->GetValue()); + + value << bmpText; + return true; +} + +// ---------------------------------------------------------------------------- +// BitmapChoiceRenderer +// ---------------------------------------------------------------------------- + +bool BitmapChoiceRenderer::SetValue(const wxVariant& value) +{ + m_value << value; + return true; +} + +bool BitmapChoiceRenderer::GetValue(wxVariant& value) const +{ + value << m_value; + return true; +} + +bool BitmapChoiceRenderer::Render(wxRect rect, wxDC* dc, int state) +{ + int xoffset = 0; + + const wxBitmap& icon = m_value.GetBitmap(); + if (icon.IsOk()) + { + dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon.GetHeight()) / 2); + xoffset = icon.GetWidth() + 4; + + if (rect.height==0) + rect.height= icon.GetHeight(); + } + + RenderText(m_value.GetText(), xoffset, rect, dc, state); + + return true; +} + +wxSize BitmapChoiceRenderer::GetSize() const +{ + wxSize sz = GetTextExtent(m_value.GetText()); + + if (m_value.GetBitmap().IsOk()) + sz.x += m_value.GetBitmap().GetWidth() + 4; + + return sz; +} + + +wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelRect, const wxVariant& value) +{ + if (can_create_editor_ctrl && !can_create_editor_ctrl()) + return nullptr; + + std::vector icons = get_extruder_color_icons(); + if (icons.empty()) + return nullptr; + + DataViewBitmapText data; + data << value; + + auto c_editor = new wxBitmapComboBox(parent, wxID_ANY, wxEmptyString, + labelRect.GetTopLeft(), wxSize(labelRect.GetWidth(), -1), + 0, nullptr , wxCB_READONLY); + + int i=0; + for (wxBitmap* bmp : icons) { + if (i==0) { + c_editor->Append(_L("default"), *bmp); + ++i; + } + + c_editor->Append(wxString::Format("%d", i), *bmp); + ++i; + } + c_editor->SetSelection(atoi(data.GetText().c_str())); + + // to avoid event propagation to other sidebar items + c_editor->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { + evt.StopPropagation(); + // FinishEditing grabs new selection and triggers config update. We better call + // it explicitly, automatic update on KILL_FOCUS didn't work on Linux. + this->FinishEditing(); + }); + + return c_editor; +} + +bool BitmapChoiceRenderer::GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) +{ + wxBitmapComboBox* c = (wxBitmapComboBox*)ctrl; + int selection = c->GetSelection(); + if (selection < 0) + return false; + + DataViewBitmapText bmpText; + + bmpText.SetText(c->GetString(selection)); + bmpText.SetBitmap(c->GetItemBitmap(selection)); + + value << bmpText; + return true; +} + + diff --git a/src/slic3r/GUI/ExtraRenderers.hpp b/src/slic3r/GUI/ExtraRenderers.hpp new file mode 100644 index 0000000000..4c1fb09dec --- /dev/null +++ b/src/slic3r/GUI/ExtraRenderers.hpp @@ -0,0 +1,160 @@ +#ifndef slic3r_GUI_ExtraRenderers_hpp_ +#define slic3r_GUI_ExtraRenderers_hpp_ + +#include + +#if wxUSE_MARKUP && wxCHECK_VERSION(3, 1, 1) + #define SUPPORTS_MARKUP +#endif + +// ---------------------------------------------------------------------------- +// DataViewBitmapText: helper class used by BitmapTextRenderer +// ---------------------------------------------------------------------------- + +class DataViewBitmapText : public wxObject +{ +public: + DataViewBitmapText( const wxString &text = wxEmptyString, + const wxBitmap& bmp = wxNullBitmap) : + m_text(text), + m_bmp(bmp) + { } + + DataViewBitmapText(const DataViewBitmapText &other) + : wxObject(), + m_text(other.m_text), + m_bmp(other.m_bmp) + { } + + void SetText(const wxString &text) { m_text = text; } + wxString GetText() const { return m_text; } + void SetBitmap(const wxBitmap &bmp) { m_bmp = bmp; } + const wxBitmap &GetBitmap() const { return m_bmp; } + + bool IsSameAs(const DataViewBitmapText& other) const { + return m_text == other.m_text && m_bmp.IsSameAs(other.m_bmp); + } + + bool operator==(const DataViewBitmapText& other) const { + return IsSameAs(other); + } + + bool operator!=(const DataViewBitmapText& other) const { + return !IsSameAs(other); + } + +private: + wxString m_text; + wxBitmap m_bmp; + + wxDECLARE_DYNAMIC_CLASS(DataViewBitmapText); +}; +DECLARE_VARIANT_OBJECT(DataViewBitmapText) + +// ---------------------------------------------------------------------------- +// BitmapTextRenderer +// ---------------------------------------------------------------------------- +#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING +class BitmapTextRenderer : public wxDataViewRenderer +#else +class BitmapTextRenderer : public wxDataViewCustomRenderer +#endif //ENABLE_NONCUSTOM_DATA_VIEW_RENDERING +{ +public: + BitmapTextRenderer(bool use_markup = false, + wxDataViewCellMode mode = +#ifdef __WXOSX__ + wxDATAVIEW_CELL_INERT +#else + wxDATAVIEW_CELL_EDITABLE +#endif + + , int align = wxDVR_DEFAULT_ALIGNMENT +#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING + ); +#else + ) : + wxDataViewCustomRenderer(wxT("DataViewBitmapText"), mode, align) + { + EnableMarkup(use_markup); + } +#endif //ENABLE_NONCUSTOM_DATA_VIEW_RENDERING + + ~BitmapTextRenderer(); + + void EnableMarkup(bool enable = true); + + bool SetValue(const wxVariant& value) override; + bool GetValue(wxVariant& value) const override; +#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING && wxUSE_ACCESSIBILITY + virtual wxString GetAccessibleDescription() const override; +#endif // wxUSE_ACCESSIBILITY && ENABLE_NONCUSTOM_DATA_VIEW_RENDERING + + virtual bool Render(wxRect cell, wxDC* dc, int state) override; + virtual wxSize GetSize() const override; + + bool HasEditorCtrl() const override + { +#ifdef __WXOSX__ + return false; +#else + return true; +#endif + } + wxWindow* CreateEditorCtrl(wxWindow* parent, wxRect labelRect, const wxVariant& value) override; + bool GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) override; + bool WasCanceled() const { return m_was_unusable_symbol; } + + void set_can_create_editor_ctrl_function(std::function can_create_fn) { can_create_editor_ctrl = can_create_fn; } + +private: + DataViewBitmapText m_value; + bool m_was_unusable_symbol{ false }; + + std::function can_create_editor_ctrl { nullptr }; + +#ifdef SUPPORTS_MARKUP + #ifdef wxHAS_GENERIC_DATAVIEWCTRL + class wxItemMarkupText* m_markupText { nullptr };; + #else + bool is_markupText {false}; + #endif +#endif // SUPPORTS_MARKUP +}; + + +// ---------------------------------------------------------------------------- +// BitmapChoiceRenderer +// ---------------------------------------------------------------------------- + +class BitmapChoiceRenderer : public wxDataViewCustomRenderer +{ +public: + BitmapChoiceRenderer(wxDataViewCellMode mode = +#ifdef __WXOSX__ + wxDATAVIEW_CELL_INERT +#else + wxDATAVIEW_CELL_EDITABLE +#endif + , int align = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL + ) : wxDataViewCustomRenderer(wxT("DataViewBitmapText"), mode, align) {} + + bool SetValue(const wxVariant& value); + bool GetValue(wxVariant& value) const; + + virtual bool Render(wxRect cell, wxDC* dc, int state) override; + virtual wxSize GetSize() const override; + + bool HasEditorCtrl() const override { return true; } + wxWindow* CreateEditorCtrl(wxWindow* parent, wxRect labelRect, const wxVariant& value) override; + bool GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) override; + + void set_can_create_editor_ctrl_function(std::function can_create_fn) { can_create_editor_ctrl = can_create_fn; } + +private: + DataViewBitmapText m_value; + std::function can_create_editor_ctrl { nullptr }; +}; + + +#endif // slic3r_GUI_ExtraRenderers_hpp_ diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 3a06c3056e..a21826205b 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -295,6 +295,7 @@ void Field::msw_rescale(bool rescale_sidetext) { m_Undo_to_sys_btn->msw_rescale(); m_Undo_btn->msw_rescale(); + m_blinking_bmp->msw_rescale(); // update em_unit value m_em_unit = em_unit(m_parent); @@ -1079,6 +1080,8 @@ boost::any& Choice::get_value() m_value = static_cast(ret_enum); else if (m_opt_id.compare("support_pillar_connection_mode") == 0) m_value = static_cast(ret_enum); + else if (m_opt_id == "printhost_authorization_type") + m_value = static_cast(ret_enum); } else if (m_opt.gui_type == "f_enum_open") { const int ret_enum = field->GetSelection(); diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 484b2059f0..1a49977565 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -151,6 +151,8 @@ public: virtual wxSizer* getSizer() { return nullptr; } virtual wxWindow* getWindow() { return nullptr; } + wxStaticText* getLabel() { return m_Label; } + bool is_matched(const std::string& string, const std::string& pattern); void get_value_by_opt_type(wxString& str, const bool check_value = true); diff --git a/src/slic3r/GUI/FirmwareDialog.cpp b/src/slic3r/GUI/FirmwareDialog.cpp index fe7ff4e5de..5441c84ed5 100644 --- a/src/slic3r/GUI/FirmwareDialog.cpp +++ b/src/slic3r/GUI/FirmwareDialog.cpp @@ -766,7 +766,7 @@ const char* FirmwareDialog::priv::avr109_dev_name(Avr109Pid usb_pid) { return "Original Prusa CW1"; break; - default: throw std::runtime_error((boost::format("Invalid avr109 device USB PID: %1%") % usb_pid.boot).str()); + default: throw Slic3r::RuntimeError((boost::format("Invalid avr109 device USB PID: %1%") % usb_pid.boot).str()); } } diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp new file mode 100644 index 0000000000..831a3885e8 --- /dev/null +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -0,0 +1,2417 @@ +#include "libslic3r/libslic3r.h" +#include "GCodeViewer.hpp" + +#if ENABLE_GCODE_VIEWER +#include "libslic3r/Print.hpp" +#include "libslic3r/Geometry.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/Utils.hpp" +#include "GUI_App.hpp" +#include "MainFrame.hpp" +#include "Plater.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "Camera.hpp" +#include "I18N.hpp" +#include "GUI_Utils.hpp" +#include "DoubleSlider.hpp" +#include "GLCanvas3D.hpp" +#include "GLToolbar.hpp" +#include "GUI_Preview.hpp" +#include + +#include +#include +#include + +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +static unsigned char buffer_id(EMoveType type) { + return static_cast(type) - static_cast(EMoveType::Retract); +} + +static EMoveType buffer_type(unsigned char id) { + return static_cast(static_cast(EMoveType::Retract) + id); +} + +static std::array decode_color(const std::string& color) { + static const float INV_255 = 1.0f / 255.0f; + + std::array ret = { 0.0f, 0.0f, 0.0f }; + const char* c = color.data() + 1; + if (color.size() == 7 && color.front() == '#') { + for (size_t j = 0; j < 3; ++j) { + int digit1 = hex_digit_to_int(*c++); + int digit2 = hex_digit_to_int(*c++); + if (digit1 == -1 || digit2 == -1) + break; + + ret[j] = float(digit1 * 16 + digit2) * INV_255; + } + } + return ret; +} + +static std::vector> decode_colors(const std::vector& colors) { + std::vector> output(colors.size(), { 0.0f, 0.0f, 0.0f }); + for (size_t i = 0; i < colors.size(); ++i) { + output[i] = decode_color(colors[i]); + } + return output; +} + +static float round_to_nearest(float value, unsigned int decimals) +{ + float res = 0.0f; + if (decimals == 0) + res = std::round(value); + else { + char buf[64]; + sprintf(buf, "%.*g", decimals, value); + res = std::stof(buf); + } + return res; +} + +void GCodeViewer::VBuffer::reset() +{ + // release gpu memory + if (id > 0) { + glsafe(::glDeleteBuffers(1, &id)); + id = 0; + } + + count = 0; +} + +void GCodeViewer::IBuffer::reset() +{ + // release gpu memory + if (id > 0) { + glsafe(::glDeleteBuffers(1, &id)); + id = 0; + } + + count = 0; +} + +bool GCodeViewer::Path::matches(const GCodeProcessor::MoveVertex& move) const +{ + switch (move.type) + { + case EMoveType::Tool_change: + case EMoveType::Color_change: + case EMoveType::Pause_Print: + case EMoveType::Custom_GCode: + case EMoveType::Retract: + case EMoveType::Unretract: + case EMoveType::Extrude: + { + // use rounding to reduce the number of generated paths + return type == move.type && role == move.extrusion_role && height == round_to_nearest(move.height, 2) && + width == round_to_nearest(move.width, 2) && feedrate == move.feedrate && fan_speed == move.fan_speed && + volumetric_rate == round_to_nearest(move.volumetric_rate(), 2) && extruder_id == move.extruder_id && + cp_color_id == move.cp_color_id; + } + case EMoveType::Travel: + { + return type == move.type && feedrate == move.feedrate && extruder_id == move.extruder_id && cp_color_id == move.cp_color_id; + } + default: { return false; } + } +} + +void GCodeViewer::TBuffer::reset() +{ + // release gpu memory + vertices.reset(); + indices.reset(); + + // release cpu memory + paths = std::vector(); + render_paths = std::vector(); +} + +void GCodeViewer::TBuffer::add_path(const GCodeProcessor::MoveVertex& move, unsigned int i_id, unsigned int s_id) +{ + Path::Endpoint endpoint = { i_id, s_id, move.position }; + // use rounding to reduce the number of generated paths + paths.push_back({ move.type, move.extrusion_role, endpoint, endpoint, move.delta_extruder, + round_to_nearest(move.height, 2), round_to_nearest(move.width, 2), move.feedrate, move.fan_speed, + round_to_nearest(move.volumetric_rate(), 2), move.extruder_id, move.cp_color_id }); +} + +GCodeViewer::Color GCodeViewer::Extrusions::Range::get_color_at(float value) const +{ + // Input value scaled to the colors range + const float step = step_size(); + const float global_t = (step != 0.0f) ? std::max(0.0f, value - min) / step : 0.0f; // lower limit of 0.0f + + const size_t color_max_idx = Range_Colors.size() - 1; + + // Compute the two colors just below (low) and above (high) the input value + const size_t color_low_idx = std::clamp(static_cast(global_t), 0, color_max_idx); + const size_t color_high_idx = std::clamp(color_low_idx + 1, 0, color_max_idx); + + // Compute how far the value is between the low and high colors so that they can be interpolated + const float local_t = std::clamp(global_t - static_cast(color_low_idx), 0.0f, 1.0f); + + // Interpolate between the low and high colors to find exactly which color the input value should get + Color ret; + for (unsigned int i = 0; i < 3; ++i) { + ret[i] = lerp(Range_Colors[color_low_idx][i], Range_Colors[color_high_idx][i], local_t); + } + return ret; +} + +void GCodeViewer::SequentialView::Marker::init() +{ + m_model.init_from(stilized_arrow(16, 2.0f, 4.0f, 1.0f, 8.0f)); +} + +void GCodeViewer::SequentialView::Marker::set_world_position(const Vec3f& position) +{ + m_world_position = position; + m_world_transform = (Geometry::assemble_transform((position + m_z_offset * Vec3f::UnitZ()).cast()) * Geometry::assemble_transform(m_model.get_bounding_box().size()[2] * Vec3d::UnitZ(), { M_PI, 0.0, 0.0 })).cast(); +} + +void GCodeViewer::SequentialView::Marker::render() const +{ + if (!m_visible) + return; + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + + shader->start_using(); + shader->set_uniform("uniform_color", m_color); + + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixf(m_world_transform.data())); + + m_model.render(); + + glsafe(::glPopMatrix()); + + shader->stop_using(); + + glsafe(::glDisable(GL_BLEND)); + + static float last_window_width = 0.0f; + static size_t last_text_length = 0; + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + Size cnv_size = wxGetApp().plater()->get_current_canvas3D()->get_canvas_size(); + imgui.set_next_window_pos(0.5f * static_cast(cnv_size.get_width()), static_cast(cnv_size.get_height()), ImGuiCond_Always, 0.5f, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::SetNextWindowBgAlpha(0.25f); + imgui.begin(std::string("ToolPosition"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _u8L("Tool position") + ":"); + ImGui::SameLine(); + char buf[1024]; + sprintf(buf, "X: %.2f, Y: %.2f, Z: %.2f", m_world_position(0), m_world_position(1), m_world_position(2)); + imgui.text(std::string(buf)); + + // force extra frame to automatically update window size + float width = ImGui::GetWindowWidth(); + size_t length = strlen(buf); + if (width != last_window_width || length != last_text_length) { + last_window_width = width; + last_text_length = length; + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); + } + + imgui.end(); + ImGui::PopStyleVar(); +} + +const std::vector GCodeViewer::Extrusion_Role_Colors {{ + { 0.75f, 0.75f, 0.75f }, // erNone + { 1.00f, 0.90f, 0.43f }, // erPerimeter + { 1.00f, 0.49f, 0.22f }, // erExternalPerimeter + { 0.12f, 0.12f, 1.00f }, // erOverhangPerimeter + { 0.69f, 0.19f, 0.16f }, // erInternalInfill + { 0.59f, 0.33f, 0.80f }, // erSolidInfill + { 0.94f, 0.33f, 0.33f }, // erTopSolidInfill + { 1.00f, 0.55f, 0.41f }, // erIroning + { 0.30f, 0.50f, 0.73f }, // erBridgeInfill + { 1.00f, 1.00f, 1.00f }, // erGapFill + { 0.00f, 0.53f, 0.43f }, // erSkirt + { 0.00f, 1.00f, 0.00f }, // erSupportMaterial + { 0.00f, 0.50f, 0.00f }, // erSupportMaterialInterface + { 0.70f, 0.89f, 0.67f }, // erWipeTower + { 0.37f, 0.82f, 0.58f }, // erCustom + { 0.00f, 0.00f, 0.00f } // erMixed +}}; + +const std::vector GCodeViewer::Options_Colors {{ + { 0.803f, 0.135f, 0.839f }, // Retractions + { 0.287f, 0.679f, 0.810f }, // Unretractions + { 0.758f, 0.744f, 0.389f }, // ToolChanges + { 0.856f, 0.582f, 0.546f }, // ColorChanges + { 0.322f, 0.942f, 0.512f }, // PausePrints + { 0.886f, 0.825f, 0.262f } // CustomGCodes +}}; + +const std::vector GCodeViewer::Travel_Colors {{ + { 0.219f, 0.282f, 0.609f }, // Move + { 0.112f, 0.422f, 0.103f }, // Extrude + { 0.505f, 0.064f, 0.028f } // Retract +}}; + +const std::vector GCodeViewer::Range_Colors {{ + { 0.043f, 0.173f, 0.478f }, // bluish + { 0.075f, 0.349f, 0.522f }, + { 0.110f, 0.533f, 0.569f }, + { 0.016f, 0.839f, 0.059f }, + { 0.667f, 0.949f, 0.000f }, + { 0.988f, 0.975f, 0.012f }, + { 0.961f, 0.808f, 0.039f }, + { 0.890f, 0.533f, 0.125f }, + { 0.820f, 0.408f, 0.188f }, + { 0.761f, 0.322f, 0.235f }, + { 0.581f, 0.149f, 0.087f } // reddish +}}; + +bool GCodeViewer::init() +{ + for (size_t i = 0; i < m_buffers.size(); ++i) + { + TBuffer& buffer = m_buffers[i]; + switch (buffer_type(i)) + { + default: { break; } + case EMoveType::Tool_change: + case EMoveType::Color_change: + case EMoveType::Pause_Print: + case EMoveType::Custom_GCode: + case EMoveType::Retract: + case EMoveType::Unretract: + { + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Point; + buffer.vertices.format = VBuffer::EFormat::Position; + break; + } + case EMoveType::Extrude: + { + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Triangle; + buffer.vertices.format = VBuffer::EFormat::PositionNormal3; + break; + } + case EMoveType::Travel: + { + buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Line; + buffer.vertices.format = VBuffer::EFormat::PositionNormal1; + break; + } + } + } + + set_toolpath_move_type_visible(EMoveType::Extrude, true); + m_sequential_view.marker.init(); + init_shaders(); + + std::array point_sizes; + ::glGetIntegerv(GL_ALIASED_POINT_SIZE_RANGE, point_sizes.data()); + m_detected_point_sizes = { static_cast(point_sizes[0]), static_cast(point_sizes[1]) }; + + return true; +} + +void GCodeViewer::load(const GCodeProcessor::Result& gcode_result, const Print& print, bool initialized) +{ + // avoid processing if called with the same gcode_result + if (m_last_result_id == gcode_result.id) + return; + + m_last_result_id = gcode_result.id; + + // release gpu memory, if used + reset(); + + load_toolpaths(gcode_result); + if (wxGetApp().is_editor()) + load_shells(print, initialized); + else { + Pointfs bed_shape; + std::string texture; + std::string model; + + if (!gcode_result.bed_shape.empty()) { + // bed shape detected in the gcode + bed_shape = gcode_result.bed_shape; + auto bundle = wxGetApp().preset_bundle; + if (bundle != nullptr && !gcode_result.printer_settings_id.empty()) { + const Preset* preset = bundle->printers.find_preset(gcode_result.printer_settings_id); + if (preset != nullptr) { + model = PresetUtils::system_printer_bed_model(*preset); + texture = PresetUtils::system_printer_bed_texture(*preset); + } + } + } + else { + // adjust printbed size in dependence of toolpaths bbox + const double margin = 10.0; + Vec2d min(m_paths_bounding_box.min(0) - margin, m_paths_bounding_box.min(1) - margin); + Vec2d max(m_paths_bounding_box.max(0) + margin, m_paths_bounding_box.max(1) + margin); + + Vec2d size = max - min; + bed_shape = { + { min(0), min(1) }, + { max(0), min(1) }, + { max(0), min(1) + 0.442265 * size[1]}, + { max(0) - 10.0, min(1) + 0.4711325 * size[1]}, + { max(0) + 10.0, min(1) + 0.5288675 * size[1]}, + { max(0), min(1) + 0.557735 * size[1]}, + { max(0), max(1) }, + { min(0) + 0.557735 * size[0], max(1)}, + { min(0) + 0.5288675 * size[0], max(1) - 10.0}, + { min(0) + 0.4711325 * size[0], max(1) + 10.0}, + { min(0) + 0.442265 * size[0], max(1)}, + { min(0), max(1) } }; + } + + wxGetApp().plater()->set_bed_shape(bed_shape, texture, model, gcode_result.bed_shape.empty()); + } + + m_time_statistics = gcode_result.time_statistics; +} + +void GCodeViewer::refresh(const GCodeProcessor::Result& gcode_result, const std::vector& str_tool_colors) +{ +#if ENABLE_GCODE_VIEWER_STATISTICS + auto start_time = std::chrono::high_resolution_clock::now(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + if (m_moves_count == 0) + return; + + wxBusyCursor busy; + + if (m_view_type == EViewType::Tool && !gcode_result.extruder_colors.empty()) + // update tool colors from config stored in the gcode + m_tool_colors = decode_colors(gcode_result.extruder_colors); + else + // update tool colors + m_tool_colors = decode_colors(str_tool_colors); + + // update ranges for coloring / legend + m_extrusions.reset_ranges(); + for (size_t i = 0; i < m_moves_count; ++i) { + // skip first vertex + if (i == 0) + continue; + + const GCodeProcessor::MoveVertex& curr = gcode_result.moves[i]; + + switch (curr.type) + { + case EMoveType::Extrude: + { + m_extrusions.ranges.height.update_from(round_to_nearest(curr.height, 2)); + m_extrusions.ranges.width.update_from(round_to_nearest(curr.width, 2)); + m_extrusions.ranges.fan_speed.update_from(curr.fan_speed); + m_extrusions.ranges.volumetric_rate.update_from(round_to_nearest(curr.volumetric_rate(), 2)); + [[fallthrough]]; + } + case EMoveType::Travel: + { + if (m_buffers[buffer_id(curr.type)].visible) + m_extrusions.ranges.feedrate.update_from(curr.feedrate); + + break; + } + default: { break; } + } + } + +#if ENABLE_GCODE_VIEWER_STATISTICS + m_statistics.refresh_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + // update buffers' render paths + refresh_render_paths(false, false); + + if (Slic3r::get_logging_level() >= 5) { + long long paths_size = 0; + for (const TBuffer& buffer : m_buffers) { + paths_size += SLIC3R_STDVEC_MEMSIZE(buffer.paths, Path); + } + long long layers_zs_size = SLIC3R_STDVEC_MEMSIZE(m_layers_zs, double); + long long roles_size = SLIC3R_STDVEC_MEMSIZE(m_roles, Slic3r::ExtrusionRole); + long long extruder_ids_size = SLIC3R_STDVEC_MEMSIZE(m_extruder_ids, unsigned char); + BOOST_LOG_TRIVIAL(trace) << "Refreshed G-code extrusion paths, " + << format_memsize_MB(paths_size + layers_zs_size + roles_size + extruder_ids_size) + << log_memory_info(); + } +} + +void GCodeViewer::reset() +{ + m_moves_count = 0; + for (TBuffer& buffer : m_buffers) { + buffer.reset(); + } + + m_paths_bounding_box = BoundingBoxf3(); + m_max_bounding_box = BoundingBoxf3(); + m_tool_colors = std::vector(); + m_extruder_ids = std::vector(); + m_extrusions.reset_role_visibility_flags(); + m_extrusions.reset_ranges(); + m_shells.volumes.clear(); + m_layers_zs = std::vector(); + m_layers_z_range = { 0.0, 0.0 }; + m_roles = std::vector(); + m_time_statistics.reset(); + m_time_estimate_mode = PrintEstimatedTimeStatistics::ETimeMode::Normal; + +#if ENABLE_GCODE_VIEWER_STATISTICS + m_statistics.reset_all(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS +} + +void GCodeViewer::render() const +{ +#if ENABLE_GCODE_VIEWER_STATISTICS + m_statistics.reset_opengl(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + if (m_roles.empty()) + return; + + glsafe(::glEnable(GL_DEPTH_TEST)); + render_toolpaths(); + m_sequential_view.marker.set_world_position(m_sequential_view.current_position); + m_sequential_view.marker.render(); + render_shells(); + render_legend(); +#if ENABLE_GCODE_VIEWER_STATISTICS + render_statistics(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS +} + +bool GCodeViewer::is_toolpath_move_type_visible(EMoveType type) const +{ + size_t id = static_cast(buffer_id(type)); + return (id < m_buffers.size()) ? m_buffers[id].visible : false; +} + +void GCodeViewer::set_toolpath_move_type_visible(EMoveType type, bool visible) +{ + size_t id = static_cast(buffer_id(type)); + if (id < m_buffers.size()) + m_buffers[id].visible = visible; +} + +unsigned int GCodeViewer::get_options_visibility_flags() const +{ + auto set_flag = [](unsigned int flags, unsigned int flag, bool active) { + return active ? (flags | (1 << flag)) : flags; + }; + + unsigned int flags = 0; + flags = set_flag(flags, static_cast(Preview::OptionType::Travel), is_toolpath_move_type_visible(EMoveType::Travel)); + flags = set_flag(flags, static_cast(Preview::OptionType::Retractions), is_toolpath_move_type_visible(EMoveType::Retract)); + flags = set_flag(flags, static_cast(Preview::OptionType::Unretractions), is_toolpath_move_type_visible(EMoveType::Unretract)); + flags = set_flag(flags, static_cast(Preview::OptionType::ToolChanges), is_toolpath_move_type_visible(EMoveType::Tool_change)); + flags = set_flag(flags, static_cast(Preview::OptionType::ColorChanges), is_toolpath_move_type_visible(EMoveType::Color_change)); + flags = set_flag(flags, static_cast(Preview::OptionType::PausePrints), is_toolpath_move_type_visible(EMoveType::Pause_Print)); + flags = set_flag(flags, static_cast(Preview::OptionType::CustomGCodes), is_toolpath_move_type_visible(EMoveType::Custom_GCode)); + flags = set_flag(flags, static_cast(Preview::OptionType::Shells), m_shells.visible); + flags = set_flag(flags, static_cast(Preview::OptionType::ToolMarker), m_sequential_view.marker.is_visible()); + flags = set_flag(flags, static_cast(Preview::OptionType::Legend), is_legend_enabled()); + return flags; +} + +void GCodeViewer::set_options_visibility_from_flags(unsigned int flags) +{ + auto is_flag_set = [flags](unsigned int flag) { + return (flags & (1 << flag)) != 0; + }; + + set_toolpath_move_type_visible(EMoveType::Travel, is_flag_set(static_cast(Preview::OptionType::Travel))); + set_toolpath_move_type_visible(EMoveType::Retract, is_flag_set(static_cast(Preview::OptionType::Retractions))); + set_toolpath_move_type_visible(EMoveType::Unretract, is_flag_set(static_cast(Preview::OptionType::Unretractions))); + set_toolpath_move_type_visible(EMoveType::Tool_change, is_flag_set(static_cast(Preview::OptionType::ToolChanges))); + set_toolpath_move_type_visible(EMoveType::Color_change, is_flag_set(static_cast(Preview::OptionType::ColorChanges))); + set_toolpath_move_type_visible(EMoveType::Pause_Print, is_flag_set(static_cast(Preview::OptionType::PausePrints))); + set_toolpath_move_type_visible(EMoveType::Custom_GCode, is_flag_set(static_cast(Preview::OptionType::CustomGCodes))); + m_shells.visible = is_flag_set(static_cast(Preview::OptionType::Shells)); + m_sequential_view.marker.set_visible(is_flag_set(static_cast(Preview::OptionType::ToolMarker))); + enable_legend(is_flag_set(static_cast(Preview::OptionType::Legend))); +} + +void GCodeViewer::set_layers_z_range(const std::array& layers_z_range) +{ + bool keep_sequential_current_first = layers_z_range[0] >= m_layers_z_range[0]; + bool keep_sequential_current_last = layers_z_range[1] <= m_layers_z_range[1]; + m_layers_z_range = layers_z_range; + refresh_render_paths(keep_sequential_current_first, keep_sequential_current_last); + wxGetApp().plater()->update_preview_moves_slider(); +} + +void GCodeViewer::export_toolpaths_to_obj(const char* filename) const +{ + if (filename == nullptr) + return; + + if (!has_data()) + return; + + wxBusyCursor busy; + + // the data needed is contained into the Extrude TBuffer + const TBuffer& buffer = m_buffers[buffer_id(EMoveType::Extrude)]; + if (buffer.vertices.id == 0 || buffer.indices.id == 0) + return; + + // collect color information to generate materials + std::vector colors; + for (const RenderPath& path : buffer.render_paths) { + colors.push_back(path.color); + } + + // save materials file + boost::filesystem::path mat_filename(filename); + mat_filename.replace_extension("mtl"); + FILE* fp = boost::nowide::fopen(mat_filename.string().c_str(), "w"); + if (fp == nullptr) { + BOOST_LOG_TRIVIAL(error) << "GCodeViewer::export_toolpaths_to_obj: Couldn't open " << mat_filename.string().c_str() << " for writing"; + return; + } + + fprintf(fp, "# G-Code Toolpaths Materials\n"); + fprintf(fp, "# Generated by %s based on Slic3r\n", SLIC3R_BUILD_ID); + + unsigned int colors_count = 1; + for (const Color& color : colors) { + fprintf(fp, "\nnewmtl material_%d\n", colors_count++); + fprintf(fp, "Ka 1 1 1\n"); + fprintf(fp, "Kd %f %f %f\n", color[0], color[1], color[2]); + fprintf(fp, "Ks 0 0 0\n"); + } + + fclose(fp); + + // save geometry file + fp = boost::nowide::fopen(filename, "w"); + if (fp == nullptr) { + BOOST_LOG_TRIVIAL(error) << "GCodeViewer::export_toolpaths_to_obj: Couldn't open " << filename << " for writing"; + return; + } + + fprintf(fp, "# G-Code Toolpaths\n"); + fprintf(fp, "# Generated by %s based on Slic3r\n", SLIC3R_BUILD_ID); + fprintf(fp, "\nmtllib ./%s\n", mat_filename.filename().string().c_str()); + + // get vertices data from vertex buffer on gpu + size_t floats_per_vertex = buffer.vertices.vertex_size_floats(); + std::vector vertices = std::vector(buffer.vertices.count * floats_per_vertex); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, buffer.vertices.id)); + glsafe(::glGetBufferSubData(GL_ARRAY_BUFFER, 0, buffer.vertices.data_size_bytes(), vertices.data())); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + // get indices data from index buffer on gpu + std::vector indices = std::vector(buffer.indices.count); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer.indices.id)); + glsafe(::glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, static_cast(indices.size() * sizeof(unsigned int)), indices.data())); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + + auto get_vertex = [&vertices, floats_per_vertex](unsigned int id) { + // extract vertex from vector of floats + unsigned int base_id = id * floats_per_vertex; + return Vec3f(vertices[base_id + 0], vertices[base_id + 1], vertices[base_id + 2]); + }; + + struct Segment + { + Vec3f v1; + Vec3f v2; + Vec3f dir; + Vec3f right; + Vec3f up; + Vec3f rl_displacement; + Vec3f tb_displacement; + float length; + }; + + auto generate_segment = [get_vertex](unsigned int start_id, unsigned int end_id, float half_width, float half_height) { + auto local_basis = [](const Vec3f& dir) { + // calculate local basis (dir, right, up) on given segment + std::array ret; + ret[0] = dir.normalized(); + if (std::abs(ret[0][2]) < EPSILON) { + // segment parallel to XY plane + ret[1] = { ret[0][1], -ret[0][0], 0.0f }; + ret[2] = Vec3f::UnitZ(); + } + else if (std::abs(std::abs(ret[0].dot(Vec3f::UnitZ())) - 1.0f) < EPSILON) { + // segment parallel to Z axis + ret[1] = Vec3f::UnitX(); + ret[2] = Vec3f::UnitY(); + } + else { + ret[0] = dir.normalized(); + ret[1] = ret[0].cross(Vec3f::UnitZ()).normalized(); + ret[2] = ret[1].cross(ret[0]); + } + return ret; + }; + + Vec3f v1 = get_vertex(start_id) - half_height * Vec3f::UnitZ(); + Vec3f v2 = get_vertex(end_id) - half_height * Vec3f::UnitZ(); + float length = (v2 - v1).norm(); + const auto&& [dir, right, up] = local_basis(v2 - v1); + return Segment({ v1, v2, dir, right, up, half_width * right, half_height * up, length }); + }; + + size_t out_vertices_count = 0; + unsigned int indices_per_segment = buffer.indices_per_segment(); + unsigned int start_vertex_offset = buffer.start_segment_vertex_offset(); + unsigned int end_vertex_offset = buffer.end_segment_vertex_offset(); + + for (size_t i = 0; i < buffer.render_paths.size(); ++i) { + // get paths segments from buffer paths + const RenderPath& render_path = buffer.render_paths[i]; + const Path& path = buffer.paths[render_path.path_id]; + float half_width = 0.5f * path.width; + // clamp height to avoid artifacts due to z-fighting when importing the obj file into blender and similar + float half_height = std::max(0.5f * path.height, 0.005f); + + // generates vertices/normals/triangles + std::vector out_vertices; + std::vector out_normals; + using Triangle = std::array; + std::vector out_triangles; + for (size_t j = 0; j < render_path.offsets.size(); ++j) { + unsigned int start = static_cast(render_path.offsets[j] / sizeof(unsigned int)); + unsigned int end = start + render_path.sizes[j]; + + for (size_t k = start; k < end; k += static_cast(indices_per_segment)) { + Segment curr = generate_segment(indices[k + start_vertex_offset], indices[k + end_vertex_offset], half_width, half_height); + if (k == start) { + // starting endpoint vertices/normals + out_vertices.push_back(curr.v1 + curr.rl_displacement); out_normals.push_back(curr.right); // right + out_vertices.push_back(curr.v1 + curr.tb_displacement); out_normals.push_back(curr.up); // top + out_vertices.push_back(curr.v1 - curr.rl_displacement); out_normals.push_back(-curr.right); // left + out_vertices.push_back(curr.v1 - curr.tb_displacement); out_normals.push_back(-curr.up); // bottom + out_vertices_count += 4; + + // starting cap triangles + size_t base_id = out_vertices_count - 4 + 1; + out_triangles.push_back({ base_id + 0, base_id + 1, base_id + 2 }); + out_triangles.push_back({ base_id + 0, base_id + 2, base_id + 3 }); + } + else { + // for the endpoint shared by the current and the previous segments + // we keep the top and bottom vertices of the previous vertices + // and add new left/right vertices for the current segment + out_vertices.push_back(curr.v1 + curr.rl_displacement); out_normals.push_back(curr.right); // right + out_vertices.push_back(curr.v1 - curr.rl_displacement); out_normals.push_back(-curr.right); // left + out_vertices_count += 2; + + size_t first_vertex_id = k - static_cast(indices_per_segment); + Segment prev = generate_segment(indices[first_vertex_id + start_vertex_offset], indices[first_vertex_id + end_vertex_offset], half_width, half_height); + float disp = 0.0f; + float cos_dir = prev.dir.dot(curr.dir); + if (cos_dir > -0.9998477f) { + // if the angle between adjacent segments is smaller than 179 degrees + Vec3f med_dir = (prev.dir + curr.dir).normalized(); + disp = half_width * ::tan(::acos(std::clamp(curr.dir.dot(med_dir), -1.0f, 1.0f))); + } + + Vec3f disp_vec = disp * prev.dir; + + bool is_right_turn = prev.up.dot(prev.dir.cross(curr.dir)) <= 0.0f; + if (cos_dir < 0.7071068f) { + // if the angle between two consecutive segments is greater than 45 degrees + // we add a cap in the outside corner + // and displace the vertices in the inside corner to the same position, if possible + if (is_right_turn) { + // corner cap triangles (left) + size_t base_id = out_vertices_count - 6 + 1; + out_triangles.push_back({ base_id + 5, base_id + 2, base_id + 1 }); + out_triangles.push_back({ base_id + 5, base_id + 3, base_id + 2 }); + + // update right vertices + if (disp > 0.0f && disp < prev.length && disp < curr.length) { + base_id = out_vertices.size() - 6; + out_vertices[base_id + 0] -= disp_vec; + out_vertices[base_id + 4] = out_vertices[base_id + 0]; + } + } + else { + // corner cap triangles (right) + size_t base_id = out_vertices_count - 6 + 1; + out_triangles.push_back({ base_id + 0, base_id + 4, base_id + 1 }); + out_triangles.push_back({ base_id + 0, base_id + 3, base_id + 4 }); + + // update left vertices + if (disp > 0.0f && disp < prev.length && disp < curr.length) { + base_id = out_vertices.size() - 6; + out_vertices[base_id + 2] -= disp_vec; + out_vertices[base_id + 5] = out_vertices[base_id + 2]; + } + } + } + else { + // if the angle between two consecutive segments is lesser than 45 degrees + // displace the vertices to the same position + if (is_right_turn) { + size_t base_id = out_vertices.size() - 6; + // right + out_vertices[base_id + 0] -= disp_vec; + out_vertices[base_id + 4] = out_vertices[base_id + 0]; + // left + out_vertices[base_id + 2] += disp_vec; + out_vertices[base_id + 5] = out_vertices[base_id + 2]; + } + else { + size_t base_id = out_vertices.size() - 6; + // right + out_vertices[base_id + 0] += disp_vec; + out_vertices[base_id + 4] = out_vertices[base_id + 0]; + // left + out_vertices[base_id + 2] -= disp_vec; + out_vertices[base_id + 5] = out_vertices[base_id + 2]; + } + } + } + + // current second endpoint vertices/normals + out_vertices.push_back(curr.v2 + curr.rl_displacement); out_normals.push_back(curr.right); // right + out_vertices.push_back(curr.v2 + curr.tb_displacement); out_normals.push_back(curr.up); // top + out_vertices.push_back(curr.v2 - curr.rl_displacement); out_normals.push_back(-curr.right); // left + out_vertices.push_back(curr.v2 - curr.tb_displacement); out_normals.push_back(-curr.up); // bottom + out_vertices_count += 4; + + // sides triangles + if (k == start) { + size_t base_id = out_vertices_count - 8 + 1; + out_triangles.push_back({ base_id + 0, base_id + 4, base_id + 5 }); + out_triangles.push_back({ base_id + 0, base_id + 5, base_id + 1 }); + out_triangles.push_back({ base_id + 1, base_id + 5, base_id + 6 }); + out_triangles.push_back({ base_id + 1, base_id + 6, base_id + 2 }); + out_triangles.push_back({ base_id + 2, base_id + 6, base_id + 7 }); + out_triangles.push_back({ base_id + 2, base_id + 7, base_id + 3 }); + out_triangles.push_back({ base_id + 3, base_id + 7, base_id + 4 }); + out_triangles.push_back({ base_id + 3, base_id + 4, base_id + 0 }); + } + else { + size_t base_id = out_vertices_count - 10 + 1; + out_triangles.push_back({ base_id + 4, base_id + 6, base_id + 7 }); + out_triangles.push_back({ base_id + 4, base_id + 7, base_id + 1 }); + out_triangles.push_back({ base_id + 1, base_id + 7, base_id + 8 }); + out_triangles.push_back({ base_id + 1, base_id + 8, base_id + 5 }); + out_triangles.push_back({ base_id + 5, base_id + 8, base_id + 9 }); + out_triangles.push_back({ base_id + 5, base_id + 9, base_id + 3 }); + out_triangles.push_back({ base_id + 3, base_id + 9, base_id + 6 }); + out_triangles.push_back({ base_id + 3, base_id + 6, base_id + 4 }); + } + + if (k + 2 == end) { + // ending cap triangles + size_t base_id = out_vertices_count - 4 + 1; + out_triangles.push_back({ base_id + 0, base_id + 2, base_id + 1 }); + out_triangles.push_back({ base_id + 0, base_id + 3, base_id + 2 }); + } + } + } + + // save to file + fprintf(fp, "\n# vertices path %zu\n", i + 1); + for (const Vec3f& v : out_vertices) { + fprintf(fp, "v %g %g %g\n", v[0], v[1], v[2]); + } + + fprintf(fp, "\n# normals path %zu\n", i + 1); + for (const Vec3f& n : out_normals) { + fprintf(fp, "vn %g %g %g\n", n[0], n[1], n[2]); + } + + fprintf(fp, "\n# material path %zu\n", i + 1); + fprintf(fp, "usemtl material_%zu\n", i + 1); + + fprintf(fp, "\n# triangles path %zu\n", i + 1); + for (const Triangle& t : out_triangles) { + fprintf(fp, "f %zu//%zu %zu//%zu %zu//%zu\n", t[0], t[0], t[1], t[1], t[2], t[2]); + } + } + + fclose(fp); +} + +void GCodeViewer::init_shaders() +{ + unsigned char begin_id = buffer_id(EMoveType::Retract); + unsigned char end_id = buffer_id(EMoveType::Count); + + bool is_glsl_120 = wxGetApp().is_glsl_version_greater_or_equal_to(1, 20); + for (unsigned char i = begin_id; i < end_id; ++i) { + switch (buffer_type(i)) + { + case EMoveType::Tool_change: { m_buffers[i].shader = is_glsl_120 ? "options_120" : "options_110"; break; } + case EMoveType::Color_change: { m_buffers[i].shader = is_glsl_120 ? "options_120" : "options_110"; break; } + case EMoveType::Pause_Print: { m_buffers[i].shader = is_glsl_120 ? "options_120" : "options_110"; break; } + case EMoveType::Custom_GCode: { m_buffers[i].shader = is_glsl_120 ? "options_120" : "options_110"; break; } + case EMoveType::Retract: { m_buffers[i].shader = is_glsl_120 ? "options_120" : "options_110"; break; } + case EMoveType::Unretract: { m_buffers[i].shader = is_glsl_120 ? "options_120" : "options_110"; break; } + case EMoveType::Extrude: { m_buffers[i].shader = "gouraud_light"; break; } + case EMoveType::Travel: { m_buffers[i].shader = "toolpaths_lines"; break; } + default: { break; } + } + } +} + +void GCodeViewer::load_toolpaths(const GCodeProcessor::Result& gcode_result) +{ +#if ENABLE_GCODE_VIEWER_STATISTICS + auto start_time = std::chrono::high_resolution_clock::now(); + m_statistics.results_size = SLIC3R_STDVEC_MEMSIZE(gcode_result.moves, GCodeProcessor::MoveVertex); + m_statistics.results_time = gcode_result.time; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + // vertices data + m_moves_count = gcode_result.moves.size(); + if (m_moves_count == 0) + return; + + for (size_t i = 0; i < m_moves_count; ++i) { + const GCodeProcessor::MoveVertex& move = gcode_result.moves[i]; + if (wxGetApp().is_gcode_viewer()) + // for the gcode viewer we need all moves to correctly size the printbed + m_paths_bounding_box.merge(move.position.cast()); + else { + if (move.type == EMoveType::Extrude && move.width != 0.0f && move.height != 0.0f) + m_paths_bounding_box.merge(move.position.cast()); + } + } + + // add origin + m_paths_bounding_box.merge(Vec3d::Zero()); + + // max bounding box (account for tool marker) + m_max_bounding_box = m_paths_bounding_box; + m_max_bounding_box.merge(m_paths_bounding_box.max + m_sequential_view.marker.get_bounding_box().size()[2] * Vec3d::UnitZ()); + + // format data into the buffers to be rendered as points + auto add_as_point = [](const GCodeProcessor::MoveVertex& curr, TBuffer& buffer, + std::vector& buffer_vertices, std::vector& buffer_indices, size_t move_id) { + for (int j = 0; j < 3; ++j) { + buffer_vertices.push_back(curr.position[j]); + } + buffer.add_path(curr, static_cast(buffer_indices.size()), static_cast(move_id)); + buffer_indices.push_back(static_cast(buffer_indices.size())); + }; + + // format data into the buffers to be rendered as lines + auto add_as_line = [](const GCodeProcessor::MoveVertex& prev, const GCodeProcessor::MoveVertex& curr, TBuffer& buffer, + std::vector& buffer_vertices, std::vector& buffer_indices, size_t move_id) { + // x component of the normal to the current segment (the normal is parallel to the XY plane) + float normal_x = (curr.position - prev.position).normalized()[1]; + + if (prev.type != curr.type || !buffer.paths.back().matches(curr)) { + // add starting vertex position + for (int j = 0; j < 3; ++j) { + buffer_vertices.push_back(prev.position[j]); + } + // add starting vertex normal x component + buffer_vertices.push_back(normal_x); + // add starting index + buffer_indices.push_back(static_cast(buffer_indices.size())); + buffer.add_path(curr, static_cast(buffer_indices.size() - 1), static_cast(move_id - 1)); + buffer.paths.back().first.position = prev.position; + } + + Path& last_path = buffer.paths.back(); + if (last_path.first.i_id != last_path.last.i_id) { + // add previous vertex position + for (int j = 0; j < 3; ++j) { + buffer_vertices.push_back(prev.position[j]); + } + // add previous vertex normal x component + buffer_vertices.push_back(normal_x); + // add previous index + buffer_indices.push_back(static_cast(buffer_indices.size())); + } + + // add current vertex position + for (int j = 0; j < 3; ++j) { + buffer_vertices.push_back(curr.position[j]); + } + // add current vertex normal x component + buffer_vertices.push_back(normal_x); + // add current index + buffer_indices.push_back(static_cast(buffer_indices.size())); + last_path.last = { static_cast(buffer_indices.size() - 1), static_cast(move_id), curr.position }; + }; + + // format data into the buffers to be rendered as solid + auto add_as_solid = [](const GCodeProcessor::MoveVertex& prev, const GCodeProcessor::MoveVertex& curr, TBuffer& buffer, + std::vector& buffer_vertices, std::vector& buffer_indices, size_t move_id) { + static Vec3f prev_dir; + static Vec3f prev_up; + static float prev_length; + auto store_vertex = [](std::vector& buffer_vertices, const Vec3f& position, const Vec3f& normal) { + // append position + for (int j = 0; j < 3; ++j) { + buffer_vertices.push_back(position[j]); + } + // append normal + for (int j = 0; j < 3; ++j) { + buffer_vertices.push_back(normal[j]); + } + }; + auto store_triangle = [](std::vector& buffer_indices, unsigned int i1, unsigned int i2, unsigned int i3) { + buffer_indices.push_back(i1); + buffer_indices.push_back(i2); + buffer_indices.push_back(i3); + }; + auto extract_position_at = [](const std::vector& vertices, size_t id) { + return Vec3f(vertices[id + 0], vertices[id + 1], vertices[id + 2]); + }; + auto update_position_at = [](std::vector& vertices, size_t id, const Vec3f& position) { + vertices[id + 0] = position[0]; + vertices[id + 1] = position[1]; + vertices[id + 2] = position[2]; + }; + auto append_dummy_cap = [store_triangle](std::vector& buffer_indices, unsigned int id) { + store_triangle(buffer_indices, id, id, id); + store_triangle(buffer_indices, id, id, id); + }; + + if (prev.type != curr.type || !buffer.paths.back().matches(curr)) { + buffer.add_path(curr, static_cast(buffer_indices.size()), static_cast(move_id - 1)); + buffer.paths.back().first.position = prev.position; + } + + unsigned int starting_vertices_size = static_cast(buffer_vertices.size() / buffer.vertices.vertex_size_floats()); + + Vec3f dir = (curr.position - prev.position).normalized(); + Vec3f right = (std::abs(std::abs(dir.dot(Vec3f::UnitZ())) - 1.0f) < EPSILON) ? -Vec3f::UnitY() : Vec3f(dir[1], -dir[0], 0.0f).normalized(); + Vec3f left = -right; + Vec3f up = right.cross(dir); + Vec3f bottom = -up; + + Path& last_path = buffer.paths.back(); + + float half_width = 0.5f * last_path.width; + float half_height = 0.5f * last_path.height; + + Vec3f prev_pos = prev.position - half_height * up; + Vec3f curr_pos = curr.position - half_height * up; + + float length = (curr_pos - prev_pos).norm(); + if (last_path.vertices_count() == 1) { + // 1st segment + + // vertices 1st endpoint + store_vertex(buffer_vertices, prev_pos + half_height * up, up); + store_vertex(buffer_vertices, prev_pos + half_width * right, right); + store_vertex(buffer_vertices, prev_pos + half_height * bottom, bottom); + store_vertex(buffer_vertices, prev_pos + half_width * left, left); + + // vertices 2nd endpoint + store_vertex(buffer_vertices, curr_pos + half_height * up, up); + store_vertex(buffer_vertices, curr_pos + half_width * right, right); + store_vertex(buffer_vertices, curr_pos + half_height * bottom, bottom); + store_vertex(buffer_vertices, curr_pos + half_width * left, left); + + // triangles starting cap + store_triangle(buffer_indices, starting_vertices_size + 0, starting_vertices_size + 2, starting_vertices_size + 1); + store_triangle(buffer_indices, starting_vertices_size + 0, starting_vertices_size + 3, starting_vertices_size + 2); + + // dummy triangles outer corner cap + append_dummy_cap(buffer_indices, starting_vertices_size); + + // triangles sides + store_triangle(buffer_indices, starting_vertices_size + 0, starting_vertices_size + 1, starting_vertices_size + 4); + store_triangle(buffer_indices, starting_vertices_size + 1, starting_vertices_size + 5, starting_vertices_size + 4); + store_triangle(buffer_indices, starting_vertices_size + 1, starting_vertices_size + 2, starting_vertices_size + 5); + store_triangle(buffer_indices, starting_vertices_size + 2, starting_vertices_size + 6, starting_vertices_size + 5); + store_triangle(buffer_indices, starting_vertices_size + 2, starting_vertices_size + 3, starting_vertices_size + 6); + store_triangle(buffer_indices, starting_vertices_size + 3, starting_vertices_size + 7, starting_vertices_size + 6); + store_triangle(buffer_indices, starting_vertices_size + 3, starting_vertices_size + 0, starting_vertices_size + 7); + store_triangle(buffer_indices, starting_vertices_size + 0, starting_vertices_size + 4, starting_vertices_size + 7); + + // triangles ending cap + store_triangle(buffer_indices, starting_vertices_size + 4, starting_vertices_size + 6, starting_vertices_size + 7); + store_triangle(buffer_indices, starting_vertices_size + 4, starting_vertices_size + 5, starting_vertices_size + 6); + } + else { + // any other segment + float displacement = 0.0f; + float cos_dir = prev_dir.dot(dir); + if (cos_dir > -0.9998477f) { + // if the angle between adjacent segments is smaller than 179 degrees + Vec3f med_dir = (prev_dir + dir).normalized(); + displacement = half_width * ::tan(::acos(std::clamp(dir.dot(med_dir), -1.0f, 1.0f))); + } + + Vec3f displacement_vec = displacement * prev_dir; + bool can_displace = displacement > 0.0f && displacement < prev_length && displacement < length; + + size_t prev_right_id = (starting_vertices_size - 3) * buffer.vertices.vertex_size_floats(); + size_t prev_left_id = (starting_vertices_size - 1) * buffer.vertices.vertex_size_floats(); + Vec3f prev_right_pos = extract_position_at(buffer_vertices, prev_right_id); + Vec3f prev_left_pos = extract_position_at(buffer_vertices, prev_left_id); + + bool is_right_turn = prev_up.dot(prev_dir.cross(dir)) <= 0.0f; + // whether the angle between adjacent segments is greater than 45 degrees + bool is_sharp = cos_dir < 0.7071068f; + + bool right_displaced = false; + bool left_displaced = false; + + // displace the vertex (inner with respect to the corner) of the previous segment 2nd enpoint, if possible + if (can_displace) { + if (is_right_turn) { + prev_right_pos -= displacement_vec; + update_position_at(buffer_vertices, prev_right_id, prev_right_pos); + right_displaced = true; + } + else { + prev_left_pos -= displacement_vec; + update_position_at(buffer_vertices, prev_left_id, prev_left_pos); + left_displaced = true; + } + } + + if (!is_sharp) { + // displace the vertex (outer with respect to the corner) of the previous segment 2nd enpoint, if possible + if (can_displace) { + if (is_right_turn) { + prev_left_pos += displacement_vec; + update_position_at(buffer_vertices, prev_left_id, prev_left_pos); + left_displaced = true; + } + else { + prev_right_pos += displacement_vec; + update_position_at(buffer_vertices, prev_right_id, prev_right_pos); + right_displaced = true; + } + } + + // vertices 1st endpoint (top and bottom are from previous segment 2nd endpoint) + // vertices position matches that of the previous segment 2nd endpoint, if displaced + store_vertex(buffer_vertices, right_displaced ? prev_right_pos : prev_pos + half_width * right, right); + store_vertex(buffer_vertices, left_displaced ? prev_left_pos : prev_pos + half_width * left, left); + } + else { + // vertices 1st endpoint (top and bottom are from previous segment 2nd endpoint) + // the inner corner vertex position matches that of the previous segment 2nd endpoint, if displaced + if (is_right_turn) { + store_vertex(buffer_vertices, right_displaced ? prev_right_pos : prev_pos + half_width * right, right); + store_vertex(buffer_vertices, prev_pos + half_width * left, left); + } + else { + store_vertex(buffer_vertices, prev_pos + half_width * right, right); + store_vertex(buffer_vertices, left_displaced ? prev_left_pos : prev_pos + half_width * left, left); + } + } + + // vertices 2nd endpoint + store_vertex(buffer_vertices, curr_pos + half_height * up, up); + store_vertex(buffer_vertices, curr_pos + half_width * right, right); + store_vertex(buffer_vertices, curr_pos + half_height * bottom, bottom); + store_vertex(buffer_vertices, curr_pos + half_width * left, left); + + // triangles starting cap + store_triangle(buffer_indices, starting_vertices_size - 4, starting_vertices_size - 2, starting_vertices_size + 0); + store_triangle(buffer_indices, starting_vertices_size - 4, starting_vertices_size + 1, starting_vertices_size - 2); + + // triangles outer corner cap + if (is_right_turn) { + if (left_displaced) + // dummy triangles + append_dummy_cap(buffer_indices, starting_vertices_size); + else { + store_triangle(buffer_indices, starting_vertices_size - 4, starting_vertices_size + 1, starting_vertices_size - 1); + store_triangle(buffer_indices, starting_vertices_size + 1, starting_vertices_size - 2, starting_vertices_size - 1); + } + } + else { + if (right_displaced) + // dummy triangles + append_dummy_cap(buffer_indices, starting_vertices_size); + else { + store_triangle(buffer_indices, starting_vertices_size - 4, starting_vertices_size - 3, starting_vertices_size + 0); + store_triangle(buffer_indices, starting_vertices_size - 3, starting_vertices_size - 2, starting_vertices_size + 0); + } + } + + // triangles sides + store_triangle(buffer_indices, starting_vertices_size - 4, starting_vertices_size + 0, starting_vertices_size + 2); + store_triangle(buffer_indices, starting_vertices_size + 0, starting_vertices_size + 3, starting_vertices_size + 2); + store_triangle(buffer_indices, starting_vertices_size + 0, starting_vertices_size - 2, starting_vertices_size + 3); + store_triangle(buffer_indices, starting_vertices_size - 2, starting_vertices_size + 4, starting_vertices_size + 3); + store_triangle(buffer_indices, starting_vertices_size - 2, starting_vertices_size + 1, starting_vertices_size + 4); + store_triangle(buffer_indices, starting_vertices_size + 1, starting_vertices_size + 5, starting_vertices_size + 4); + store_triangle(buffer_indices, starting_vertices_size + 1, starting_vertices_size - 4, starting_vertices_size + 5); + store_triangle(buffer_indices, starting_vertices_size - 4, starting_vertices_size + 2, starting_vertices_size + 5); + + // triangles ending cap + store_triangle(buffer_indices, starting_vertices_size + 2, starting_vertices_size + 4, starting_vertices_size + 5); + store_triangle(buffer_indices, starting_vertices_size + 2, starting_vertices_size + 3, starting_vertices_size + 4); + } + + last_path.last = { static_cast(buffer_indices.size() - 1), static_cast(move_id), curr.position }; + prev_dir = dir; + prev_up = up; + prev_length = length; + }; + + // toolpaths data -> extract from result + std::vector> vertices(m_buffers.size()); + std::vector> indices(m_buffers.size()); + for (size_t i = 0; i < m_moves_count; ++i) { + // skip first vertex + if (i == 0) + continue; + + const GCodeProcessor::MoveVertex& prev = gcode_result.moves[i - 1]; + const GCodeProcessor::MoveVertex& curr = gcode_result.moves[i]; + + unsigned char id = buffer_id(curr.type); + TBuffer& buffer = m_buffers[id]; + std::vector& buffer_vertices = vertices[id]; + std::vector& buffer_indices = indices[id]; + + switch (curr.type) + { + case EMoveType::Tool_change: + case EMoveType::Color_change: + case EMoveType::Pause_Print: + case EMoveType::Custom_GCode: + case EMoveType::Retract: + case EMoveType::Unretract: + { + add_as_point(curr, buffer, buffer_vertices, buffer_indices, i); + break; + } + case EMoveType::Extrude: + { + add_as_solid(prev, curr, buffer, buffer_vertices, buffer_indices, i); + break; + } + case EMoveType::Travel: + { + add_as_line(prev, curr, buffer, buffer_vertices, buffer_indices, i); + break; + } + default: { break; } + } + } + + // toolpaths data -> send data to gpu + for (size_t i = 0; i < m_buffers.size(); ++i) { + TBuffer& buffer = m_buffers[i]; + + // vertices + const std::vector& buffer_vertices = vertices[i]; + buffer.vertices.count = buffer_vertices.size() / buffer.vertices.vertex_size_floats(); +#if ENABLE_GCODE_VIEWER_STATISTICS + m_statistics.vertices_gpu_size += buffer_vertices.size() * sizeof(float); + m_statistics.max_vertices_in_vertex_buffer = std::max(m_statistics.max_vertices_in_vertex_buffer, static_cast(buffer.vertices.count)); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + glsafe(::glGenBuffers(1, &buffer.vertices.id)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, buffer.vertices.id)); + glsafe(::glBufferData(GL_ARRAY_BUFFER, buffer_vertices.size() * sizeof(float), buffer_vertices.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + // indices + const std::vector& buffer_indices = indices[i]; + buffer.indices.count = buffer_indices.size(); +#if ENABLE_GCODE_VIEWER_STATISTICS + m_statistics.indices_gpu_size += buffer.indices.count * sizeof(unsigned int); + m_statistics.max_indices_in_index_buffer = std::max(m_statistics.max_indices_in_index_buffer, static_cast(buffer.indices.count)); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + if (buffer.indices.count > 0) { + glsafe(::glGenBuffers(1, &buffer.indices.id)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer.indices.id)); + glsafe(::glBufferData(GL_ELEMENT_ARRAY_BUFFER, buffer.indices.count * sizeof(unsigned int), buffer_indices.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + } + } + +#if ENABLE_GCODE_VIEWER_STATISTICS + for (const TBuffer& buffer : m_buffers) { + m_statistics.paths_size += SLIC3R_STDVEC_MEMSIZE(buffer.paths, Path); + } + unsigned int travel_buffer_id = buffer_id(EMoveType::Travel); + m_statistics.travel_segments_count = indices[travel_buffer_id].size() / m_buffers[travel_buffer_id].indices_per_segment(); + unsigned int extrude_buffer_id = buffer_id(EMoveType::Extrude); + m_statistics.extrude_segments_count = indices[extrude_buffer_id].size() / m_buffers[extrude_buffer_id].indices_per_segment(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + // layers zs / roles / extruder ids / cp color ids -> extract from result + for (size_t i = 0; i < m_moves_count; ++i) { + const GCodeProcessor::MoveVertex& move = gcode_result.moves[i]; + if (move.type == EMoveType::Extrude) + m_layers_zs.emplace_back(static_cast(move.position[2])); + + m_extruder_ids.emplace_back(move.extruder_id); + + if (i > 0) + m_roles.emplace_back(move.extrusion_role); + } + + // layers zs -> replace intervals of layers with similar top positions with their average value. + std::sort(m_layers_zs.begin(), m_layers_zs.end()); + int n = int(m_layers_zs.size()); + int k = 0; + for (int i = 0; i < n;) { + int j = i + 1; + double zmax = m_layers_zs[i] + EPSILON; + for (; j < n && m_layers_zs[j] <= zmax; ++j); + m_layers_zs[k++] = (j > i + 1) ? (0.5 * (m_layers_zs[i] + m_layers_zs[j - 1])) : m_layers_zs[i]; + i = j; + } + if (k < n) + m_layers_zs.erase(m_layers_zs.begin() + k, m_layers_zs.end()); + + // set layers z range + m_layers_z_range = { m_layers_zs.front(), m_layers_zs.back() }; + + // roles -> remove duplicates + std::sort(m_roles.begin(), m_roles.end()); + m_roles.erase(std::unique(m_roles.begin(), m_roles.end()), m_roles.end()); + + // extruder ids -> remove duplicates + std::sort(m_extruder_ids.begin(), m_extruder_ids.end()); + m_extruder_ids.erase(std::unique(m_extruder_ids.begin(), m_extruder_ids.end()), m_extruder_ids.end()); + + if (Slic3r::get_logging_level() >= 5) { + long long vertices_size = 0; + for (size_t i = 0; i < vertices.size(); ++i) { + vertices_size += SLIC3R_STDVEC_MEMSIZE(vertices[i], float); + } + long long indices_size = 0; + for (size_t i = 0; i < indices.size(); ++i) { + indices_size += SLIC3R_STDVEC_MEMSIZE(indices[i], unsigned int); + } + long long paths_size = 0; + for (const TBuffer& buffer : m_buffers) { + paths_size += SLIC3R_STDVEC_MEMSIZE(buffer.paths, Path); + } + long long layers_zs_size = SLIC3R_STDVEC_MEMSIZE(m_layers_zs, double); + long long roles_size = SLIC3R_STDVEC_MEMSIZE(m_roles, Slic3r::ExtrusionRole); + long long extruder_ids_size = SLIC3R_STDVEC_MEMSIZE(m_extruder_ids, unsigned char); + BOOST_LOG_TRIVIAL(trace) << "Loaded G-code extrusion paths, " + << format_memsize_MB(vertices_size + indices_size + paths_size + layers_zs_size + roles_size + extruder_ids_size) + << log_memory_info(); + } + +#if ENABLE_GCODE_VIEWER_STATISTICS + m_statistics.load_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS +} + +void GCodeViewer::load_shells(const Print& print, bool initialized) +{ + if (print.objects().empty()) + // no shells, return + return; + + // adds objects' volumes + int object_id = 0; + for (const PrintObject* obj : print.objects()) { + const ModelObject* model_obj = obj->model_object(); + + std::vector instance_ids(model_obj->instances.size()); + for (int i = 0; i < (int)model_obj->instances.size(); ++i) { + instance_ids[i] = i; + } + + m_shells.volumes.load_object(model_obj, object_id, instance_ids, "object", initialized); + + ++object_id; + } + + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF) { + // adds wipe tower's volume + double max_z = print.objects()[0]->model_object()->get_model()->bounding_box().max(2); + const PrintConfig& config = print.config(); + size_t extruders_count = config.nozzle_diameter.size(); + if ((extruders_count > 1) && config.wipe_tower && !config.complete_objects) { + const DynamicPrintConfig& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + double layer_height = print_config.opt_float("layer_height"); + double first_layer_height = print_config.get_abs_value("first_layer_height", layer_height); + double nozzle_diameter = print.config().nozzle_diameter.values[0]; + float depth = print.wipe_tower_data(extruders_count, first_layer_height, nozzle_diameter).depth; + float brim_width = print.wipe_tower_data(extruders_count, first_layer_height, nozzle_diameter).brim_width; + + m_shells.volumes.load_wipe_tower_preview(1000, config.wipe_tower_x, config.wipe_tower_y, config.wipe_tower_width, depth, max_z, config.wipe_tower_rotation_angle, + !print.is_step_done(psWipeTower), brim_width, initialized); + } + } + + // remove modifiers + while (true) { + GLVolumePtrs::iterator it = std::find_if(m_shells.volumes.volumes.begin(), m_shells.volumes.volumes.end(), [](GLVolume* volume) { return volume->is_modifier; }); + if (it != m_shells.volumes.volumes.end()) { + delete (*it); + m_shells.volumes.volumes.erase(it); + } + else + break; + } + + for (GLVolume* volume : m_shells.volumes.volumes) { + volume->zoom_to_volumes = false; + volume->color[3] = 0.25f; + volume->force_native_color = true; + volume->set_render_color(); + } +} + +void GCodeViewer::refresh_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) const +{ +#if ENABLE_GCODE_VIEWER_STATISTICS + auto start_time = std::chrono::high_resolution_clock::now(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + auto extrusion_color = [this](const Path& path) { + Color color; + switch (m_view_type) + { + case EViewType::FeatureType: { color = Extrusion_Role_Colors[static_cast(path.role)]; break; } + case EViewType::Height: { color = m_extrusions.ranges.height.get_color_at(path.height); break; } + case EViewType::Width: { color = m_extrusions.ranges.width.get_color_at(path.width); break; } + case EViewType::Feedrate: { color = m_extrusions.ranges.feedrate.get_color_at(path.feedrate); break; } + case EViewType::FanSpeed: { color = m_extrusions.ranges.fan_speed.get_color_at(path.fan_speed); break; } + case EViewType::VolumetricRate: { color = m_extrusions.ranges.volumetric_rate.get_color_at(path.volumetric_rate); break; } + case EViewType::Tool: { color = m_tool_colors[path.extruder_id]; break; } + case EViewType::ColorPrint: { color = m_tool_colors[path.cp_color_id]; break; } + default: { color = { 1.0f, 1.0f, 1.0f }; break; } + } + return color; + }; + + auto travel_color = [this](const Path& path) { + return (path.delta_extruder < 0.0f) ? Travel_Colors[2] /* Retract */ : + ((path.delta_extruder > 0.0f) ? Travel_Colors[1] /* Extrude */ : + Travel_Colors[0] /* Move */); + }; + +#if ENABLE_GCODE_VIEWER_STATISTICS + m_statistics.render_paths_size = 0; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + + m_sequential_view.endpoints.first = m_moves_count; + m_sequential_view.endpoints.last = 0; + if (!keep_sequential_current_first) + m_sequential_view.current.first = 0; + if (!keep_sequential_current_last) + m_sequential_view.current.last = m_moves_count; + + // first pass: collect visible paths and update sequential view data + std::vector> paths; + for (TBuffer& buffer : m_buffers) { + // reset render paths + buffer.render_paths.clear(); + + if (!buffer.visible) + continue; + + for (size_t i = 0; i < buffer.paths.size(); ++i) { + const Path& path = buffer.paths[i]; + if (path.type == EMoveType::Travel) { + if (!is_travel_in_z_range(i)) + continue; + } + else if (!is_in_z_range(path)) + continue; + + if (path.type == EMoveType::Extrude && !is_visible(path)) + continue; + + // store valid path + paths.push_back({ &buffer, i }); + + m_sequential_view.endpoints.first = std::min(m_sequential_view.endpoints.first, path.first.s_id); + m_sequential_view.endpoints.last = std::max(m_sequential_view.endpoints.last, path.last.s_id); + } + } + + // update current sequential position + m_sequential_view.current.first = keep_sequential_current_first ? std::clamp(m_sequential_view.current.first, m_sequential_view.endpoints.first, m_sequential_view.endpoints.last) : m_sequential_view.endpoints.first; + m_sequential_view.current.last = keep_sequential_current_last ? std::clamp(m_sequential_view.current.last, m_sequential_view.endpoints.first, m_sequential_view.endpoints.last) : m_sequential_view.endpoints.last; + + // get the world position from gpu + bool found = false; + for (const TBuffer& buffer : m_buffers) { + // searches the path containing the current position + for (const Path& path : buffer.paths) { + if (path.contains(m_sequential_view.current.last)) { + unsigned int offset = m_sequential_view.current.last - path.first.s_id; + if (offset > 0) { + if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Line) + offset = 2 * offset - 1; + else if (buffer.render_primitive_type == TBuffer::ERenderPrimitiveType::Triangle) { + unsigned int indices_count = buffer.indices_per_segment(); + offset = indices_count * (offset - 1) + (indices_count - 6); + } + } + offset += path.first.i_id; + + // gets the index from the index buffer on gpu + unsigned int index = 0; + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer.indices.id)); + glsafe(::glGetBufferSubData(GL_ELEMENT_ARRAY_BUFFER, static_cast(offset * sizeof(unsigned int)), static_cast(sizeof(unsigned int)), static_cast(&index))); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + + // gets the position from the vertices buffer on gpu + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, buffer.vertices.id)); + glsafe(::glGetBufferSubData(GL_ARRAY_BUFFER, static_cast(index * buffer.vertices.vertex_size_bytes()), static_cast(3 * sizeof(float)), static_cast(m_sequential_view.current_position.data()))); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + found = true; + break; + } + } + if (found) + break; + } + + // second pass: filter paths by sequential data and collect them by color + for (const auto& [buffer, id] : paths) { + const Path& path = buffer->paths[id]; + if (m_sequential_view.current.last <= path.first.s_id || path.last.s_id <= m_sequential_view.current.first) + continue; + + Color color; + switch (path.type) + { + case EMoveType::Extrude: { color = extrusion_color(path); break; } + case EMoveType::Travel: { color = (m_view_type == EViewType::Feedrate || m_view_type == EViewType::Tool || m_view_type == EViewType::ColorPrint) ? extrusion_color(path) : travel_color(path); break; } + default: { color = { 0.0f, 0.0f, 0.0f }; break; } + } + + auto it = std::find_if(buffer->render_paths.begin(), buffer->render_paths.end(), [color](const RenderPath& path) { return path.color == color; }); + if (it == buffer->render_paths.end()) { + it = buffer->render_paths.insert(buffer->render_paths.end(), RenderPath()); + it->color = color; + it->path_id = id; + } + + unsigned int size_in_vertices = std::min(m_sequential_view.current.last, path.last.s_id) - std::max(m_sequential_view.current.first, path.first.s_id) + 1; + unsigned int size_in_indices = 0; + switch (buffer->render_primitive_type) + { + case TBuffer::ERenderPrimitiveType::Point: { size_in_indices = size_in_vertices; break; } + case TBuffer::ERenderPrimitiveType::Line: + case TBuffer::ERenderPrimitiveType::Triangle: { size_in_indices = buffer->indices_per_segment() * (size_in_vertices - 1); break; } + } + it->sizes.push_back(size_in_indices); + + unsigned int delta_1st = 0; + if (path.first.s_id < m_sequential_view.current.first && m_sequential_view.current.first <= path.last.s_id) + delta_1st = m_sequential_view.current.first - path.first.s_id; + + if (buffer->render_primitive_type == TBuffer::ERenderPrimitiveType::Triangle) + delta_1st *= buffer->indices_per_segment(); + + it->offsets.push_back(static_cast((path.first.i_id + delta_1st) * sizeof(unsigned int))); + } + +#if ENABLE_GCODE_VIEWER_STATISTICS + for (const TBuffer& buffer : m_buffers) { + m_statistics.render_paths_size += SLIC3R_STDVEC_MEMSIZE(buffer.render_paths, RenderPath); + for (const RenderPath& path : buffer.render_paths) { + m_statistics.render_paths_size += SLIC3R_STDVEC_MEMSIZE(path.sizes, unsigned int); + m_statistics.render_paths_size += SLIC3R_STDVEC_MEMSIZE(path.offsets, size_t); + } + } + m_statistics.refresh_paths_time = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start_time).count(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS +} + +void GCodeViewer::render_toolpaths() const +{ + float point_size = 0.8f; + std::array light_intensity = { 0.25f, 0.70f, 0.75f, 0.75f }; + const Camera& camera = wxGetApp().plater()->get_camera(); + double zoom = camera.get_zoom(); + const std::array& viewport = camera.get_viewport(); + float near_plane_height = camera.get_type() == Camera::Perspective ? static_cast(viewport[3]) / (2.0f * static_cast(2.0 * std::tan(0.5 * Geometry::deg2rad(camera.get_fov())))) : + static_cast(viewport[3]) * 0.0005; + + auto set_uniform_color = [](const std::array& color, GLShaderProgram& shader) { + std::array color4 = { color[0], color[1], color[2], 1.0f }; + shader.set_uniform("uniform_color", color4); + }; + + auto render_as_points = [this, zoom, point_size, near_plane_height, set_uniform_color](const TBuffer& buffer, EOptionsColors color_id, GLShaderProgram& shader) { + set_uniform_color(Options_Colors[static_cast(color_id)], shader); + shader.set_uniform("zoom", zoom); + shader.set_uniform("percent_outline_radius", 0.0f); + shader.set_uniform("percent_center_radius", 0.33f); + shader.set_uniform("point_size", point_size); + shader.set_uniform("near_plane_height", near_plane_height); + + glsafe(::glEnable(GL_VERTEX_PROGRAM_POINT_SIZE)); + glsafe(::glEnable(GL_POINT_SPRITE)); + + for (const RenderPath& path : buffer.render_paths) { + glsafe(::glMultiDrawElements(GL_POINTS, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_INT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.gl_multi_points_calls_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + } + + glsafe(::glDisable(GL_POINT_SPRITE)); + glsafe(::glDisable(GL_VERTEX_PROGRAM_POINT_SIZE)); + }; + + auto render_as_lines = [this, light_intensity, set_uniform_color](const TBuffer& buffer, GLShaderProgram& shader) { + shader.set_uniform("light_intensity", light_intensity); + for (const RenderPath& path : buffer.render_paths) { + set_uniform_color(path.color, shader); + glsafe(::glMultiDrawElements(GL_LINES, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_INT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.gl_multi_lines_calls_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + } + }; + + auto render_as_triangles = [this, set_uniform_color](const TBuffer& buffer, GLShaderProgram& shader) { + for (const RenderPath& path : buffer.render_paths) { + set_uniform_color(path.color, shader); + glsafe(::glMultiDrawElements(GL_TRIANGLES, (const GLsizei*)path.sizes.data(), GL_UNSIGNED_INT, (const void* const*)path.offsets.data(), (GLsizei)path.sizes.size())); +#if ENABLE_GCODE_VIEWER_STATISTICS + ++m_statistics.gl_multi_triangles_calls_count; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + } + }; + + auto line_width = [](double zoom) { + return (zoom < 5.0) ? 1.0 : (1.0 + 5.0 * (zoom - 5.0) / (100.0 - 5.0)); + }; + + glsafe(::glLineWidth(static_cast(line_width(zoom)))); + + unsigned char begin_id = buffer_id(EMoveType::Retract); + unsigned char end_id = buffer_id(EMoveType::Count); + + for (unsigned char i = begin_id; i < end_id; ++i) { + const TBuffer& buffer = m_buffers[i]; + if (!buffer.visible) + continue; + + if (buffer.vertices.id == 0 || buffer.indices.id == 0) + continue; + + GLShaderProgram* shader = wxGetApp().get_shader(buffer.shader.c_str()); + if (shader != nullptr) { + shader->start_using(); + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, buffer.vertices.id)); + glsafe(::glVertexPointer(buffer.vertices.position_size_floats(), GL_FLOAT, buffer.vertices.vertex_size_bytes(), (const void*)buffer.vertices.position_offset_size())); + glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); + bool has_normals = buffer.vertices.normal_size_floats() > 0; + if (has_normals) { + glsafe(::glNormalPointer(GL_FLOAT, buffer.vertices.vertex_size_bytes(), (const void*)buffer.vertices.normal_offset_size())); + glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); + } + + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer.indices.id)); + + switch (buffer.render_primitive_type) + { + case TBuffer::ERenderPrimitiveType::Point: + { + EOptionsColors color; + switch (buffer_type(i)) + { + case EMoveType::Tool_change: { color = EOptionsColors::ToolChanges; break; } + case EMoveType::Color_change: { color = EOptionsColors::ColorChanges; break; } + case EMoveType::Pause_Print: { color = EOptionsColors::PausePrints; break; } + case EMoveType::Custom_GCode: { color = EOptionsColors::CustomGCodes; break; } + case EMoveType::Retract: { color = EOptionsColors::Retractions; break; } + case EMoveType::Unretract: { color = EOptionsColors::Unretractions; break; } + } + render_as_points(buffer, color, *shader); + break; + } + case TBuffer::ERenderPrimitiveType::Line: + { + render_as_lines(buffer, *shader); + break; + } + case TBuffer::ERenderPrimitiveType::Triangle: + { + render_as_triangles(buffer, *shader); + break; + } + } + + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + + if (has_normals) + glsafe(::glDisableClientState(GL_NORMAL_ARRAY)); + + glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + shader->stop_using(); + } + } +} + +void GCodeViewer::render_shells() const +{ + if (!m_shells.visible || m_shells.volumes.empty()) + return; + + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + +// glsafe(::glDepthMask(GL_FALSE)); + + shader->start_using(); + m_shells.volumes.render(GLVolumeCollection::Transparent, true, wxGetApp().plater()->get_camera().get_view_matrix()); + shader->stop_using(); + +// glsafe(::glDepthMask(GL_TRUE)); +} + +void GCodeViewer::render_legend() const +{ + if (!m_legend_enabled) + return; + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + + imgui.set_next_window_pos(0.0f, 0.0f, ImGuiCond_Always); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::SetNextWindowBgAlpha(0.6f); + imgui.begin(std::string("Legend"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + enum class EItemType : unsigned char + { + Rect, + Circle, + Hexagon, + Line + }; + + const PrintEstimatedTimeStatistics::Mode& time_mode = m_time_statistics.modes[static_cast(m_time_estimate_mode)]; + + float icon_size = ImGui::GetTextLineHeight(); + float percent_bar_size = 2.0f * ImGui::GetTextLineHeight(); + + auto append_item = [this, draw_list, icon_size, percent_bar_size, &imgui](EItemType type, const Color& color, const std::string& label, + bool visible = true, const std::string& time = "", float percent = 0.0f, float max_percent = 0.0f, const std::array& offsets = { 0.0f, 0.0f }, + std::function callback = nullptr) { + if (!visible) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3333f); + ImVec2 pos = ImGui::GetCursorScreenPos(); + switch (type) + { + default: + case EItemType::Rect: + { + draw_list->AddRectFilled({ pos.x + 1.0f, pos.y + 1.0f }, { pos.x + icon_size - 1.0f, pos.y + icon_size - 1.0f }, + ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f })); + break; + } + case EItemType::Circle: + { + ImVec2 center(0.5f * (pos.x + pos.x + icon_size), 0.5f * (pos.y + pos.y + icon_size)); + if (m_buffers[buffer_id(EMoveType::Retract)].shader == "options_120") { + draw_list->AddCircleFilled(center, 0.5f * icon_size, + ImGui::GetColorU32({ 0.5f * color[0], 0.5f * color[1], 0.5f * color[2], 1.0f }), 16); + float radius = 0.5f * icon_size; + draw_list->AddCircleFilled(center, radius, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 16); + radius = 0.5f * icon_size * 0.01f * 33.0f; + draw_list->AddCircleFilled(center, radius, ImGui::GetColorU32({ 0.5f * color[0], 0.5f * color[1], 0.5f * color[2], 1.0f }), 16); + } + else + draw_list->AddCircleFilled(center, 0.5f * icon_size, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 16); + + break; + } + case EItemType::Hexagon: + { + ImVec2 center(0.5f * (pos.x + pos.x + icon_size), 0.5f * (pos.y + pos.y + icon_size)); + draw_list->AddNgonFilled(center, 0.5f * icon_size, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 6); + break; + } + case EItemType::Line: + { + draw_list->AddLine({ pos.x + 1, pos.y + icon_size - 1}, { pos.x + icon_size - 1, pos.y + 1 }, ImGui::GetColorU32({ color[0], color[1], color[2], 1.0f }), 3.0f); + break; + } + } + + // draw text + ImGui::Dummy({ icon_size, icon_size }); + ImGui::SameLine(); + if (callback != nullptr) { + if (ImGui::MenuItem(label.c_str())) + callback(); + else { + // show tooltip + if (ImGui::IsItemHovered()) { + if (!visible) + ImGui::PopStyleVar(); + ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND); + ImGui::BeginTooltip(); + imgui.text(visible ? _u8L("Click to hide") : _u8L("Click to show")); + ImGui::EndTooltip(); + ImGui::PopStyleColor(); + if (!visible) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3333f); + + // to avoid the tooltip to change size when moving the mouse + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); + } + } + + if (!time.empty()) { + ImGui::SameLine(offsets[0]); + imgui.text(time); + ImGui::SameLine(offsets[1]); + pos = ImGui::GetCursorScreenPos(); + float width = std::max(1.0f, percent_bar_size * percent / max_percent); + draw_list->AddRectFilled({ pos.x, pos.y + 2.0f }, { pos.x + width, pos.y + icon_size - 2.0f }, + ImGui::GetColorU32(ImGuiWrapper::COL_ORANGE_LIGHT)); + ImGui::Dummy({ percent_bar_size, icon_size }); + ImGui::SameLine(); + char buf[64]; + ::sprintf(buf, "%.1f%%", 100.0f * percent); + ImGui::TextUnformatted((percent > 0.0f) ? buf : ""); + } + } + else + imgui.text(label); + + if (!visible) + ImGui::PopStyleVar(); + }; + + auto append_range = [this, draw_list, &imgui, append_item](const Extrusions::Range& range, unsigned int decimals) { + auto append_range_item = [this, draw_list, &imgui, append_item](int i, float value, unsigned int decimals) { + char buf[1024]; + ::sprintf(buf, "%.*f", decimals, value); + append_item(EItemType::Rect, Range_Colors[i], buf); + }; + + if (range.count == 1) + // single item use case + append_range_item(0, range.min, decimals); + else if (range.count == 2) { + append_range_item(static_cast(Range_Colors.size()) - 1, range.max, decimals); + append_range_item(0, range.min, decimals); + } + else { + float step_size = range.step_size(); + for (int i = static_cast(Range_Colors.size()) - 1; i >= 0; --i) { + append_range_item(i, range.min + static_cast(i) * step_size, decimals); + } + } + }; + + auto append_headers = [&imgui](const std::array& texts, const std::array& offsets) { + imgui.text(texts[0]); + ImGui::SameLine(offsets[0]); + imgui.text(texts[1]); + ImGui::SameLine(offsets[1]); + imgui.text(texts[2]); + ImGui::Separator(); + }; + + auto max_width = [](const std::vector& items, const std::string& title, float extra_size = 0.0f) { + float ret = ImGui::CalcTextSize(title.c_str()).x; + for (const std::string& item : items) { + ret = std::max(ret, extra_size + ImGui::CalcTextSize(item.c_str()).x); + } + return ret; + }; + + auto calculate_offsets = [max_width](const std::vector& labels, const std::vector& times, + const std::array& titles, float extra_size = 0.0f) { + const ImGuiStyle& style = ImGui::GetStyle(); + std::array ret = { 0.0f, 0.0f }; + ret[0] = max_width(labels, titles[0], extra_size) + 3.0f * style.ItemSpacing.x; + ret[1] = ret[0] + max_width(times, titles[1]) + style.ItemSpacing.x; + return ret; + }; + + auto color_print_ranges = [this](unsigned char extruder_id, const std::vector& custom_gcode_per_print_z) { + std::vector>> ret; + ret.reserve(custom_gcode_per_print_z.size()); + + for (const auto& item : custom_gcode_per_print_z) { + if (extruder_id + 1 != static_cast(item.extruder)) + continue; + + if (item.type != ColorChange) + continue; + + auto lower_b = std::lower_bound(m_layers_zs.begin(), m_layers_zs.end(), item.print_z - Slic3r::DoubleSlider::epsilon()); + + if (lower_b == m_layers_zs.end()) + continue; + + double current_z = *lower_b; + double previous_z = lower_b == m_layers_zs.begin() ? 0.0 : *(--lower_b); + + // to avoid duplicate values, check adding values + if (ret.empty() || !(ret.back().second.first == previous_z && ret.back().second.second == current_z)) + ret.push_back({ decode_color(item.color), { previous_z, current_z } }); + } + + return ret; + }; + + auto upto_label = [](double z) { + char buf[64]; + ::sprintf(buf, "%.2f", z); + return _u8L("up to") + " " + std::string(buf) + " " + _u8L("mm"); + }; + + auto above_label = [](double z) { + char buf[64]; + ::sprintf(buf, "%.2f", z); + return _u8L("above") + " " + std::string(buf) + " " + _u8L("mm"); + }; + + auto fromto_label = [](double z1, double z2) { + char buf1[64]; + ::sprintf(buf1, "%.2f", z1); + char buf2[64]; + ::sprintf(buf2, "%.2f", z2); + return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm"); + }; + + auto role_time_and_percent = [this, time_mode](ExtrusionRole role) { + auto it = std::find_if(time_mode.roles_times.begin(), time_mode.roles_times.end(), [role](const std::pair& item) { return role == item.first; }); + return (it != time_mode.roles_times.end()) ? std::make_pair(it->second, it->second / time_mode.time) : std::make_pair(0.0f, 0.0f); + }; + + // data used to properly align items in columns when showing time + std::array offsets = { 0.0f, 0.0f }; + std::vector labels; + std::vector times; + std::vector percents; + float max_percent = 0.0f; + + if (m_view_type == EViewType::FeatureType) { + // calculate offsets to align time/percentage data + for (size_t i = 0; i < m_roles.size(); ++i) { + ExtrusionRole role = m_roles[i]; + if (role < erCount) { + labels.push_back(_u8L(ExtrusionEntity::role_to_string(role))); + auto [time, percent] = role_time_and_percent(role); + times.push_back((time > 0.0f) ? short_time(get_time_dhms(time)) : ""); + percents.push_back(percent); + max_percent = std::max(max_percent, percent); + } + } + + offsets = calculate_offsets(labels, times, { _u8L("Feature type"), _u8L("Time") }, icon_size); + } + + // total estimated printing time section + if (time_mode.time > 0.0f && (m_view_type == EViewType::FeatureType || + (m_view_type == EViewType::ColorPrint && !time_mode.custom_gcode_times.empty()))) { + ImGui::AlignTextToFramePadding(); + switch (m_time_estimate_mode) + { + case PrintEstimatedTimeStatistics::ETimeMode::Normal: + { + imgui.text(_u8L("Estimated printing time") + " [" + _u8L("Normal mode") + "]:"); + break; + } + case PrintEstimatedTimeStatistics::ETimeMode::Stealth: + { + imgui.text(_u8L("Estimated printing time") + " [" + _u8L("Stealth mode") + "]:"); + break; + } + } + ImGui::SameLine(); + imgui.text(short_time(get_time_dhms(time_mode.time))); + + auto show_mode_button = [this, &imgui](const std::string& label, PrintEstimatedTimeStatistics::ETimeMode mode) { + bool show = false; + for (size_t i = 0; i < m_time_statistics.modes.size(); ++i) { + if (i != static_cast(mode) && + short_time(get_time_dhms(m_time_statistics.modes[static_cast(mode)].time)) != short_time(get_time_dhms(m_time_statistics.modes[i].time))) { + show = true; + break; + } + } + if (show && m_time_statistics.modes[static_cast(mode)].roles_times.size() > 0) { + if (imgui.button(label)) { + m_time_estimate_mode = mode; + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); + } + } + }; + + switch (m_time_estimate_mode) + { + case PrintEstimatedTimeStatistics::ETimeMode::Normal: + { + show_mode_button(_u8L("Show stealth mode"), PrintEstimatedTimeStatistics::ETimeMode::Stealth); + break; + } + case PrintEstimatedTimeStatistics::ETimeMode::Stealth: + { + show_mode_button(_u8L("Show normal mode"), PrintEstimatedTimeStatistics::ETimeMode::Normal); + break; + } + } + ImGui::Spacing(); + } + + // extrusion paths section -> title + switch (m_view_type) + { + case EViewType::FeatureType: + { + append_headers({ _u8L("Feature type"), _u8L("Time"), _u8L("Percentage") }, offsets); + break; + } + case EViewType::Height: { imgui.title(_u8L("Height (mm)")); break; } + case EViewType::Width: { imgui.title(_u8L("Width (mm)")); break; } + case EViewType::Feedrate: { imgui.title(_u8L("Speed (mm/s)")); break; } + case EViewType::FanSpeed: { imgui.title(_u8L("Fan Speed (%)")); break; } + case EViewType::VolumetricRate: { imgui.title(_u8L("Volumetric flow rate (mm³/s)")); break; } + case EViewType::Tool: { imgui.title(_u8L("Tool")); break; } + case EViewType::ColorPrint: { imgui.title(_u8L("Color Print")); break; } + default: { break; } + } + + // extrusion paths section -> items + switch (m_view_type) + { + case EViewType::FeatureType: + { + for (size_t i = 0; i < m_roles.size(); ++i) { + ExtrusionRole role = m_roles[i]; + if (role >= erCount) + continue; + bool visible = is_visible(role); + append_item(EItemType::Rect, Extrusion_Role_Colors[static_cast(role)], labels[i], + visible, times[i], percents[i], max_percent, offsets, [this, role, visible]() { + m_extrusions.role_visibility_flags = visible ? m_extrusions.role_visibility_flags & ~(1 << role) : m_extrusions.role_visibility_flags | (1 << role); + // update buffers' render paths + refresh_render_paths(false, false); + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->update_preview_bottom_toolbar(); + } + ); + } + break; + } + case EViewType::Height: { append_range(m_extrusions.ranges.height, 3); break; } + case EViewType::Width: { append_range(m_extrusions.ranges.width, 3); break; } + case EViewType::Feedrate: { append_range(m_extrusions.ranges.feedrate, 1); break; } + case EViewType::FanSpeed: { append_range(m_extrusions.ranges.fan_speed, 0); break; } + case EViewType::VolumetricRate: { append_range(m_extrusions.ranges.volumetric_rate, 3); break; } + case EViewType::Tool: + { + // shows only extruders actually used + for (unsigned char i : m_extruder_ids) { + append_item(EItemType::Rect, m_tool_colors[i], _u8L("Extruder") + " " + std::to_string(i + 1)); + } + break; + } + case EViewType::ColorPrint: + { + const std::vector& custom_gcode_per_print_z = wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes; + const int extruders_count = wxGetApp().extruders_edited_cnt(); + if (extruders_count == 1) { // single extruder use case + std::vector>> cp_values = color_print_ranges(0, custom_gcode_per_print_z); + const int items_cnt = static_cast(cp_values.size()); + if (items_cnt == 0) { // There are no color changes, but there are some pause print or custom Gcode + append_item(EItemType::Rect, m_tool_colors.front(), _u8L("Default color")); + } + else { + for (int i = items_cnt; i >= 0; --i) { + // create label for color change item + if (i == 0) { + append_item(EItemType::Rect, m_tool_colors[0], upto_label(cp_values.front().second.first)); + break; + } + else if (i == items_cnt) { + append_item(EItemType::Rect, cp_values[i - 1].first, above_label(cp_values[i - 1].second.second)); + continue; + } + append_item(EItemType::Rect, cp_values[i - 1].first, fromto_label(cp_values[i - 1].second.second, cp_values[i].second.first)); + } + } + } + else // multi extruder use case + { + // shows only extruders actually used + for (unsigned char i : m_extruder_ids) { + std::vector>> cp_values = color_print_ranges(i, custom_gcode_per_print_z); + const int items_cnt = static_cast(cp_values.size()); + if (items_cnt == 0) { // There are no color changes, but there are some pause print or custom Gcode + append_item(EItemType::Rect, m_tool_colors[i], _u8L("Extruder") + " " + std::to_string(i + 1) + " " + _u8L("default color")); + } + else { + for (int j = items_cnt; j >= 0; --j) { + // create label for color change item + std::string label = _u8L("Extruder") + " " + std::to_string(i + 1); + if (j == 0) { + label += " " + upto_label(cp_values.front().second.first); + append_item(EItemType::Rect, m_tool_colors[i], label); + break; + } + else if (j == items_cnt) { + label += " " + above_label(cp_values[j - 1].second.second); + append_item(EItemType::Rect, cp_values[j - 1].first, label); + continue; + } + + label += " " + fromto_label(cp_values[j - 1].second.second, cp_values[j].second.first); + append_item(EItemType::Rect, cp_values[j - 1].first, label); + } + } + } + } + + break; + } + default: { break; } + } + + // partial estimated printing time section + if (m_view_type == EViewType::ColorPrint) { + using Times = std::pair; + using TimesList = std::vector>; + + // helper structure containig the data needed to render the time items + struct PartialTime + { + enum class EType : unsigned char + { + Print, + ColorChange, + Pause + }; + EType type; + int extruder_id; + Color color1; + Color color2; + Times times; + }; + using PartialTimes = std::vector; + + auto generate_partial_times = [this](const TimesList& times) { + PartialTimes items; + + std::vector custom_gcode_per_print_z = wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes; + int extruders_count = wxGetApp().extruders_edited_cnt(); + std::vector last_color(extruders_count); + for (int i = 0; i < extruders_count; ++i) { + last_color[i] = m_tool_colors[i]; + } + int last_extruder_id = 1; + for (const auto& time_rec : times) { + switch (time_rec.first) + { + case CustomGCode::PausePrint: + { + auto it = std::find_if(custom_gcode_per_print_z.begin(), custom_gcode_per_print_z.end(), [time_rec](const CustomGCode::Item& item) { return item.type == time_rec.first; }); + if (it != custom_gcode_per_print_z.end()) { + items.push_back({ PartialTime::EType::Print, it->extruder, Color(), Color(), time_rec.second }); + items.push_back({ PartialTime::EType::Pause, it->extruder, Color(), Color(), time_rec.second }); + custom_gcode_per_print_z.erase(it); + } + break; + } + case CustomGCode::ColorChange: + { + auto it = std::find_if(custom_gcode_per_print_z.begin(), custom_gcode_per_print_z.end(), [time_rec](const CustomGCode::Item& item) { return item.type == time_rec.first; }); + if (it != custom_gcode_per_print_z.end()) { + items.push_back({ PartialTime::EType::Print, it->extruder, Color(), Color(), time_rec.second }); + items.push_back({ PartialTime::EType::ColorChange, it->extruder, last_color[it->extruder - 1], decode_color(it->color), time_rec.second }); + last_color[it->extruder - 1] = decode_color(it->color); + last_extruder_id = it->extruder; + custom_gcode_per_print_z.erase(it); + } + else + items.push_back({ PartialTime::EType::Print, last_extruder_id, Color(), Color(), time_rec.second }); + + break; + } + default: { break; } + } + } + + return items; + }; + + auto append_color = [this, &imgui](const Color& color1, const Color& color2, std::array& offsets, const Times& times) { + imgui.text(_u8L("Color change")); + ImGui::SameLine(); + + float icon_size = ImGui::GetTextLineHeight(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 pos = ImGui::GetCursorScreenPos(); + pos.x -= 0.5f * ImGui::GetStyle().ItemSpacing.x; + + draw_list->AddRectFilled({ pos.x + 1.0f, pos.y + 1.0f }, { pos.x + icon_size - 1.0f, pos.y + icon_size - 1.0f }, + ImGui::GetColorU32({ color1[0], color1[1], color1[2], 1.0f })); + pos.x += icon_size; + draw_list->AddRectFilled({ pos.x + 1.0f, pos.y + 1.0f }, { pos.x + icon_size - 1.0f, pos.y + icon_size - 1.0f }, + ImGui::GetColorU32({ color2[0], color2[1], color2[2], 1.0f })); + + ImGui::SameLine(offsets[0]); + imgui.text(short_time(get_time_dhms(times.second - times.first))); + }; + + PartialTimes partial_times = generate_partial_times(time_mode.custom_gcode_times); + if (!partial_times.empty()) { + labels.clear(); + times.clear(); + + for (const PartialTime& item : partial_times) { + switch (item.type) + { + case PartialTime::EType::Print: { labels.push_back(_u8L("Print")); break; } + case PartialTime::EType::Pause: { labels.push_back(_u8L("Pause")); break; } + case PartialTime::EType::ColorChange: { labels.push_back(_u8L("Color change")); break; } + } + times.push_back(short_time(get_time_dhms(item.times.second))); + } + offsets = calculate_offsets(labels, times, { _u8L("Event"), _u8L("Remaining time") }, 2.0f * icon_size); + + ImGui::Spacing(); + append_headers({ _u8L("Event"), _u8L("Remaining time"), _u8L("Duration") }, offsets); + for (const PartialTime& item : partial_times) { + switch (item.type) + { + case PartialTime::EType::Print: + { + imgui.text(_u8L("Print")); + ImGui::SameLine(offsets[0]); + imgui.text(short_time(get_time_dhms(item.times.second))); + ImGui::SameLine(offsets[1]); + imgui.text(short_time(get_time_dhms(item.times.first))); + break; + } + case PartialTime::EType::Pause: + { + imgui.text(_u8L("Pause")); + ImGui::SameLine(offsets[0]); + imgui.text(short_time(get_time_dhms(item.times.second - item.times.first))); + break; + } + case PartialTime::EType::ColorChange: + { + append_color(item.color1, item.color2, offsets, item.times); + break; + } + } + } + } + } + + // travel paths section + if (m_buffers[buffer_id(EMoveType::Travel)].visible) { + switch (m_view_type) + { + case EViewType::Feedrate: + case EViewType::Tool: + case EViewType::ColorPrint: + { + break; + } + default: + { + // title + ImGui::Spacing(); + imgui.title(_u8L("Travel")); + + // items + append_item(EItemType::Line, Travel_Colors[0], _u8L("Movement")); + append_item(EItemType::Line, Travel_Colors[1], _u8L("Extrusion")); + append_item(EItemType::Line, Travel_Colors[2], _u8L("Retraction")); + + break; + } + } + } + + auto any_option_available = [this]() { + auto available = [this](EMoveType type) { + const TBuffer& buffer = m_buffers[buffer_id(type)]; + return buffer.visible && buffer.indices.count > 0; + }; + + return available(EMoveType::Color_change) || + available(EMoveType::Custom_GCode) || + available(EMoveType::Pause_Print) || + available(EMoveType::Retract) || + available(EMoveType::Tool_change) || + available(EMoveType::Unretract); + }; + + auto add_option = [this, append_item](EMoveType move_type, EOptionsColors color, const std::string& text) { + const TBuffer& buffer = m_buffers[buffer_id(move_type)]; + if (buffer.visible && buffer.indices.count > 0) + append_item((buffer.shader == "options_110") ? EItemType::Rect : EItemType::Circle, Options_Colors[static_cast(color)], text); + }; + + // options section + if (any_option_available()) { + // title + ImGui::Spacing(); + imgui.title(_u8L("Options")); + + // items + add_option(EMoveType::Retract, EOptionsColors::Retractions, _u8L("Retractions")); + add_option(EMoveType::Unretract, EOptionsColors::Unretractions, _u8L("Unretractions")); + add_option(EMoveType::Tool_change, EOptionsColors::ToolChanges, _u8L("Tool changes")); + add_option(EMoveType::Color_change, EOptionsColors::ColorChanges, _u8L("Color changes")); + add_option(EMoveType::Pause_Print, EOptionsColors::PausePrints, _u8L("Pause prints")); + add_option(EMoveType::Custom_GCode, EOptionsColors::CustomGCodes, _u8L("Custom GCodes")); + } + + imgui.end(); + ImGui::PopStyleVar(); +} + +#if ENABLE_GCODE_VIEWER_STATISTICS +void GCodeViewer::render_statistics() const +{ + static const float offset = 230.0f; + + ImGuiWrapper& imgui = *wxGetApp().imgui(); + + auto add_time = [this, &imgui](const std::string& label, long long time) { + char buf[1024]; + sprintf(buf, "%lld ms (%s)", time, get_time_dhms(static_cast(time) * 0.001f).c_str()); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, label); + ImGui::SameLine(offset); + imgui.text(buf); + }; + + auto add_memory = [this, &imgui](const std::string& label, long long memory) { + static const float mb = 1024.0f * 1024.0f; + static const float inv_mb = 1.0f / mb; + static const float gb = 1024.0f * mb; + static const float inv_gb = 1.0f / gb; + char buf[1024]; + if (static_cast(memory) < gb) + sprintf(buf, "%lld bytes (%.3f MB)", memory, static_cast(memory) * inv_mb); + else + sprintf(buf, "%lld bytes (%.3f GB)", memory, static_cast(memory) * inv_gb); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, label); + ImGui::SameLine(offset); + imgui.text(buf); + }; + + auto add_counter = [this, &imgui](const std::string& label, long long counter) { + char buf[1024]; + sprintf(buf, "%lld", counter); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, label); + ImGui::SameLine(offset); + imgui.text(buf); + }; + + imgui.set_next_window_pos(0.5f * wxGetApp().plater()->get_current_canvas3D()->get_canvas_size().get_width(), 0.0f, ImGuiCond_Once, 0.5f, 0.0f); + ImGui::SetNextWindowSizeConstraints({ 300, -1 }, { 600, -1 }); + imgui.begin(std::string("GCodeViewer Statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + + if (ImGui::CollapsingHeader("Time")) { + add_time(std::string("GCodeProcessor:"), m_statistics.results_time); + + ImGui::Separator(); + add_time(std::string("Load:"), m_statistics.load_time); + add_time(std::string("Refresh:"), m_statistics.refresh_time); + add_time(std::string("Refresh paths:"), m_statistics.refresh_paths_time); + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); + } + + if (ImGui::CollapsingHeader("OpenGL calls")) { + add_counter(std::string("Multi GL_POINTS:"), m_statistics.gl_multi_points_calls_count); + add_counter(std::string("Multi GL_LINES:"), m_statistics.gl_multi_lines_calls_count); + add_counter(std::string("Multi GL_TRIANGLES:"), m_statistics.gl_multi_triangles_calls_count); + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); + } + + if (ImGui::CollapsingHeader("CPU memory")) { + add_memory(std::string("GCodeProcessor results:"), m_statistics.results_size); + + ImGui::Separator(); + add_memory(std::string("Paths:"), m_statistics.paths_size); + add_memory(std::string("Render paths:"), m_statistics.render_paths_size); + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); + } + + if (ImGui::CollapsingHeader("GPU memory")) { + add_memory(std::string("Vertices:"), m_statistics.vertices_gpu_size); + add_memory(std::string("Indices:"), m_statistics.indices_gpu_size); + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); + } + + if (ImGui::CollapsingHeader("Other")) { + add_counter(std::string("Travel segments count:"), m_statistics.travel_segments_count); + add_counter(std::string("Extrude segments count:"), m_statistics.extrude_segments_count); + add_counter(std::string("Max vertices in vertex buffer:"), m_statistics.max_vertices_in_vertex_buffer); + add_counter(std::string("Max indices in index buffer:"), m_statistics.max_indices_in_index_buffer); + + wxGetApp().plater()->get_current_canvas3D()->set_as_dirty(); + wxGetApp().plater()->get_current_canvas3D()->request_extra_frame(); + } + + imgui.end(); +} +#endif // ENABLE_GCODE_VIEWER_STATISTICS + +bool GCodeViewer::is_travel_in_z_range(size_t id) const +{ + const TBuffer& buffer = m_buffers[buffer_id(EMoveType::Travel)]; + if (id >= buffer.paths.size()) + return false; + + Path path = buffer.paths[id]; + int first = static_cast(id); + unsigned int last = static_cast(id); + + // check adjacent paths + while (first > 0 && path.first.position.isApprox(buffer.paths[first - 1].last.position)) { + --first; + path.first = buffer.paths[first].first; + } + while (last < static_cast(buffer.paths.size() - 1) && path.last.position.isApprox(buffer.paths[last + 1].first.position)) { + ++last; + path.last = buffer.paths[last].last; + } + + return is_in_z_range(path); +} + +} // namespace GUI +} // namespace Slic3r + +#endif // ENABLE_GCODE_VIEWER diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp new file mode 100644 index 0000000000..50e41f4cf4 --- /dev/null +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -0,0 +1,481 @@ +#ifndef slic3r_GCodeViewer_hpp_ +#define slic3r_GCodeViewer_hpp_ + +#if ENABLE_GCODE_VIEWER +#include "3DScene.hpp" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "GLModel.hpp" + +#include + +namespace Slic3r { + +class Print; +class TriangleMesh; + +namespace GUI { + +class GCodeViewer +{ + using Color = std::array; + static const std::vector Extrusion_Role_Colors; + static const std::vector Options_Colors; + static const std::vector Travel_Colors; + static const std::vector Range_Colors; + + enum class EOptionsColors : unsigned char + { + Retractions, + Unretractions, + ToolChanges, + ColorChanges, + PausePrints, + CustomGCodes + }; + + // vbo buffer containing vertices data used to rendder a specific toolpath type + struct VBuffer + { + enum class EFormat : unsigned char + { + // vertex format: 3 floats -> position.x|position.y|position.z + Position, + // vertex format: 4 floats -> position.x|position.y|position.z|normal.x + PositionNormal1, + // vertex format: 6 floats -> position.x|position.y|position.z|normal.x|normal.y|normal.z + PositionNormal3 + }; + + EFormat format{ EFormat::Position }; + // vbo id + unsigned int id{ 0 }; + // count of vertices, updated after data are sent to gpu + size_t count{ 0 }; + + size_t data_size_bytes() const { return count * vertex_size_bytes(); } + + size_t vertex_size_floats() const { return position_size_floats() + normal_size_floats(); } + size_t vertex_size_bytes() const { return vertex_size_floats() * sizeof(float); } + + size_t position_offset_floats() const { return 0; } + size_t position_offset_size() const { return position_offset_floats() * sizeof(float); } + size_t position_size_floats() const + { + switch (format) + { + case EFormat::Position: + case EFormat::PositionNormal3: { return 3; } + case EFormat::PositionNormal1: { return 4; } + default: { return 0; } + } + } + size_t position_size_bytes() const { return position_size_floats() * sizeof(float); } + + size_t normal_offset_floats() const + { + switch (format) + { + case EFormat::Position: + case EFormat::PositionNormal1: { return 0; } + case EFormat::PositionNormal3: { return 3; } + default: { return 0; } + } + } + size_t normal_offset_size() const { return normal_offset_floats() * sizeof(float); } + size_t normal_size_floats() const { + switch (format) + { + default: + case EFormat::Position: + case EFormat::PositionNormal1: { return 0; } + case EFormat::PositionNormal3: { return 3; } + } + } + size_t normal_size_bytes() const { return normal_size_floats() * sizeof(float); } + + void reset(); + }; + + // ibo buffer containing indices data (lines/triangles) used to render a specific toolpath type + struct IBuffer + { + // ibo id + unsigned int id{ 0 }; + // count of indices, updated after data are sent to gpu + size_t count{ 0 }; + + void reset(); + }; + + // Used to identify different toolpath sub-types inside a IBuffer + struct Path + { + struct Endpoint + { + // index into the indices buffer + unsigned int i_id{ 0u }; + // sequential id + unsigned int s_id{ 0u }; + Vec3f position{ Vec3f::Zero() }; + }; + + EMoveType type{ EMoveType::Noop }; + ExtrusionRole role{ erNone }; + Endpoint first; + Endpoint last; + float delta_extruder{ 0.0f }; + float height{ 0.0f }; + float width{ 0.0f }; + float feedrate{ 0.0f }; + float fan_speed{ 0.0f }; + float volumetric_rate{ 0.0f }; + unsigned char extruder_id{ 0 }; + unsigned char cp_color_id{ 0 }; + + bool matches(const GCodeProcessor::MoveVertex& move) const; + size_t vertices_count() const { return last.s_id - first.s_id + 1; } + bool contains(unsigned int id) const { return first.s_id <= id && id <= last.s_id; } + }; + + // Used to batch the indices needed to render paths + struct RenderPath + { + Color color; + size_t path_id; + std::vector sizes; + std::vector offsets; // use size_t because we need the pointer's size (used in the call glMultiDrawElements()) + }; + + // buffer containing data for rendering a specific toolpath type + struct TBuffer + { + enum class ERenderPrimitiveType : unsigned char + { + Point, + Line, + Triangle + }; + + ERenderPrimitiveType render_primitive_type; + VBuffer vertices; + IBuffer indices; + + std::string shader; + std::vector paths; + std::vector render_paths; + bool visible{ false }; + + void reset(); + void add_path(const GCodeProcessor::MoveVertex& move, unsigned int i_id, unsigned int s_id); + unsigned int indices_per_segment() const { + switch (render_primitive_type) + { + case ERenderPrimitiveType::Point: { return 1; } + case ERenderPrimitiveType::Line: { return 2; } + case ERenderPrimitiveType::Triangle: { return 42; } // 3 indices x 14 triangles + default: { return 0; } + } + } + unsigned int start_segment_vertex_offset() const { + switch (render_primitive_type) + { + case ERenderPrimitiveType::Point: + case ERenderPrimitiveType::Line: + case ERenderPrimitiveType::Triangle: + default: { return 0; } + } + } + unsigned int end_segment_vertex_offset() const { + switch (render_primitive_type) + { + case ERenderPrimitiveType::Point: { return 0; } + case ERenderPrimitiveType::Line: { return 1; } + case ERenderPrimitiveType::Triangle: { return 36; } // 1 vertex of 13th triangle + default: { return 0; } + } + } + }; + + // helper to render shells + struct Shells + { + GLVolumeCollection volumes; + bool visible{ false }; + }; + + // helper to render extrusion paths + struct Extrusions + { + struct Range + { + float min; + float max; + unsigned int count; + + Range() { reset(); } + + void update_from(const float value) { + if (value != max && value != min) + ++count; + min = std::min(min, value); + max = std::max(max, value); + } + void reset() { min = FLT_MAX; max = -FLT_MAX; count = 0; } + + float step_size() const { return (max - min) / (static_cast(Range_Colors.size()) - 1.0f); } + Color get_color_at(float value) const; + }; + + struct Ranges + { + // Color mapping by layer height. + Range height; + // Color mapping by extrusion width. + Range width; + // Color mapping by feedrate. + Range feedrate; + // Color mapping by fan speed. + Range fan_speed; + // Color mapping by volumetric extrusion rate. + Range volumetric_rate; + + void reset() { + height.reset(); + width.reset(); + feedrate.reset(); + fan_speed.reset(); + volumetric_rate.reset(); + } + }; + + unsigned int role_visibility_flags{ 0 }; + Ranges ranges; + + void reset_role_visibility_flags() { + role_visibility_flags = 0; + for (unsigned int i = 0; i < erCount; ++i) { + role_visibility_flags |= 1 << i; + } + } + + void reset_ranges() { ranges.reset(); } + }; + +#if ENABLE_GCODE_VIEWER_STATISTICS + struct Statistics + { + // time + long long results_time{ 0 }; + long long load_time{ 0 }; + long long refresh_time{ 0 }; + long long refresh_paths_time{ 0 }; + // opengl calls + long long gl_multi_points_calls_count{ 0 }; + long long gl_multi_lines_calls_count{ 0 }; + long long gl_multi_triangles_calls_count{ 0 }; + // memory + long long results_size{ 0 }; + long long vertices_gpu_size{ 0 }; + long long indices_gpu_size{ 0 }; + long long paths_size{ 0 }; + long long render_paths_size{ 0 }; + // other + long long travel_segments_count{ 0 }; + long long extrude_segments_count{ 0 }; + long long max_vertices_in_vertex_buffer{ 0 }; + long long max_indices_in_index_buffer{ 0 }; + + void reset_all() { + reset_times(); + reset_opengl(); + reset_sizes(); + reset_others(); + } + + void reset_times() { + results_time = 0; + load_time = 0; + refresh_time = 0; + refresh_paths_time = 0; + } + + void reset_opengl() { + gl_multi_points_calls_count = 0; + gl_multi_lines_calls_count = 0; + gl_multi_triangles_calls_count = 0; + } + + void reset_sizes() { + results_size = 0; + vertices_gpu_size = 0; + indices_gpu_size = 0; + paths_size = 0; + render_paths_size = 0; + } + + void reset_others() { + travel_segments_count = 0; + extrude_segments_count = 0; + max_vertices_in_vertex_buffer = 0; + max_indices_in_index_buffer = 0; + } + }; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + +public: + struct SequentialView + { + class Marker + { + GLModel m_model; + Vec3f m_world_position; + Transform3f m_world_transform; + float m_z_offset{ 0.5f }; + std::array m_color{ 1.0f, 1.0f, 1.0f, 1.0f }; + bool m_visible{ false }; + + public: + void init(); + + const BoundingBoxf3& get_bounding_box() const { return m_model.get_bounding_box(); } + + void set_world_position(const Vec3f& position); + void set_color(const std::array& color) { m_color = color; } + + bool is_visible() const { return m_visible; } + void set_visible(bool visible) { m_visible = visible; } + + void render() const; + }; + + struct Endpoints + { + unsigned int first{ 0 }; + unsigned int last{ 0 }; + }; + + Endpoints endpoints; + Endpoints current; + Vec3f current_position{ Vec3f::Zero() }; + Marker marker; + }; + + enum class EViewType : unsigned char + { + FeatureType, + Height, + Width, + Feedrate, + FanSpeed, + VolumetricRate, + Tool, + ColorPrint, + Count + }; + +private: + unsigned int m_last_result_id{ 0 }; + size_t m_moves_count{ 0 }; + mutable std::vector m_buffers{ static_cast(EMoveType::Extrude) }; + // bounding box of toolpaths + BoundingBoxf3 m_paths_bounding_box; + // bounding box of toolpaths + marker tools + BoundingBoxf3 m_max_bounding_box; + std::vector m_tool_colors; + std::vector m_layers_zs; + std::array m_layers_z_range; + std::vector m_roles; + std::vector m_extruder_ids; + mutable Extrusions m_extrusions; + mutable SequentialView m_sequential_view; + Shells m_shells; + EViewType m_view_type{ EViewType::FeatureType }; + bool m_legend_enabled{ true }; + PrintEstimatedTimeStatistics m_time_statistics; + mutable PrintEstimatedTimeStatistics::ETimeMode m_time_estimate_mode{ PrintEstimatedTimeStatistics::ETimeMode::Normal }; +#if ENABLE_GCODE_VIEWER_STATISTICS + mutable Statistics m_statistics; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + std::array m_detected_point_sizes = { 0.0f, 0.0f }; + +public: + GCodeViewer() = default; + ~GCodeViewer() { reset(); } + + bool init(); + + // extract rendering data from the given parameters + void load(const GCodeProcessor::Result& gcode_result, const Print& print, bool initialized); + // recalculate ranges in dependence of what is visible and sets tool/print colors + void refresh(const GCodeProcessor::Result& gcode_result, const std::vector& str_tool_colors); + + void reset(); + void render() const; + + bool has_data() const { return !m_roles.empty(); } + + const BoundingBoxf3& get_paths_bounding_box() const { return m_paths_bounding_box; } + const BoundingBoxf3& get_max_bounding_box() const { return m_max_bounding_box; } + const std::vector& get_layers_zs() const { return m_layers_zs; }; + + const SequentialView& get_sequential_view() const { return m_sequential_view; } + void update_sequential_view_current(unsigned int first, unsigned int last) + { + m_sequential_view.current.first = first; + m_sequential_view.current.last = last; + refresh_render_paths(true, true); + } + + EViewType get_view_type() const { return m_view_type; } + void set_view_type(EViewType type) { + if (type == EViewType::Count) + type = EViewType::FeatureType; + + m_view_type = type; + } + + bool is_toolpath_move_type_visible(EMoveType type) const; + void set_toolpath_move_type_visible(EMoveType type, bool visible); + unsigned int get_toolpath_role_visibility_flags() const { return m_extrusions.role_visibility_flags; } + void set_toolpath_role_visibility_flags(unsigned int flags) { m_extrusions.role_visibility_flags = flags; } + unsigned int get_options_visibility_flags() const; + void set_options_visibility_from_flags(unsigned int flags); + void set_layers_z_range(const std::array& layers_z_range); + + bool is_legend_enabled() const { return m_legend_enabled; } + void enable_legend(bool enable) { m_legend_enabled = enable; } + + void export_toolpaths_to_obj(const char* filename) const; + +private: + void init_shaders(); + void load_toolpaths(const GCodeProcessor::Result& gcode_result); + void load_shells(const Print& print, bool initialized); + void refresh_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) const; + void render_toolpaths() const; + void render_shells() const; + void render_legend() const; +#if ENABLE_GCODE_VIEWER_STATISTICS + void render_statistics() const; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + bool is_visible(ExtrusionRole role) const { + return role < erCount && (m_extrusions.role_visibility_flags & (1 << role)) != 0; + } + bool is_visible(const Path& path) const { return is_visible(path.role); } + bool is_in_z_range(const Path& path) const { + auto in_z_range = [this](double z) { + return z > m_layers_z_range[0] - EPSILON && z < m_layers_z_range[1] + EPSILON; + }; + + return in_z_range(path.first.position[2]) || in_z_range(path.last.position[2]); + } + bool is_travel_in_z_range(size_t id) const; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // ENABLE_GCODE_VIEWER + +#endif // slic3r_GCodeViewer_hpp_ + diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index b4e672c4fb..e0c8c4c5bd 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -5,7 +5,9 @@ #include "polypartition.h" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/PrintConfig.hpp" +#if !ENABLE_GCODE_VIEWER #include "libslic3r/GCode/PreviewData.hpp" +#endif // !ENABLE_GCODE_VIEWER #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/ExtrusionEntity.hpp" @@ -13,11 +15,11 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/Technologies.hpp" #include "libslic3r/Tesselate.hpp" +#include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/3DScene.hpp" #include "slic3r/GUI/BackgroundSlicingProcess.hpp" #include "slic3r/GUI/GLShader.hpp" #include "slic3r/GUI/GUI.hpp" -#include "slic3r/GUI/PresetBundle.hpp" #include "slic3r/GUI/Tab.hpp" #include "slic3r/GUI/GUI_Preview.hpp" #include "slic3r/GUI/OpenGLManager.hpp" @@ -31,6 +33,7 @@ #include "GUI_ObjectManipulation.hpp" #include "Mouse3DController.hpp" #include "I18N.hpp" +#include "NotificationManager.hpp" #if ENABLE_RETINA_GL #include "slic3r/Utils/RetinaHelper.hpp" @@ -157,11 +160,8 @@ GLCanvas3D::LayersEditing::~LayersEditing() const float GLCanvas3D::LayersEditing::THICKNESS_BAR_WIDTH = 70.0f; -bool GLCanvas3D::LayersEditing::init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename) +void GLCanvas3D::LayersEditing::init() { - if (!m_shader.init(vertex_shader_filename, fragment_shader_filename)) - return false; - glsafe(::glGenTextures(1, (GLuint*)&m_z_texture_id)); glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)); @@ -170,8 +170,6 @@ bool GLCanvas3D::LayersEditing::init(const std::string& vertex_shader_filename, glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST)); glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1)); glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); - - return true; } void GLCanvas3D::LayersEditing::set_config(const DynamicPrintConfig* config) @@ -204,7 +202,7 @@ void GLCanvas3D::LayersEditing::select_object(const Model &model, int object_id) bool GLCanvas3D::LayersEditing::is_allowed() const { - return m_shader.is_initialized() && m_shader.get_shader()->shader_program_id > 0 && m_z_texture_id > 0; + return wxGetApp().get_shader("variable_layer_height") != nullptr && m_z_texture_id > 0; } bool GLCanvas3D::LayersEditing::is_enabled() const @@ -222,59 +220,45 @@ void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas) const if (!m_enabled) return; - static const ImVec4 ORANGE(1.0f, 0.49f, 0.22f, 1.0f); - const Size& cnv_size = canvas.get_canvas_size(); - float canvas_w = (float)cnv_size.get_width(); - float canvas_h = (float)cnv_size.get_height(); ImGuiWrapper& imgui = *wxGetApp().imgui(); - imgui.set_next_window_pos(canvas_w - imgui.get_style_scaling() * THICKNESS_BAR_WIDTH, canvas_h, ImGuiCond_Always, 1.0f, 1.0f); + imgui.set_next_window_pos(static_cast(cnv_size.get_width()) - imgui.get_style_scaling() * THICKNESS_BAR_WIDTH, + static_cast(cnv_size.get_height()), ImGuiCond_Always, 1.0f, 1.0f); - imgui.begin(_(L("Variable layer height")), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); + imgui.begin(_L("Variable layer height"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); - ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); - imgui.text(_(L("Left mouse button:"))); - ImGui::PopStyleColor(); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Left mouse button:")); ImGui::SameLine(); - imgui.text(_(L("Add detail"))); + imgui.text(_L("Add detail")); - ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); - imgui.text(_(L("Right mouse button:"))); - ImGui::PopStyleColor(); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Right mouse button:")); ImGui::SameLine(); - imgui.text(_(L("Remove detail"))); + imgui.text(_L("Remove detail")); - ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); - imgui.text(_(L("Shift + Left mouse button:"))); - ImGui::PopStyleColor(); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Left mouse button:")); ImGui::SameLine(); - imgui.text(_(L("Reset to base"))); + imgui.text(_L("Reset to base")); - ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); - imgui.text(_(L("Shift + Right mouse button:"))); - ImGui::PopStyleColor(); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Right mouse button:")); ImGui::SameLine(); - imgui.text(_(L("Smoothing"))); + imgui.text(_L("Smoothing")); - ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); - imgui.text(_(L("Mouse wheel:"))); - ImGui::PopStyleColor(); + imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Mouse wheel:")); ImGui::SameLine(); - imgui.text(_(L("Increase/decrease edit area"))); + imgui.text(_L("Increase/decrease edit area")); ImGui::Separator(); - if (imgui.button(_(L("Adaptive")))) + if (imgui.button(_L("Adaptive"))) wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), Event(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, m_adaptive_quality)); ImGui::SameLine(); float text_align = ImGui::GetCursorPosX(); ImGui::AlignTextToFramePadding(); - imgui.text(_(L("Quality / Speed"))); - if (ImGui::IsItemHovered()) - { + imgui.text(_L("Quality / Speed")); + if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); - ImGui::TextUnformatted(_(L("Higher print quality versus higher print speed.")).ToUTF8()); + ImGui::TextUnformatted(_L("Higher print quality versus higher print speed.").ToUTF8()); ImGui::EndTooltip(); } @@ -285,13 +269,13 @@ void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas) const ImGui::SliderFloat("", &m_adaptive_quality, 0.0f, 1.f, "%.2f"); ImGui::Separator(); - if (imgui.button(_(L("Smooth")))) + if (imgui.button(_L("Smooth"))) wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), HeightProfileSmoothEvent(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, m_smooth_params)); ImGui::SameLine(); ImGui::SetCursorPosX(text_align); ImGui::AlignTextToFramePadding(); - imgui.text(_(L("Radius"))); + imgui.text(_L("Radius")); ImGui::SameLine(); ImGui::SetCursorPosX(widget_align); ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f); @@ -301,7 +285,7 @@ void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas) const ImGui::SetCursorPosX(text_align); ImGui::AlignTextToFramePadding(); - imgui.text(_(L("Keep min"))); + imgui.text(_L("Keep min")); ImGui::SameLine(); if (ImGui::GetCursorPosX() < widget_align) // because of line lenght after localization ImGui::SetCursorPosX(widget_align); @@ -310,7 +294,7 @@ void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas) const imgui.checkbox("##2", m_smooth_params.keep_min); ImGui::Separator(); - if (imgui.button(_(L("Reset")))) + if (imgui.button(_L("Reset"))) wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), SimpleEvent(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE)); imgui.end(); @@ -362,7 +346,7 @@ Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas) bool GLCanvas3D::LayersEditing::is_initialized() const { - return m_shader.is_initialized(); + return wxGetApp().get_shader("variable_layer_height") != nullptr; } std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) const @@ -396,13 +380,17 @@ std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) con void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect) const { - m_shader.start_using(); + GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height"); + if (shader == nullptr) + return; - m_shader.set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * m_object_max_z)); - m_shader.set_uniform("z_texture_row_to_normalized", 1.0f / (float)m_layers_texture.height); - m_shader.set_uniform("z_cursor", m_object_max_z * this->get_cursor_z_relative(canvas)); - m_shader.set_uniform("z_cursor_band_width", band_width); - m_shader.set_uniform("object_max_z", m_object_max_z); + shader->start_using(); + + shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * m_object_max_z)); + shader->set_uniform("z_texture_row_to_normalized", 1.0f / (float)m_layers_texture.height); + shader->set_uniform("z_cursor", m_object_max_z * this->get_cursor_z_relative(canvas)); + shader->set_uniform("z_cursor_band_width", band_width); + shader->set_uniform("object_max_z", m_object_max_z); glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); @@ -422,7 +410,7 @@ void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3 glsafe(::glEnd()); glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); - m_shader.stop_using(); + shader->stop_using(); } void GLCanvas3D::LayersEditing::render_profile(const Rect& bar_rect) const @@ -456,73 +444,50 @@ void GLCanvas3D::LayersEditing::render_volumes(const GLCanvas3D& canvas, const G { assert(this->is_allowed()); assert(this->last_object_id != -1); - GLint shader_id = m_shader.get_shader()->shader_program_id; - assert(shader_id > 0); + GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height"); + if (shader == nullptr) + return; - GLint current_program_id; - glsafe(::glGetIntegerv(GL_CURRENT_PROGRAM, ¤t_program_id)); - if (shader_id > 0 && shader_id != current_program_id) + GLShaderProgram* current_shader = wxGetApp().get_current_shader(); + if (shader->get_id() != current_shader->get_id()) // The layer editing shader is not yet active. Activate it. - glsafe(::glUseProgram(shader_id)); + shader->start_using(); else // The layer editing shader was already active. - current_program_id = -1; + current_shader = nullptr; - GLint z_to_texture_row_id = ::glGetUniformLocation(shader_id, "z_to_texture_row"); - GLint z_texture_row_to_normalized_id = ::glGetUniformLocation(shader_id, "z_texture_row_to_normalized"); - GLint z_cursor_id = ::glGetUniformLocation(shader_id, "z_cursor"); - GLint z_cursor_band_width_id = ::glGetUniformLocation(shader_id, "z_cursor_band_width"); - GLint world_matrix_id = ::glGetUniformLocation(shader_id, "volume_world_matrix"); - GLint object_max_z_id = ::glGetUniformLocation(shader_id, "object_max_z"); - glcheck(); + const_cast(this)->generate_layer_height_texture(); - if (z_to_texture_row_id != -1 && z_texture_row_to_normalized_id != -1 && z_cursor_id != -1 && z_cursor_band_width_id != -1 && world_matrix_id != -1) - { - const_cast(this)->generate_layer_height_texture(); + // Uniforms were resolved, go ahead using the layer editing shader. + shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * float(m_object_max_z))); + shader->set_uniform("z_texture_row_to_normalized", 1.0f / float(m_layers_texture.height)); + shader->set_uniform("z_cursor", float(m_object_max_z) * float(this->get_cursor_z_relative(canvas))); + shader->set_uniform("z_cursor_band_width", float(this->band_width)); - // Uniforms were resolved, go ahead using the layer editing shader. - glsafe(::glUniform1f(z_to_texture_row_id, GLfloat(m_layers_texture.cells - 1) / (GLfloat(m_layers_texture.width) * GLfloat(m_object_max_z)))); - glsafe(::glUniform1f(z_texture_row_to_normalized_id, GLfloat(1.0f / m_layers_texture.height))); - glsafe(::glUniform1f(z_cursor_id, GLfloat(m_object_max_z) * GLfloat(this->get_cursor_z_relative(canvas)))); - glsafe(::glUniform1f(z_cursor_band_width_id, GLfloat(this->band_width))); - // Initialize the layer height texture mapping. - GLsizei w = (GLsizei)m_layers_texture.width; - GLsizei h = (GLsizei)m_layers_texture.height; - GLsizei half_w = w / 2; - GLsizei half_h = h / 2; - glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); - glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); - glsafe(::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); - glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data())); - glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data() + m_layers_texture.width * m_layers_texture.height * 4)); - for (const GLVolume* glvolume : volumes.volumes) { - // Render the object using the layer editing shader and texture. - if (! glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier) - continue; - if (world_matrix_id != -1) - glsafe(::glUniformMatrix4fv(world_matrix_id, 1, GL_FALSE, (const GLfloat*)glvolume->world_matrix().cast().data())); - if (object_max_z_id != -1) - glsafe(::glUniform1f(object_max_z_id, GLfloat(0))); - glvolume->render(); - } - // Revert back to the previous shader. - glBindTexture(GL_TEXTURE_2D, 0); - if (current_program_id > 0) - glsafe(::glUseProgram(current_program_id)); - } - else - { - // Something went wrong. Just render the object. - assert(false); - for (const GLVolume* glvolume : volumes.volumes) { - // Render the object using the layer editing shader and texture. - if (!glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier) - continue; - glsafe(::glUniformMatrix4fv(world_matrix_id, 1, GL_FALSE, (const GLfloat*)glvolume->world_matrix().cast().data())); - glvolume->render(); - } - } + // Initialize the layer height texture mapping. + GLsizei w = (GLsizei)m_layers_texture.width; + GLsizei h = (GLsizei)m_layers_texture.height; + GLsizei half_w = w / 2; + GLsizei half_h = h / 2; + glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1)); + glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); + glsafe(::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0)); + glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data())); + glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data() + m_layers_texture.width * m_layers_texture.height * 4)); + for (const GLVolume* glvolume : volumes.volumes) { + // Render the object using the layer editing shader and texture. + if (! glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier) + continue; + + shader->set_uniform("volume_world_matrix", glvolume->world_matrix()); + shader->set_uniform("object_max_z", GLfloat(0)); + glvolume->render(); + } + // Revert back to the previous shader. + glBindTexture(GL_TEXTURE_2D, 0); + if (current_shader != nullptr) + current_shader->start_using(); } void GLCanvas3D::LayersEditing::adjust_layer_height_profile() @@ -655,13 +620,43 @@ GLCanvas3D::WarningTexture::WarningTexture() void GLCanvas3D::WarningTexture::activate(WarningTexture::Warning warning, bool state, const GLCanvas3D& canvas) { + // Since we have NotificationsManager.hpp the warning textures are no loger needed. + // However i have left the infrastructure here and only commented the rendering. + // The plater warning / error notifications are added and closed from here. + + std::string text; + bool error = false; + switch (warning) { + case ObjectOutside: text = L("An object outside the print area was detected."); break; + case ToolpathOutside: text = L("A toolpath outside the print area was detected."); break; + case SlaSupportsOutside: text = L("SLA supports outside the print area were detected."); break; + case SomethingNotShown: text = L("Some objects are not visible."); break; + case ObjectClashed: + text = L( "An object outside the print area was detected.\n" + "Resolve the current problem to continue slicing."); + error = true; + break; + } + if(state) { + if(error) + wxGetApp().plater()->get_notification_manager()->push_plater_error_notification(text,*(wxGetApp().plater()->get_current_canvas3D())); + else + wxGetApp().plater()->get_notification_manager()->push_plater_warning_notification(text, *(wxGetApp().plater()->get_current_canvas3D())); + } else { + if (error) + wxGetApp().plater()->get_notification_manager()->close_plater_error_notification(); + else + wxGetApp().plater()->get_notification_manager()->close_plater_warning_notification(text); + } + + /* auto it = std::find(m_warnings.begin(), m_warnings.end(), warning); if (state) { if (it != m_warnings.end()) // this warning is already set to be shown return; - m_warnings.emplace_back(warning); + m_warnings.push_back(warning); std::sort(m_warnings.begin(), m_warnings.end()); } else { @@ -683,7 +678,7 @@ void GLCanvas3D::WarningTexture::activate(WarningTexture::Warning warning, bool case ObjectOutside : text = L("An object outside the print area was detected"); break; case ToolpathOutside : text = L("A toolpath outside the print area was detected"); break; case SlaSupportsOutside : text = L("SLA supports outside the print area were detected"); break; - case SomethingNotShown : text = L("Some objects are not visible"); break; + case SomethingNotShown : text = L("Some objects are not visible when editing supports"); break; case ObjectClashed: { text = L("An object outside the print area was detected\n" "Resolve the current problem to continue slicing"); @@ -697,6 +692,7 @@ void GLCanvas3D::WarningTexture::activate(WarningTexture::Warning warning, bool // save information for rescaling m_msg_text = text; m_is_colored_red = red_colored; + */ } @@ -883,6 +879,7 @@ void GLCanvas3D::WarningTexture::msw_rescale(const GLCanvas3D& canvas) generate(m_msg_text, canvas, true, m_is_colored_red); } +#if !ENABLE_GCODE_VIEWER const unsigned char GLCanvas3D::LegendTexture::Squares_Border_Color[3] = { 64, 64, 64 }; const unsigned char GLCanvas3D::LegendTexture::Default_Background_Color[3] = { (unsigned char)(DEFAULT_BG_LIGHT_COLOR[0] * 255.0f), (unsigned char)(DEFAULT_BG_LIGHT_COLOR[1] * 255.0f), (unsigned char)(DEFAULT_BG_LIGHT_COLOR[2] * 255.0f) }; const unsigned char GLCanvas3D::LegendTexture::Error_Background_Color[3] = { (unsigned char)(ERROR_BG_LIGHT_COLOR[0] * 255.0f), (unsigned char)(ERROR_BG_LIGHT_COLOR[1] * 255.0f), (unsigned char)(ERROR_BG_LIGHT_COLOR[2] * 255.0f) }; @@ -1241,6 +1238,7 @@ void GLCanvas3D::LegendTexture::render(const GLCanvas3D& canvas) const GLTexture::render_sub_texture(m_id, left, right, bottom, top, uvs); } } +#endif // !ENABLE_GCODE_VIEWER void GLCanvas3D::Labels::render(const std::vector& sorted_instances) const { @@ -1430,8 +1428,7 @@ void GLCanvas3D::Tooltip::render(const Vec2d& mouse_position, GLCanvas3D& canvas #if ENABLE_SLOPE_RENDERING void GLCanvas3D::Slope::render() const { - if (m_dialog_shown) - { + if (m_dialog_shown) { const std::array& z_range = m_volumes.get_slope_z_range(); std::array angle_range = { Geometry::rad2deg(::acos(z_range[0])) - 90.0f, Geometry::rad2deg(::acos(z_range[1])) - 90.0f }; bool modified = false; @@ -1439,9 +1436,9 @@ void GLCanvas3D::Slope::render() const ImGuiWrapper& imgui = *wxGetApp().imgui(); const Size& cnv_size = m_canvas.get_canvas_size(); imgui.set_next_window_pos((float)cnv_size.get_width(), (float)cnv_size.get_height(), ImGuiCond_Always, 1.0f, 1.0f); - imgui.begin(_(L("Slope visualization")), nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + imgui.begin(_L("Slope visualization"), nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - imgui.text(_(L("Facets' slope range (degrees)")) + ":"); + imgui.text(_L("Facets' slope range (degrees)") + ":"); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.75f, 0.0f, 0.0f, 0.5f)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(1.0f, 0.0f, 0.0f, 0.5f)); @@ -1453,8 +1450,7 @@ void GLCanvas3D::Slope::render() const float slope_bound = 90.f - angle_range[1]; bool mod = ImGui::SliderFloat("##red", &slope_bound, 0.0f, 90.0f, "%.1f"); angle_range[1] = 90.f - slope_bound; - if (mod) - { + if (mod) { modified = true; if (angle_range[0] > angle_range[1]) angle_range[0] = angle_range[1]; @@ -1462,15 +1458,14 @@ void GLCanvas3D::Slope::render() const ImGui::PopStyleColor(4); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.75f, 0.75f, 0.0f, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(1.0f, 1.0f, 0.0f, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.85f, 0.85f, 0.0f, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_SliderGrab, ImVec4(0.25f, 0.25f, 0.0f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(1.0f, 1.0f, 0.0f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(0.85f, 0.85f, 0.0f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, ImVec4(0.25f, 0.25f, 0.0f, 1.0f)); slope_bound = 90.f - angle_range[0]; mod = ImGui::SliderFloat("##yellow", &slope_bound, 0.0f, 90.0f, "%.1f"); angle_range[0] = 90.f - slope_bound; - if (mod) - { + if (mod) { modified = true; if (angle_range[1] < angle_range[0]) angle_range[1] = angle_range[0]; @@ -1515,7 +1510,11 @@ wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent); +#if ENABLE_GCODE_VIEWER +wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_LAYERS_SLIDER, wxKeyEvent); +#else wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_DOUBLE_SLIDER, wxKeyEvent); +#endif // ENABLE_GCODE_VIEWER wxDEFINE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent); wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent); @@ -1546,7 +1545,9 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas) , m_dirty(true) , m_initialized(false) , m_apply_zoom_to_volumes_filter(false) +#if !ENABLE_GCODE_VIEWER , m_legend_texture_enabled(false) +#endif // !ENABLE_GCODE_VIEWER , m_picking_enabled(false) , m_moving_enabled(false) , m_dynamic_background_enabled(false) @@ -1637,17 +1638,16 @@ bool GLCanvas3D::init() if (m_multisample_allowed) glsafe(::glEnable(GL_MULTISAMPLE)); - if (!m_shader.init("gouraud.vs", "gouraud.fs")) - { - std::cout << "Unable to initialize gouraud shader: please, check that the files gouraud.vs and gouraud.fs are available" << std::endl; - return false; - } + if (m_main_toolbar.is_enabled()) + m_layers_editing.init(); - if (m_main_toolbar.is_enabled() && !m_layers_editing.init("variable_layer_height.vs", "variable_layer_height.fs")) +#if ENABLE_GCODE_VIEWER + if (!m_main_toolbar.is_enabled()) { - std::cout << "Unable to initialize variable_layer_height shader: please, check that the files variable_layer_height.vs and variable_layer_height.fs are available" << std::endl; - return false; + if (!m_gcode_viewer.init()) + return false; } +#endif // ENABLE_GCODE_VIEWER // on linux the gl context is not valid until the canvas is not shown on screen // we defer the geometry finalization of volumes until the first call to render() @@ -1879,7 +1879,11 @@ void GLCanvas3D::enable_layers_editing(bool enable) void GLCanvas3D::enable_legend_texture(bool enable) { +#if ENABLE_GCODE_VIEWER + m_gcode_viewer.enable_legend(enable); +#else m_legend_texture_enabled = enable; +#endif // ENABLE_GCODE_VIEWER } void GLCanvas3D::enable_picking(bool enable) @@ -1941,6 +1945,13 @@ void GLCanvas3D::zoom_to_selection() _zoom_to_box(m_selection.get_bounding_box()); } +#if ENABLE_GCODE_VIEWER +void GLCanvas3D::zoom_to_gcode() +{ + _zoom_to_box(m_gcode_viewer.get_paths_bounding_box(), 1.05); +} +#endif // ENABLE_GCODE_VIEWER + void GLCanvas3D::select_view(const std::string& direction) { wxGetApp().plater()->get_camera().select_view(direction); @@ -2036,6 +2047,10 @@ void GLCanvas3D::render() _render_background(); _render_objects(); +#if ENABLE_GCODE_VIEWER + if (!m_main_toolbar.is_enabled()) + _render_gcode(); +#endif // ENABLE_GCODE_VIEWER _render_sla_slices(); _render_selection(); _render_bed(!camera.is_looking_downward(), true); @@ -2047,6 +2062,9 @@ void GLCanvas3D::render() // we need to set the mouse's scene position here because the depth buffer // could be invalidated by the following gizmo render methods // this position is used later into on_mouse() to drag the objects +#if ENABLE_GCODE_VIEWER + if (m_picking_enabled) +#endif // ENABLE_GCODE_VIEWER m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast()); _render_current_gizmo(); @@ -2084,11 +2102,13 @@ void GLCanvas3D::render() #endif // ENABLE_RENDER_STATISTICS #if ENABLE_CAMERA_STATISTICS - m_camera.debug_render(); + camera.debug_render(); #endif // ENABLE_CAMERA_STATISTICS std::string tooltip; + + // Negative coordinate means out of the window, likely because the window was deactivated. // In that case the tooltip should be hidden. if (m_mouse.position.x() >= 0. && m_mouse.position.y() >= 0.) @@ -2118,6 +2138,8 @@ void GLCanvas3D::render() m_tooltip.render(m_mouse.position, *this); wxGetApp().plater()->get_mouse3d_controller().render_settings_dialog(*this); + + wxGetApp().plater()->get_notification_manager()->render_notifications(*this); wxGetApp().imgui()->render(); @@ -2187,6 +2209,40 @@ void GLCanvas3D::ensure_on_bed(unsigned int object_idx) } } + +#if ENABLE_GCODE_VIEWER +const std::vector& GLCanvas3D::get_gcode_layers_zs() const +{ + return m_gcode_viewer.get_layers_zs(); +} + +std::vector GLCanvas3D::get_volumes_print_zs(bool active_only) const +{ + return m_volumes.get_current_print_zs(active_only); +} + +void GLCanvas3D::set_gcode_options_visibility_from_flags(unsigned int flags) +{ + m_gcode_viewer.set_options_visibility_from_flags(flags); +} + +void GLCanvas3D::set_toolpath_role_visibility_flags(unsigned int flags) +{ + m_gcode_viewer.set_toolpath_role_visibility_flags(flags); +} + +void GLCanvas3D::set_toolpath_view_type(GCodeViewer::EViewType type) +{ + m_gcode_viewer.set_view_type(type); +} + +void GLCanvas3D::set_toolpaths_z_range(const std::array& range) +{ + m_volumes.set_range(range[0] - 1e-6, range[1] + 1e-6); + if (m_gcode_viewer.has_data()) + m_gcode_viewer.set_layers_z_range(range); +} +#else std::vector GLCanvas3D::get_current_print_zs(bool active_only) const { return m_volumes.get_current_print_zs(active_only); @@ -2196,6 +2252,7 @@ void GLCanvas3D::set_toolpaths_range(double low, double high) { m_volumes.set_range(low, high); } +#endif // ENABLE_GCODE_VIEWER std::vector GLCanvas3D::load_object(const ModelObject& model_object, int obj_idx, std::vector instance_idxs) { @@ -2638,6 +2695,7 @@ static void reserve_new_volume_finalize_old_volume(GLVolume& vol_new, GLVolume& vol_old.finalize_geometry(gl_initialized); } +#if !ENABLE_GCODE_VIEWER static void load_gcode_retractions(const GCodePreviewData::Retraction& retractions, GLCanvas3D::GCodePreviewVolumeIndex::EType extrusion_type, GLVolumeCollection &volumes, GLCanvas3D::GCodePreviewVolumeIndex &volume_index, bool gl_initialized) { // nothing to render, return @@ -2668,7 +2726,23 @@ static void load_gcode_retractions(const GCodePreviewData::Retraction& retractio } volume->indexed_vertex_array.finalize_geometry(gl_initialized); } +#endif // !ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER +void GLCanvas3D::load_gcode_preview(const GCodeProcessor::Result& gcode_result) +{ + m_gcode_viewer.load(gcode_result, *this->fff_print(), m_initialized); + if (wxGetApp().is_editor()) + _show_warning_texture_if_needed(WarningTexture::ToolpathOutside); +} + +void GLCanvas3D::refresh_gcode_preview(const GCodeProcessor::Result& gcode_result, const std::vector& str_tool_colors) +{ + m_gcode_viewer.refresh(gcode_result, str_tool_colors); + set_as_dirty(); + request_extra_frame(); +} +#else void GLCanvas3D::load_gcode_preview(const GCodePreviewData& preview_data, const std::vector& str_tool_colors) { const Print *print = this->fff_print(); @@ -2737,6 +2811,7 @@ void GLCanvas3D::load_gcode_preview(const GCodePreviewData& preview_data, const _generate_legend_texture(preview_data, tool_colors); } } +#endif // ENABLE_GCODE_VIEWER void GLCanvas3D::load_sla_preview() { @@ -2770,6 +2845,7 @@ void GLCanvas3D::load_preview(const std::vector& str_tool_colors, c _update_toolpath_volumes_outside_state(); _show_warning_texture_if_needed(WarningTexture::ToolpathOutside); +#if !ENABLE_GCODE_VIEWER if (color_print_values.empty()) reset_legend_texture(); else { @@ -2778,6 +2854,7 @@ void GLCanvas3D::load_preview(const std::vector& str_tool_colors, c const std::vector tool_colors = _parse_colors(str_tool_colors); _generate_legend_texture(preview_data, tool_colors); } +#endif // !ENABLE_GCODE_VIEWER } void GLCanvas3D::bind_event_handlers() @@ -3015,18 +3092,46 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) case 'i': { _update_camera_zoom(1.0); break; } case 'K': case 'k': { wxGetApp().plater()->get_camera().select_next_type(); m_dirty = true; break; } +#if ENABLE_GCODE_VIEWER + case 'L': + case 'l': { + if (!m_main_toolbar.is_enabled()) { + m_gcode_viewer.enable_legend(!m_gcode_viewer.is_legend_enabled()); + m_dirty = true; + wxGetApp().plater()->update_preview_bottom_toolbar(); + } + break; + } +#endif // ENABLE_GCODE_VIEWER case 'O': case 'o': { _update_camera_zoom(-1.0); break; } #if ENABLE_RENDER_PICKING_PASS - case 'T': - case 't': { + case 'P': + case 'p': { m_show_picking_texture = !m_show_picking_texture; - m_dirty = true; + m_dirty = true; break; } #endif // ENABLE_RENDER_PICKING_PASS case 'Z': +#if ENABLE_GCODE_VIEWER + case 'z': + { + if (!m_selection.is_empty()) + zoom_to_selection(); + else + { + if (!m_volumes.empty()) + zoom_to_volumes(); + else + _zoom_to_box(m_gcode_viewer.get_paths_bounding_box()); + } + + break; + } +#else case 'z': { m_selection.is_empty() ? zoom_to_volumes() : zoom_to_selection(); break; } +#endif // ENABLE_GCODE_VIEWER default: { evt.Skip(); break; } } } @@ -3269,7 +3374,11 @@ void GLCanvas3D::on_key(wxKeyEvent& evt) keyCode == WXK_DOWN) { if (dynamic_cast(m_canvas->GetParent()) != nullptr) +#if ENABLE_GCODE_VIEWER + post_event(wxKeyEvent(EVT_GLCANVAS_MOVE_LAYERS_SLIDER, evt)); +#else post_event(wxKeyEvent(EVT_GLCANVAS_MOVE_DOUBLE_SLIDER, evt)); +#endif // ENABLE_GCODE_VIEWER } } } @@ -3300,6 +3409,11 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) if (evt.MiddleIsDown()) return; + if (wxGetApp().imgui()->update_mouse_data(evt)) { + m_dirty = true; + return; + } + #if ENABLE_RETINA_GL const float scale = m_retina_helper->get_scale_factor(); evt.SetX(evt.GetX() * scale); @@ -3428,6 +3542,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) #ifdef SLIC3R_DEBUG_MOUSE_EVENTS printf((format_mouse_event_debug_message(evt) + " - Consumed by ImGUI\n").c_str()); #endif /* SLIC3R_DEBUG_MOUSE_EVENTS */ + m_dirty = true; // do not return if dragging or tooltip not empty to allow for tooltip update if (!m_mouse.dragging && m_tooltip.is_empty()) return; @@ -3559,7 +3674,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) else if (evt.LeftDown() && (evt.ShiftDown() || evt.AltDown()) && m_picking_enabled) { if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports - && m_gizmos.get_current_type() != GLGizmosManager::FdmSupports) + && m_gizmos.get_current_type() != GLGizmosManager::FdmSupports + && m_gizmos.get_current_type() != GLGizmosManager::Seam) { m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::Select : GLSelectionRectangle::Deselect); m_dirty = true; @@ -3634,7 +3750,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_mouse.drag.move_requires_threshold) { m_mouse.dragging = true; - Vec3d cur_pos = m_mouse.drag.start_position_3D; // we do not want to translate objects if the user just clicked on an object while pressing shift to remove it from the selection and then drag if (m_selection.contains_volume(get_first_hover_volume_idx())) @@ -3820,9 +3935,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (m_selection.is_empty()) m_gizmos.reset_all_states(); +#if ENABLE_GCODE_VIEWER + m_dirty = true; +#else // Only refresh if picking is enabled, in that case the objects may get highlighted if the mouse cursor hovers over. - if (m_picking_enabled) + //if (m_picking_enabled) m_dirty = true; +#endif // ENABLE_GCODE_VIEWER } else evt.Skip(); @@ -3883,6 +4002,7 @@ Vec2d GLCanvas3D::get_local_mouse_position() const return Vec2d(factor * mouse_pos.x, factor * mouse_pos.y); } +#if !ENABLE_GCODE_VIEWER void GLCanvas3D::reset_legend_texture() { if (m_legend_texture.get_id() != 0) @@ -3891,6 +4011,7 @@ void GLCanvas3D::reset_legend_texture() m_legend_texture.reset(); } } +#endif // !ENABLE_GCODE_VIEWER void GLCanvas3D::set_tooltip(const std::string& tooltip) const { @@ -4180,12 +4301,15 @@ void GLCanvas3D::update_ui_from_settings() } #endif // ENABLE_RETINA_GL +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor()) + wxGetApp().plater()->get_collapse_toolbar().set_enabled(wxGetApp().app_config->get("show_collapse_button") == "1"); +#else bool enable_collapse = wxGetApp().app_config->get("show_collapse_button") == "1"; wxGetApp().plater()->get_collapse_toolbar().set_enabled(enable_collapse); +#endif // ENABLE_GCODE_VIEWER } - - GLCanvas3D::WipeTowerInfo GLCanvas3D::get_wipe_tower_info() const { WipeTowerInfo wti; @@ -4247,12 +4371,20 @@ void GLCanvas3D::update_tooltip_for_settings_item_in_main_toolbar() bool GLCanvas3D::has_toolpaths_to_export() const { +#if ENABLE_GCODE_VIEWER + return m_gcode_viewer.has_data(); +#else return m_volumes.has_toolpaths_to_export(); +#endif // ENABLE_GCODE_VIEWER } void GLCanvas3D::export_toolpaths_to_obj(const char* filename) const { +#if ENABLE_GCODE_VIEWER + m_gcode_viewer.export_toolpaths_to_obj(filename); +#else m_volumes.export_toolpaths_to_obj(filename); +#endif // ENABLE_GCODE_VIEWER } void GLCanvas3D::mouse_up_cleanup() @@ -4402,8 +4534,8 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, bool return ret; }; - static const GLfloat orange[] = { 0.923f, 0.504f, 0.264f, 1.0f }; - static const GLfloat gray[] = { 0.64f, 0.64f, 0.64f, 1.0f }; + static const std::array orange = { 0.923f, 0.504f, 0.264f, 1.0f }; + static const std::array gray = { 0.64f, 0.64f, 0.64f, 1.0f }; GLVolumePtrs visible_volumes; @@ -4447,39 +4579,33 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, bool camera.apply_projection(box, near_z, far_z); + GLShaderProgram* shader = wxGetApp().get_shader("gouraud"); + if (shader == nullptr) + return; + if (transparent_background) glsafe(::glClearColor(0.0f, 0.0f, 0.0f, 0.0f)); glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_DEPTH_TEST)); - m_shader.start_using(); - - GLint shader_id = m_shader.get_shader_program_id(); - GLint color_id = ::glGetUniformLocation(shader_id, "uniform_color"); - GLint print_box_detection_id = ::glGetUniformLocation(shader_id, "print_box.volume_detection"); - glcheck(); - - if (print_box_detection_id != -1) - glsafe(::glUniform1i(print_box_detection_id, 0)); + shader->start_using(); + shader->set_uniform("print_box.volume_detection", 0); for (const GLVolume* vol : visible_volumes) { - if (color_id >= 0) - glsafe(::glUniform4fv(color_id, 1, (vol->printable && !vol->is_outside) ? orange : gray)); - else - glsafe(::glColor4fv((vol->printable && !vol->is_outside) ? orange : gray)); - + shader->set_uniform("uniform_color", (vol->printable && !vol->is_outside) ? orange : gray); vol->render(); } - m_shader.stop_using(); + shader->stop_using(); glsafe(::glDisable(GL_DEPTH_TEST)); if (show_bed) _render_bed(!camera.is_looking_downward(), false); + // restore background color if (transparent_background) glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f)); } @@ -4873,7 +4999,7 @@ bool GLCanvas3D::_init_main_toolbar() return false; item.name = "settings"; - item.icon_filename = "cog_.svg"; + item.icon_filename = "settings.svg"; item.tooltip = _u8L("Switch to Settings") + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab") + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (m_process->current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ; @@ -4885,35 +5011,16 @@ bool GLCanvas3D::_init_main_toolbar() if (!m_main_toolbar.add_item(item)) return false; + /* if (!m_main_toolbar.add_separator()) return false; - - item.name = "layersediting"; - item.icon_filename = "layers_white.svg"; - item.tooltip = _utf8(L("Variable layer height")); - item.sprite_id = 11; - item.left.toggable = true; - item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); }; - item.visibility_callback = [this]()->bool - { - bool res = m_process->current_printer_technology() == ptFFF; - // turns off if changing printer technology - if (!res && m_main_toolbar.is_item_visible("layersediting") && m_main_toolbar.is_item_pressed("layersediting")) - force_main_toolbar_left_action(get_main_toolbar_item_id("layersediting")); - - return res; - }; - item.enabling_callback = []()->bool { return wxGetApp().plater()->can_layers_editing(); }; - if (!m_main_toolbar.add_item(item)) - return false; - - if (!m_main_toolbar.add_separator()) - return false; + */ item.name = "search"; item.icon_filename = "search_.svg"; item.tooltip = _utf8(L("Search")) + " [" + GUI::shortkey_ctrl_prefix() + "F]"; - item.sprite_id = 12; + item.sprite_id = 11; + item.left.toggable = true; item.left.render_callback = [this](float left, float right, float, float) { if (m_canvas != nullptr) { @@ -4927,6 +5034,27 @@ bool GLCanvas3D::_init_main_toolbar() if (!m_main_toolbar.add_item(item)) return false; + if (!m_main_toolbar.add_separator()) + return false; + + item.name = "layersediting"; + item.icon_filename = "layers_white.svg"; + item.tooltip = _utf8(L("Variable layer height")); + item.sprite_id = 12; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); }; + item.visibility_callback = [this]()->bool { + bool res = m_process->current_printer_technology() == ptFFF; + // turns off if changing printer technology + if (!res && m_main_toolbar.is_item_visible("layersediting") && m_main_toolbar.is_item_pressed("layersediting")) + force_main_toolbar_left_action(get_main_toolbar_item_id("layersediting")); + + return res; + }; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_layers_editing(); }; + item.left.render_callback = GLToolbarItem::Default_Render_Callback; + if (!m_main_toolbar.add_item(item)) + return false; + return true; } @@ -5035,10 +5163,10 @@ bool GLCanvas3D::_init_undoredo_toolbar() if (!m_undoredo_toolbar.add_item(item)) return false; - + /* if (!m_undoredo_toolbar.add_separator()) return false; - + */ return true; } @@ -5090,6 +5218,12 @@ BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_be } bb.merge(wxGetApp().plater()->get_bed().get_bounding_box(include_bed_model)); + +#if ENABLE_GCODE_VIEWER + if (!m_main_toolbar.is_enabled()) + bb.merge(m_gcode_viewer.get_max_bounding_box()); +#endif // ENABLE_GCODE_VIEWER + return bb; } @@ -5250,8 +5384,40 @@ void GLCanvas3D::_rectangular_selection_picking_pass() const _update_volumes_hover_state(); } +#if ENABLE_GCODE_VIEWER +static BoundingBoxf3 print_volume(const DynamicPrintConfig& config) +{ + // tolerance to avoid false detection at bed edges + const double tolerance_x = 0.05; + const double tolerance_y = 0.05; + + BoundingBoxf3 ret; + const ConfigOptionPoints* opt = dynamic_cast(config.option("bed_shape")); + if (opt != nullptr) { + BoundingBox bed_box_2D = get_extents(Polygon::new_scale(opt->values)); + ret = BoundingBoxf3(Vec3d(unscale(bed_box_2D.min(0)) - tolerance_x, unscale(bed_box_2D.min(1)) - tolerance_y, 0.0), Vec3d(unscale(bed_box_2D.max(0)) + tolerance_x, unscale(bed_box_2D.max(1)) + tolerance_y, config.opt_float("max_print_height"))); + // Allow the objects to protrude below the print bed + ret.min(2) = -1e10; + } + return ret; +} +#endif // ENABLE_GCODE_VIEWER + void GLCanvas3D::_render_background() const { +#if ENABLE_GCODE_VIEWER + bool use_error_color = false; + if (wxGetApp().is_editor()) { + use_error_color = m_dynamic_background_enabled; + if (!m_volumes.empty()) + use_error_color &= _is_any_volume_outside(); + else { + BoundingBoxf3 test_volume = (m_config != nullptr) ? print_volume(*m_config) : BoundingBoxf3(); + use_error_color &= (test_volume.radius() > 0.0) ? !test_volume.contains(m_gcode_viewer.get_paths_bounding_box()) : false; + } + } +#endif // ENABLE_GCODE_VIEWER + glsafe(::glPushMatrix()); glsafe(::glLoadIdentity()); glsafe(::glMatrixMode(GL_PROJECTION)); @@ -5262,7 +5428,11 @@ void GLCanvas3D::_render_background() const glsafe(::glDisable(GL_DEPTH_TEST)); ::glBegin(GL_QUADS); +#if ENABLE_GCODE_VIEWER + if (use_error_color) +#else if (m_dynamic_background_enabled && _is_any_volume_outside()) +#endif // ENABLE_GCODE_VIEWER ::glColor3fv(ERROR_BG_DARK_COLOR); else ::glColor3fv(DEFAULT_BG_DARK_COLOR); @@ -5270,8 +5440,12 @@ void GLCanvas3D::_render_background() const ::glVertex2f(-1.0f, -1.0f); ::glVertex2f(1.0f, -1.0f); +#if ENABLE_GCODE_VIEWER + if (use_error_color) +#else if (m_dynamic_background_enabled && _is_any_volume_outside()) - ::glColor3fv(ERROR_BG_LIGHT_COLOR); +#endif // ENABLE_GCODE_VIEWER +::glColor3fv(ERROR_BG_LIGHT_COLOR); else ::glColor3fv(DEFAULT_BG_LIGHT_COLOR); @@ -5295,7 +5469,8 @@ void GLCanvas3D::_render_bed(bool bottom, bool show_axes) const bool show_texture = ! bottom || (m_gizmos.get_current_type() != GLGizmosManager::FdmSupports - && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports); + && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports + && m_gizmos.get_current_type() != GLGizmosManager::Seam); wxGetApp().plater()->get_bed().render(const_cast(*this), bottom, scale_factor, show_axes, show_texture); } @@ -5309,13 +5484,11 @@ void GLCanvas3D::_render_objects() const m_camera_clipping_plane = m_gizmos.get_clipping_plane(); - if (m_picking_enabled) - { + if (m_picking_enabled) { // Update the layer editing selection to the first object selected, update the current object maximum Z. const_cast(m_layers_editing).select_object(*m_model, this->is_layers_editing_enabled() ? m_selection.get_object_idx() : -1); - if (m_config != nullptr) - { + if (m_config != nullptr) { const BoundingBoxf3& bed_bb = wxGetApp().plater()->get_bed().get_bounding_box(false); m_volumes.set_print_box((float)bed_bb.min(0), (float)bed_bb.min(1), 0.0f, (float)bed_bb.max(0), (float)bed_bb.max(1), (float)m_config->opt_float("max_print_height")); m_volumes.check_outside_state(m_config, nullptr); @@ -5329,28 +5502,39 @@ void GLCanvas3D::_render_objects() const m_volumes.set_clipping_plane(m_camera_clipping_plane.get_data()); - m_shader.start_using(); - if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) { - int object_id = m_layers_editing.last_object_id; - m_volumes.render(GLVolumeCollection::Opaque, false, wxGetApp().plater()->get_camera().get_view_matrix(), [object_id](const GLVolume& volume) { - // Which volume to paint without the layer height profile shader? - return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id); - }); - // Let LayersEditing handle rendering of the active object using the layer height profile shader. - m_layers_editing.render_volumes(*this, this->m_volumes); - } else { + GLShaderProgram* shader = wxGetApp().get_shader("gouraud"); + if (shader != nullptr) { + shader->start_using(); + + if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) { + int object_id = m_layers_editing.last_object_id; + m_volumes.render(GLVolumeCollection::Opaque, false, wxGetApp().plater()->get_camera().get_view_matrix(), [object_id](const GLVolume& volume) { + // Which volume to paint without the layer height profile shader? + return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id); + }); + // Let LayersEditing handle rendering of the active object using the layer height profile shader. + m_layers_editing.render_volumes(*this, this->m_volumes); + } else { // do not cull backfaces to show broken geometry, if any m_volumes.render(GLVolumeCollection::Opaque, m_picking_enabled, wxGetApp().plater()->get_camera().get_view_matrix(), [this](const GLVolume& volume) { return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0); }); - } + } - m_volumes.render(GLVolumeCollection::Transparent, false, wxGetApp().plater()->get_camera().get_view_matrix()); - m_shader.stop_using(); + m_volumes.render(GLVolumeCollection::Transparent, false, wxGetApp().plater()->get_camera().get_view_matrix()); + shader->stop_using(); + } m_camera_clipping_plane = ClippingPlane::ClipsNothing(); } +#if ENABLE_GCODE_VIEWER +void GLCanvas3D::_render_gcode() const +{ + m_gcode_viewer.render(); +} +#endif // ENABLE_GCODE_VIEWER + void GLCanvas3D::_render_selection() const { float scale_factor = 1.0; @@ -5371,6 +5555,10 @@ void GLCanvas3D::_render_selection_center() const void GLCanvas3D::_check_and_update_toolbar_icon_scale() const { + // Don't update a toolbar scale, when we are on a Preview + if (wxGetApp().plater()->is_preview_shown()) + return; + float scale = wxGetApp().toolbar_icon_scale(); Size cnv_size = get_canvas_size(); @@ -5427,7 +5615,9 @@ void GLCanvas3D::_render_overlays() const _render_gizmos_overlay(); _render_warning_texture(); +#if !ENABLE_GCODE_VIEWER _render_legend_texture(); +#endif // !ENABLE_GCODE_VIEWER // main toolbar and undoredo toolbar need to be both updated before rendering because both their sizes are needed // to correctly place them @@ -5474,6 +5664,7 @@ void GLCanvas3D::_render_warning_texture() const m_warning_texture.render(*this); } +#if !ENABLE_GCODE_VIEWER void GLCanvas3D::_render_legend_texture() const { if (!m_legend_texture_enabled) @@ -5481,6 +5672,7 @@ void GLCanvas3D::_render_legend_texture() const m_legend_texture.render(*this); } +#endif // !ENABLE_GCODE_VIEWER void GLCanvas3D::_render_volumes_for_picking() const { @@ -5758,7 +5950,7 @@ void GLCanvas3D::_render_sla_slices() const void GLCanvas3D::_render_selection_sidebar_hints() const { - m_selection.render_sidebar_hints(m_sidebar_field, m_shader); + m_selection.render_sidebar_hints(m_sidebar_field); } void GLCanvas3D::_update_volumes_hover_state() const @@ -6428,6 +6620,7 @@ void GLCanvas3D::_load_wipe_tower_toolpaths(const std::vector& str_ BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info(); } +#if !ENABLE_GCODE_VIEWER static inline int hex_digit_to_int(const char c) { return @@ -6755,6 +6948,7 @@ void GLCanvas3D::_load_fff_shells() } } } +#endif // !ENABLE_GCODE_VIEWER // While it looks like we can call // this->reload_scene(true, true) @@ -6812,6 +7006,7 @@ void GLCanvas3D::_load_sla_shells() update_volumes_colors_by_extruder(); } +#if !ENABLE_GCODE_VIEWER void GLCanvas3D::_update_gcode_volumes_visibility(const GCodePreviewData& preview_data) { unsigned int size = (unsigned int)m_gcode_preview_volume_index.first_volumes.size(); @@ -6869,9 +7064,13 @@ void GLCanvas3D::_update_gcode_volumes_visibility(const GCodePreviewData& previe } } } +#endif // !ENABLE_GCODE_VIEWER void GLCanvas3D::_update_toolpath_volumes_outside_state() { +#if ENABLE_GCODE_VIEWER + BoundingBoxf3 test_volume = (m_config != nullptr) ? print_volume(*m_config) : BoundingBoxf3(); +#else // tolerance to avoid false detection at bed edges static const double tolerance_x = 0.05; static const double tolerance_y = 0.05; @@ -6888,15 +7087,23 @@ void GLCanvas3D::_update_toolpath_volumes_outside_state() print_volume.min(2) = -1e10; } } +#endif // ENABLE_GCODE_VIEWER for (GLVolume* volume : m_volumes.volumes) { +#if ENABLE_GCODE_VIEWER + volume->is_outside = ((test_volume.radius() > 0.0) && volume->is_extrusion_path) ? !test_volume.contains(volume->bounding_box()) : false; +#else volume->is_outside = ((print_volume.radius() > 0.0) && volume->is_extrusion_path) ? !print_volume.contains(volume->bounding_box()) : false; +#endif // ENABLE_GCODE_VIEWER } } void GLCanvas3D::_update_sla_shells_outside_state() { +#if ENABLE_GCODE_VIEWER + BoundingBoxf3 test_volume = (m_config != nullptr) ? print_volume(*m_config) : BoundingBoxf3(); +#else // tolerance to avoid false detection at bed edges static const double tolerance_x = 0.05; static const double tolerance_y = 0.05; @@ -6913,17 +7120,37 @@ void GLCanvas3D::_update_sla_shells_outside_state() print_volume.min(2) = -1e10; } } +#endif // ENABLE_GCODE_VIEWER for (GLVolume* volume : m_volumes.volumes) { +#if ENABLE_GCODE_VIEWER + volume->is_outside = ((test_volume.radius() > 0.0) && volume->shader_outside_printer_detection_enabled) ? !test_volume.contains(volume->transformed_convex_hull_bounding_box()) : false; +#else volume->is_outside = ((print_volume.radius() > 0.0) && volume->shader_outside_printer_detection_enabled) ? !print_volume.contains(volume->transformed_convex_hull_bounding_box()) : false; +#endif // ENABLE_GCODE_VIEWER } } void GLCanvas3D::_show_warning_texture_if_needed(WarningTexture::Warning warning) { _set_current(); +#if ENABLE_GCODE_VIEWER + bool show = false; + if (!m_volumes.empty()) + show = _is_any_volume_outside(); + else { + if (wxGetApp().is_editor()) { + BoundingBoxf3 test_volume = (m_config != nullptr) ? print_volume(*m_config) : BoundingBoxf3(); + const BoundingBoxf3& paths_volume = m_gcode_viewer.get_paths_bounding_box(); + if (test_volume.radius() > 0.0 && paths_volume.radius() > 0.0) + show = !test_volume.contains(paths_volume); + } + } + _set_warning_texture(warning, show); +#else _set_warning_texture(warning, _is_any_volume_outside()); +#endif // ENABLE_GCODE_VIEWER } std::vector GLCanvas3D::_parse_colors(const std::vector& colors) @@ -6951,10 +7178,12 @@ std::vector GLCanvas3D::_parse_colors(const std::vector& col return output; } +#if !ENABLE_GCODE_VIEWER void GLCanvas3D::_generate_legend_texture(const GCodePreviewData& preview_data, const std::vector& tool_colors) { m_legend_texture.generate(preview_data, tool_colors, *this, true); } +#endif // !ENABLE_GCODE_VIEWER void GLCanvas3D::_set_warning_texture(WarningTexture::Warning warning, bool state) { diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index c9433a10ea..03d42089b8 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -6,13 +6,16 @@ #include #include "GLToolbar.hpp" -#include "GLShader.hpp" #include "Event.hpp" #include "Selection.hpp" #include "Gizmos/GLGizmosManager.hpp" #include "GUI_ObjectLayers.hpp" #include "GLSelectionRectangle.hpp" #include "MeshUtils.hpp" +#if ENABLE_GCODE_VIEWER +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "GCodeViewer.hpp" +#endif // ENABLE_GCODE_VIEWER #include "libslic3r/Slicing.hpp" @@ -36,7 +39,9 @@ namespace Slic3r { struct Camera; class BackgroundSlicingProcess; +#if !ENABLE_GCODE_VIEWER class GCodePreviewData; +#endif // !ENABLE_GCODE_VIEWER struct ThumbnailData; class ModelObject; class ModelInstance; @@ -103,7 +108,11 @@ wxDECLARE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent); +#if ENABLE_GCODE_VIEWER +wxDECLARE_EVENT(EVT_GLCANVAS_MOVE_LAYERS_SLIDER, wxKeyEvent); +#else wxDECLARE_EVENT(EVT_GLCANVAS_MOVE_DOUBLE_SLIDER, wxKeyEvent); +#endif // ENABLE_GCODE_VIEWER wxDECLARE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent); wxDECLARE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent); @@ -118,6 +127,7 @@ class GLCanvas3D static const double DefaultCameraZoomToBoxMarginFactor; public: +#if !ENABLE_GCODE_VIEWER struct GCodePreviewVolumeIndex { enum EType @@ -144,6 +154,7 @@ public: void reset() { first_volumes.clear(); } }; +#endif // !ENABLE_GCODE_VIEWER private: class LayersEditing @@ -161,7 +172,6 @@ private: private: bool m_enabled; - Shader m_shader; unsigned int m_z_texture_id; // Not owned by LayersEditing. const DynamicPrintConfig *m_config; @@ -208,8 +218,9 @@ private: LayersEditing(); ~LayersEditing(); - bool init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename); - void set_config(const DynamicPrintConfig* config); + void init(); + + void set_config(const DynamicPrintConfig* config); void select_object(const Model &model, int object_id); bool is_allowed() const; @@ -339,6 +350,7 @@ private: bool generate(const std::string& msg, const GLCanvas3D& canvas, bool compress, bool red_colored = false); }; +#if !ENABLE_GCODE_VIEWER class LegendTexture : public GUI::GLTexture { static const int Px_Title_Offset = 5; @@ -365,6 +377,7 @@ private: void render(const GLCanvas3D& canvas) const; }; +#endif // !ENABLE_GCODE_VIEWER #if ENABLE_RENDER_STATISTICS struct RenderStats @@ -443,11 +456,12 @@ private: std::unique_ptr m_retina_helper; #endif bool m_in_render; +#if !ENABLE_GCODE_VIEWER LegendTexture m_legend_texture; +#endif // !ENABLE_GCODE_VIEWER WarningTexture m_warning_texture; wxTimer m_timer; LayersEditing m_layers_editing; - Shader m_shader; Mouse m_mouse; mutable GLGizmosManager m_gizmos; mutable GLToolbar m_main_toolbar; @@ -462,6 +476,10 @@ private: bool m_extra_frame_requested; mutable GLVolumeCollection m_volumes; +#if ENABLE_GCODE_VIEWER + GCodeViewer m_gcode_viewer; +#endif // ENABLE_GCODE_VIEWER + Selection m_selection; const DynamicPrintConfig* m_config; Model* m_model; @@ -472,7 +490,9 @@ private: bool m_initialized; bool m_apply_zoom_to_volumes_filter; mutable std::vector m_hover_volume_idxs; +#if !ENABLE_GCODE_VIEWER bool m_legend_texture_enabled; +#endif // !ENABLE_GCODE_VIEWER bool m_picking_enabled; bool m_moving_enabled; bool m_dynamic_background_enabled; @@ -490,7 +510,9 @@ private: bool m_reload_delayed; +#if !ENABLE_GCODE_VIEWER GCodePreviewVolumeIndex m_gcode_preview_volume_index; +#endif // !ENABLE_GCODE_VIEWER #if ENABLE_RENDER_PICKING_PASS bool m_show_picking_texture; @@ -532,6 +554,12 @@ public: void reset_volumes(); int check_volumes_outside_state() const; +#if ENABLE_GCODE_VIEWER + void reset_gcode_toolpaths() { m_gcode_viewer.reset(); } + const GCodeViewer::SequentialView& get_gcode_sequential_view() const { return m_gcode_viewer.get_sequential_view(); } + void update_gcode_sequential_view_current(unsigned int first, unsigned int last) { m_gcode_viewer.update_sequential_view_current(first, last); } +#endif // ENABLE_GCODE_VIEWER + void toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo = nullptr, int instance_idx = -1); void toggle_model_objects_visibility(bool visible, const ModelObject* mo = nullptr, int instance_idx = -1); void update_instance_printable_state_for_object(size_t obj_idx); @@ -564,7 +592,6 @@ public: void set_color_by(const std::string& value); void refresh_camera_scene_box(); - const Shader& get_shader() const { return m_shader; } BoundingBoxf3 volumes_bounding_box() const; BoundingBoxf3 scene_bounding_box() const; @@ -597,6 +624,9 @@ public: void zoom_to_bed(); void zoom_to_volumes(); void zoom_to_selection(); +#if ENABLE_GCODE_VIEWER + void zoom_to_gcode(); +#endif // ENABLE_GCODE_VIEWER void select_view(const std::string& direction); void update_volumes_colors_by_extruder(); @@ -613,7 +643,20 @@ public: void delete_selected(); void ensure_on_bed(unsigned int object_idx); +#if ENABLE_GCODE_VIEWER + bool is_gcode_legend_enabled() const { return m_gcode_viewer.is_legend_enabled(); } + GCodeViewer::EViewType get_gcode_view_type() const { return m_gcode_viewer.get_view_type(); } + const std::vector& get_gcode_layers_zs() const; + std::vector get_volumes_print_zs(bool active_only) const; + unsigned int get_gcode_options_visibility_flags() const { return m_gcode_viewer.get_options_visibility_flags(); } + void set_gcode_options_visibility_from_flags(unsigned int flags); + unsigned int get_toolpath_role_visibility_flags() const { return m_gcode_viewer.get_toolpath_role_visibility_flags(); } + void set_toolpath_role_visibility_flags(unsigned int flags); + void set_toolpath_view_type(GCodeViewer::EViewType type); + void set_toolpaths_z_range(const std::array& range); +#else std::vector get_current_print_zs(bool active_only) const; +#endif // ENABLE_GCODE_VIEWER void set_toolpaths_range(double low, double high); std::vector load_object(const ModelObject& model_object, int obj_idx, std::vector instance_idxs); @@ -623,7 +666,14 @@ public: void reload_scene(bool refresh_immediately, bool force_full_scene_refresh = false); +#if ENABLE_GCODE_VIEWER + void load_gcode_preview(const GCodeProcessor::Result& gcode_result); + void refresh_gcode_preview(const GCodeProcessor::Result& gcode_result, const std::vector& str_tool_colors); + void set_gcode_view_preview_type(GCodeViewer::EViewType type) { return m_gcode_viewer.set_view_type(type); } + GCodeViewer::EViewType get_gcode_view_preview_type() const { return m_gcode_viewer.get_view_type(); } +#else void load_gcode_preview(const GCodePreviewData& preview_data, const std::vector& str_tool_colors); +#endif // ENABLE_GCODE_VIEWER void load_sla_preview(); void load_preview(const std::vector& str_tool_colors, const std::vector& color_print_values); void bind_event_handlers(); @@ -642,7 +692,9 @@ public: Size get_canvas_size() const; Vec2d get_local_mouse_position() const; +#if !ENABLE_GCODE_VIEWER void reset_legend_texture(); +#endif // !ENABLE_GCODE_VIEWER void set_tooltip(const std::string& tooltip) const; @@ -744,6 +796,9 @@ private: void _render_background() const; void _render_bed(bool bottom, bool show_axes) const; void _render_objects() const; +#if ENABLE_GCODE_VIEWER + void _render_gcode() const; +#endif // ENABLE_GCODE_VIEWER void _render_selection() const; #if ENABLE_RENDER_SELECTION_CENTER void _render_selection_center() const; @@ -751,7 +806,9 @@ private: void _check_and_update_toolbar_icon_scale() const; void _render_overlays() const; void _render_warning_texture() const; +#if !ENABLE_GCODE_VIEWER void _render_legend_texture() const; +#endif // !ENABLE_GCODE_VIEWER void _render_volumes_for_picking() const; void _render_current_gizmo() const; void _render_gizmos_overlay() const; @@ -799,22 +856,28 @@ private: // Create 3D thick extrusion lines for wipe tower extrusions void _load_wipe_tower_toolpaths(const std::vector& str_tool_colors); +#if !ENABLE_GCODE_VIEWER // generates gcode extrusion paths geometry void _load_gcode_extrusion_paths(const GCodePreviewData& preview_data, const std::vector& tool_colors); // generates gcode travel paths geometry void _load_gcode_travel_paths(const GCodePreviewData& preview_data, const std::vector& tool_colors); // generates objects and wipe tower geometry void _load_fff_shells(); +#endif // !ENABLE_GCODE_VIEWER // Load SLA objects and support structures for objects, for which the slaposSliceSupports step has been finished. void _load_sla_shells(); +#if !ENABLE_GCODE_VIEWER // sets gcode geometry visibility according to user selection void _update_gcode_volumes_visibility(const GCodePreviewData& preview_data); +#endif // !ENABLE_GCODE_VIEWER void _update_toolpath_volumes_outside_state(); void _update_sla_shells_outside_state(); void _show_warning_texture_if_needed(WarningTexture::Warning warning); +#if !ENABLE_GCODE_VIEWER // generates the legend texture in dependence of the current shown view type void _generate_legend_texture(const GCodePreviewData& preview_data, const std::vector& tool_colors); +#endif // !ENABLE_GCODE_VIEWER // generates a warning texture containing the given message void _set_warning_texture(WarningTexture::Warning warning, bool state); diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp new file mode 100644 index 0000000000..e738aa3c49 --- /dev/null +++ b/src/slic3r/GUI/GLModel.cpp @@ -0,0 +1,531 @@ +#include "libslic3r/libslic3r.h" +#include "GLModel.hpp" + +#include "3DScene.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Model.hpp" + +#include +#include + +#include + +namespace Slic3r { +namespace GUI { + +void GLModel::init_from(const GLModelInitializationData& data) +{ + assert(!data.positions.empty() && !data.triangles.empty()); + assert(data.positions.size() == data.normals.size()); + + if (m_vbo_id > 0) // call reset() if you want to reuse this model + return; + + // vertices/normals data + std::vector vertices(6 * data.positions.size()); + for (size_t i = 0; i < data.positions.size(); ++i) { + size_t offset = i * 6; + ::memcpy(static_cast(&vertices[offset]), static_cast(data.positions[i].data()), 3 * sizeof(float)); + ::memcpy(static_cast(&vertices[3 + offset]), static_cast(data.normals[i].data()), 3 * sizeof(float)); + } + + // indices data + std::vector indices(3 * data.triangles.size()); + for (size_t i = 0; i < data.triangles.size(); ++i) { + for (size_t j = 0; j < 3; ++j) { + indices[i * 3 + j] = static_cast(data.triangles[i][j]); + } + } + + m_indices_count = static_cast(indices.size()); + m_bounding_box = BoundingBoxf3(); + for (size_t i = 0; i < data.positions.size(); ++i) { + m_bounding_box.merge(data.positions[i].cast()); + } + + send_to_gpu(vertices, indices); +} + +void GLModel::init_from(const TriangleMesh& mesh) +{ + if (m_vbo_id > 0) // call reset() if you want to reuse this model + return; + + std::vector vertices = std::vector(18 * mesh.stl.stats.number_of_facets); + std::vector indices = std::vector(3 * mesh.stl.stats.number_of_facets); + + unsigned int vertices_count = 0; + for (uint32_t i = 0; i < mesh.stl.stats.number_of_facets; ++i) { + const stl_facet& facet = mesh.stl.facet_start[i]; + for (uint32_t j = 0; j < 3; ++j) { + uint32_t offset = i * 18 + j * 6; + ::memcpy(static_cast(&vertices[offset]), static_cast(facet.vertex[j].data()), 3 * sizeof(float)); + ::memcpy(static_cast(&vertices[3 + offset]), static_cast(facet.normal.data()), 3 * sizeof(float)); + } + for (uint32_t j = 0; j < 3; ++j) { + indices[i * 3 + j] = vertices_count + j; + } + vertices_count += 3; + } + + m_indices_count = static_cast(indices.size()); + m_bounding_box = mesh.bounding_box(); + + send_to_gpu(vertices, indices); +} + +bool GLModel::init_from_file(const std::string& filename) +{ + if (!boost::filesystem::exists(filename)) + return false; + + if (!boost::algorithm::iends_with(filename, ".stl")) + return false; + + Model model; + try + { + model = Model::read_from_file(filename); + } + catch (std::exception&) + { + return false; + } + + init_from(model.mesh()); + + m_filename = filename; + + return true; +} + +void GLModel::reset() +{ + // release gpu memory + if (m_ibo_id > 0) { + glsafe(::glDeleteBuffers(1, &m_ibo_id)); + m_ibo_id = 0; + } + + if (m_vbo_id > 0) { + glsafe(::glDeleteBuffers(1, &m_vbo_id)); + m_vbo_id = 0; + } + + m_indices_count = 0; + m_bounding_box = BoundingBoxf3(); + m_filename = std::string(); +} + +void GLModel::render() const +{ + if (m_vbo_id == 0 || m_ibo_id == 0) + return; + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id)); + glsafe(::glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), (const void*)0)); + glsafe(::glNormalPointer(GL_FLOAT, 6 * sizeof(float), (const void*)(3 * sizeof(float)))); + + glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); + glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); + + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id)); + glsafe(::glDrawElements(GL_TRIANGLES, static_cast(m_indices_count), GL_UNSIGNED_INT, (const void*)0)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + + glsafe(::glDisableClientState(GL_NORMAL_ARRAY)); + glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); +} + +void GLModel::send_to_gpu(const std::vector& vertices, const std::vector& indices) +{ + // vertex data -> send to gpu + glsafe(::glGenBuffers(1, &m_vbo_id)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id)); + glsafe(::glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + + // indices data -> send to gpu + glsafe(::glGenBuffers(1, &m_ibo_id)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id)); + glsafe(::glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); +} + +GLModelInitializationData stilized_arrow(int resolution, float tip_radius, float tip_height, float stem_radius, float stem_height) +{ + auto append_vertex = [](GLModelInitializationData& data, const Vec3f& position, const Vec3f& normal) { + data.positions.emplace_back(position); + data.normals.emplace_back(normal); + }; + + resolution = std::max(4, resolution); + + GLModelInitializationData data; + + const float angle_step = 2.0f * M_PI / static_cast(resolution); + std::vector cosines(resolution); + std::vector sines(resolution); + + for (int i = 0; i < resolution; ++i) + { + float angle = angle_step * static_cast(i); + cosines[i] = ::cos(angle); + sines[i] = -::sin(angle); + } + + const float total_height = tip_height + stem_height; + + // tip vertices/normals + append_vertex(data, { 0.0f, 0.0f, total_height }, Vec3f::UnitZ()); + for (int i = 0; i < resolution; ++i) + { + append_vertex(data, { tip_radius * sines[i], tip_radius * cosines[i], stem_height }, { sines[i], cosines[i], 0.0f }); + } + + // tip triangles + for (int i = 0; i < resolution; ++i) + { + int v3 = (i < resolution - 1) ? i + 2 : 1; + data.triangles.emplace_back(0, i + 1, v3); + } + + // tip cap outer perimeter vertices + for (int i = 0; i < resolution; ++i) + { + append_vertex(data, { tip_radius * sines[i], tip_radius * cosines[i], stem_height }, -Vec3f::UnitZ()); + } + + // tip cap inner perimeter vertices + for (int i = 0; i < resolution; ++i) + { + append_vertex(data, { stem_radius * sines[i], stem_radius * cosines[i], stem_height }, -Vec3f::UnitZ()); + } + + // tip cap triangles + for (int i = 0; i < resolution; ++i) + { + int v2 = (i < resolution - 1) ? i + resolution + 2 : resolution + 1; + int v3 = (i < resolution - 1) ? i + 2 * resolution + 2 : 2 * resolution + 1; + data.triangles.emplace_back(i + resolution + 1, v3, v2); + data.triangles.emplace_back(i + resolution + 1, i + 2 * resolution + 1, v3); + } + + // stem bottom vertices + for (int i = 0; i < resolution; ++i) + { + append_vertex(data, { stem_radius * sines[i], stem_radius * cosines[i], stem_height }, { sines[i], cosines[i], 0.0f }); + } + + // stem top vertices + for (int i = 0; i < resolution; ++i) + { + append_vertex(data, { stem_radius * sines[i], stem_radius * cosines[i], 0.0f }, { sines[i], cosines[i], 0.0f }); + } + + // stem triangles + for (int i = 0; i < resolution; ++i) + { + int v2 = (i < resolution - 1) ? i + 3 * resolution + 2 : 3 * resolution + 1; + int v3 = (i < resolution - 1) ? i + 4 * resolution + 2 : 4 * resolution + 1; + data.triangles.emplace_back(i + 3 * resolution + 1, v3, v2); + data.triangles.emplace_back(i + 3 * resolution + 1, i + 4 * resolution + 1, v3); + } + + // stem cap vertices + append_vertex(data, Vec3f::Zero(), -Vec3f::UnitZ()); + for (int i = 0; i < resolution; ++i) + { + append_vertex(data, { stem_radius * sines[i], stem_radius * cosines[i], 0.0f }, -Vec3f::UnitZ()); + } + + // stem cap triangles + for (int i = 0; i < resolution; ++i) + { + int v3 = (i < resolution - 1) ? i + 5 * resolution + 3 : 5 * resolution + 2; + data.triangles.emplace_back(5 * resolution + 1, v3, i + 5 * resolution + 2); + } + + return data; +} + +GLModelInitializationData circular_arrow(int resolution, float radius, float tip_height, float tip_width, float stem_width, float thickness) +{ + auto append_vertex = [](GLModelInitializationData& data, const Vec3f& position, const Vec3f& normal) { + data.positions.emplace_back(position); + data.normals.emplace_back(normal); + }; + + resolution = std::max(2, resolution); + + GLModelInitializationData data; + + const float half_thickness = 0.5f * thickness; + const float half_stem_width = 0.5f * stem_width; + const float half_tip_width = 0.5f * tip_width; + + const float outer_radius = radius + half_stem_width; + const float inner_radius = radius - half_stem_width; + const float step_angle = 0.5f * PI / static_cast(resolution); + + // tip + // top face vertices + append_vertex(data, { 0.0f, outer_radius, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { 0.0f, radius + half_tip_width, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { -tip_height, radius, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { 0.0f, radius - half_tip_width, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { 0.0f, inner_radius, half_thickness }, Vec3f::UnitZ()); + + // top face triangles + data.triangles.emplace_back(0, 1, 2); + data.triangles.emplace_back(0, 2, 4); + data.triangles.emplace_back(4, 2, 3); + + // bottom face vertices + append_vertex(data, { 0.0f, outer_radius, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { 0.0f, radius + half_tip_width, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { -tip_height, radius, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { 0.0f, radius - half_tip_width, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { 0.0f, inner_radius, -half_thickness }, -Vec3f::UnitZ()); + + // bottom face triangles + data.triangles.emplace_back(5, 7, 6); + data.triangles.emplace_back(5, 9, 7); + data.triangles.emplace_back(9, 8, 7); + + // side faces vertices + append_vertex(data, { 0.0f, outer_radius, -half_thickness }, Vec3f::UnitX()); + append_vertex(data, { 0.0f, radius + half_tip_width, -half_thickness }, Vec3f::UnitX()); + append_vertex(data, { 0.0f, outer_radius, half_thickness }, Vec3f::UnitX()); + append_vertex(data, { 0.0f, radius + half_tip_width, half_thickness }, Vec3f::UnitX()); + + Vec3f normal(-half_tip_width, tip_height, 0.0f); + normal.normalize(); + append_vertex(data, { 0.0f, radius + half_tip_width, -half_thickness }, normal); + append_vertex(data, { -tip_height, radius, -half_thickness }, normal); + append_vertex(data, { 0.0f, radius + half_tip_width, half_thickness }, normal); + append_vertex(data, { -tip_height, radius, half_thickness }, normal); + + normal = Vec3f(-half_tip_width, -tip_height, 0.0f); + normal.normalize(); + append_vertex(data, { -tip_height, radius, -half_thickness }, normal); + append_vertex(data, { 0.0f, radius - half_tip_width, -half_thickness }, normal); + append_vertex(data, { -tip_height, radius, half_thickness }, normal); + append_vertex(data, { 0.0f, radius - half_tip_width, half_thickness }, normal); + + append_vertex(data, { 0.0f, radius - half_tip_width, -half_thickness }, Vec3f::UnitX()); + append_vertex(data, { 0.0f, inner_radius, -half_thickness }, Vec3f::UnitX()); + append_vertex(data, { 0.0f, radius - half_tip_width, half_thickness }, Vec3f::UnitX()); + append_vertex(data, { 0.0f, inner_radius, half_thickness }, Vec3f::UnitX()); + + // side face triangles + for (int i = 0; i < 4; ++i) + { + int ii = i * 4; + data.triangles.emplace_back(10 + ii, 11 + ii, 13 + ii); + data.triangles.emplace_back(10 + ii, 13 + ii, 12 + ii); + } + + // stem + // top face vertices + for (int i = 0; i <= resolution; ++i) + { + float angle = static_cast(i) * step_angle; + append_vertex(data, { inner_radius * ::sin(angle), inner_radius * ::cos(angle), half_thickness }, Vec3f::UnitZ()); + } + + for (int i = 0; i <= resolution; ++i) + { + float angle = static_cast(i) * step_angle; + append_vertex(data, { outer_radius * ::sin(angle), outer_radius * ::cos(angle), half_thickness }, Vec3f::UnitZ()); + } + + // top face triangles + for (int i = 0; i < resolution; ++i) + { + data.triangles.emplace_back(26 + i, 27 + i, 27 + resolution + i); + data.triangles.emplace_back(27 + i, 28 + resolution + i, 27 + resolution + i); + } + + // bottom face vertices + for (int i = 0; i <= resolution; ++i) + { + float angle = static_cast(i) * step_angle; + append_vertex(data, { inner_radius * ::sin(angle), inner_radius * ::cos(angle), -half_thickness }, -Vec3f::UnitZ()); + } + + for (int i = 0; i <= resolution; ++i) + { + float angle = static_cast(i) * step_angle; + append_vertex(data, { outer_radius * ::sin(angle), outer_radius * ::cos(angle), -half_thickness }, -Vec3f::UnitZ()); + } + + // bottom face triangles + for (int i = 0; i < resolution; ++i) + { + data.triangles.emplace_back(28 + 2 * resolution + i, 29 + 3 * resolution + i, 29 + 2 * resolution + i); + data.triangles.emplace_back(29 + 2 * resolution + i, 29 + 3 * resolution + i, 30 + 3 * resolution + i); + } + + // side faces vertices and triangles + for (int i = 0; i <= resolution; ++i) + { + float angle = static_cast(i) * step_angle; + float c = ::cos(angle); + float s = ::sin(angle); + append_vertex(data, { inner_radius * s, inner_radius * c, -half_thickness }, { -s, -c, 0.0f }); + } + + for (int i = 0; i <= resolution; ++i) + { + float angle = static_cast(i) * step_angle; + float c = ::cos(angle); + float s = ::sin(angle); + append_vertex(data, { inner_radius * s, inner_radius * c, half_thickness }, { -s, -c, 0.0f }); + } + + int first_id = 26 + 4 * (resolution + 1); + for (int i = 0; i < resolution; ++i) + { + int ii = first_id + i; + data.triangles.emplace_back(ii, ii + 1, ii + resolution + 2); + data.triangles.emplace_back(ii, ii + resolution + 2, ii + resolution + 1); + } + + append_vertex(data, { inner_radius, 0.0f, -half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { outer_radius, 0.0f, -half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { inner_radius, 0.0f, half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { outer_radius, 0.0f, half_thickness }, -Vec3f::UnitY()); + + first_id = 26 + 6 * (resolution + 1); + data.triangles.emplace_back(first_id, first_id + 1, first_id + 3); + data.triangles.emplace_back(first_id, first_id + 3, first_id + 2); + + for (int i = resolution; i >= 0; --i) + { + float angle = static_cast(i) * step_angle; + float c = ::cos(angle); + float s = ::sin(angle); + append_vertex(data, { outer_radius * s, outer_radius * c, -half_thickness }, { s, c, 0.0f }); + } + + for (int i = resolution; i >= 0; --i) + { + float angle = static_cast(i) * step_angle; + float c = ::cos(angle); + float s = ::sin(angle); + append_vertex(data, { outer_radius * s, outer_radius * c, +half_thickness }, { s, c, 0.0f }); + } + + first_id = 30 + 6 * (resolution + 1); + for (int i = 0; i < resolution; ++i) + { + int ii = first_id + i; + data.triangles.emplace_back(ii, ii + 1, ii + resolution + 2); + data.triangles.emplace_back(ii, ii + resolution + 2, ii + resolution + 1); + } + + return data; +} + +GLModelInitializationData straight_arrow(float tip_width, float tip_height, float stem_width, float stem_height, float thickness) +{ + auto append_vertex = [](GLModelInitializationData& data, const Vec3f& position, const Vec3f& normal) { + data.positions.emplace_back(position); + data.normals.emplace_back(normal); + }; + + GLModelInitializationData data; + + const float half_thickness = 0.5f * thickness; + const float half_stem_width = 0.5f * stem_width; + const float half_tip_width = 0.5f * tip_width; + const float total_height = tip_height + stem_height; + + // top face vertices + append_vertex(data, { half_stem_width, 0.0, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { half_stem_width, stem_height, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { half_tip_width, stem_height, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { 0.0, total_height, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { -half_tip_width, stem_height, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { -half_stem_width, stem_height, half_thickness }, Vec3f::UnitZ()); + append_vertex(data, { -half_stem_width, 0.0, half_thickness }, Vec3f::UnitZ()); + + // top face triangles + data.triangles.emplace_back(0, 1, 6); + data.triangles.emplace_back(6, 1, 5); + data.triangles.emplace_back(4, 5, 3); + data.triangles.emplace_back(5, 1, 3); + data.triangles.emplace_back(1, 2, 3); + + // bottom face vertices + append_vertex(data, { half_stem_width, 0.0, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { half_stem_width, stem_height, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { half_tip_width, stem_height, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { 0.0, total_height, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { -half_tip_width, stem_height, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { -half_stem_width, stem_height, -half_thickness }, -Vec3f::UnitZ()); + append_vertex(data, { -half_stem_width, 0.0, -half_thickness }, -Vec3f::UnitZ()); + + // bottom face triangles + data.triangles.emplace_back(7, 13, 8); + data.triangles.emplace_back(13, 12, 8); + data.triangles.emplace_back(12, 11, 10); + data.triangles.emplace_back(8, 12, 10); + data.triangles.emplace_back(9, 8, 10); + + // side faces vertices + append_vertex(data, { half_stem_width, 0.0, -half_thickness }, Vec3f::UnitX()); + append_vertex(data, { half_stem_width, stem_height, -half_thickness }, Vec3f::UnitX()); + append_vertex(data, { half_stem_width, 0.0, half_thickness }, Vec3f::UnitX()); + append_vertex(data, { half_stem_width, stem_height, half_thickness }, Vec3f::UnitX()); + + append_vertex(data, { half_stem_width, stem_height, -half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { half_tip_width, stem_height, -half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { half_stem_width, stem_height, half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { half_tip_width, stem_height, half_thickness }, -Vec3f::UnitY()); + + Vec3f normal(tip_height, half_tip_width, 0.0f); + normal.normalize(); + append_vertex(data, { half_tip_width, stem_height, -half_thickness }, normal); + append_vertex(data, { 0.0, total_height, -half_thickness }, normal); + append_vertex(data, { half_tip_width, stem_height, half_thickness }, normal); + append_vertex(data, { 0.0, total_height, half_thickness }, normal); + + normal = Vec3f(-tip_height, half_tip_width, 0.0f); + normal.normalize(); + append_vertex(data, { 0.0, total_height, -half_thickness }, normal); + append_vertex(data, { -half_tip_width, stem_height, -half_thickness }, normal); + append_vertex(data, { 0.0, total_height, half_thickness }, normal); + append_vertex(data, { -half_tip_width, stem_height, half_thickness }, normal); + + append_vertex(data, { -half_tip_width, stem_height, -half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { -half_stem_width, stem_height, -half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { -half_tip_width, stem_height, half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { -half_stem_width, stem_height, half_thickness }, -Vec3f::UnitY()); + + append_vertex(data, { -half_stem_width, stem_height, -half_thickness }, -Vec3f::UnitX()); + append_vertex(data, { -half_stem_width, 0.0, -half_thickness }, -Vec3f::UnitX()); + append_vertex(data, { -half_stem_width, stem_height, half_thickness }, -Vec3f::UnitX()); + append_vertex(data, { -half_stem_width, 0.0, half_thickness }, -Vec3f::UnitX()); + + append_vertex(data, { -half_stem_width, 0.0, -half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { half_stem_width, 0.0, -half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { -half_stem_width, 0.0, half_thickness }, -Vec3f::UnitY()); + append_vertex(data, { half_stem_width, 0.0, half_thickness }, -Vec3f::UnitY()); + + // side face triangles + for (int i = 0; i < 7; ++i) + { + int ii = i * 4; + data.triangles.emplace_back(14 + ii, 15 + ii, 17 + ii); + data.triangles.emplace_back(14 + ii, 17 + ii, 16 + ii); + } + + return data; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp new file mode 100644 index 0000000000..0b4a69bdb0 --- /dev/null +++ b/src/slic3r/GUI/GLModel.hpp @@ -0,0 +1,68 @@ +#ifndef slic3r_GLModel_hpp_ +#define slic3r_GLModel_hpp_ + +#include "libslic3r/Point.hpp" +#include "libslic3r/BoundingBox.hpp" +#include +#include + +namespace Slic3r { + +class TriangleMesh; + +namespace GUI { + + struct GLModelInitializationData + { + std::vector positions; + std::vector normals; + std::vector triangles; + }; + + class GLModel + { + unsigned int m_vbo_id{ 0 }; + unsigned int m_ibo_id{ 0 }; + size_t m_indices_count{ 0 }; + + BoundingBoxf3 m_bounding_box; + std::string m_filename; + + public: + virtual ~GLModel() { reset(); } + + void init_from(const GLModelInitializationData& data); + void init_from(const TriangleMesh& mesh); + bool init_from_file(const std::string& filename); + void reset(); + void render() const; + + const BoundingBoxf3& get_bounding_box() const { return m_bounding_box; } + + const std::string& get_filename() const { return m_filename; } + + private: + void send_to_gpu(const std::vector& vertices, const std::vector& indices); + }; + + + // create an arrow with cylindrical stem and conical tip, with the given dimensions and resolution + // the origin of the arrow is in the center of the stem cap + // the arrow has its axis of symmetry along the Z axis and is pointing upward + GLModelInitializationData stilized_arrow(int resolution, float tip_radius, float tip_height, float stem_radius, float stem_height); + + // create an arrow whose stem is a quarter of circle, with the given dimensions and resolution + // the origin of the arrow is in the center of the circle + // the arrow is contained in the 1st quadrant of the XY plane and is pointing counterclockwise + GLModelInitializationData circular_arrow(int resolution, float radius, float tip_height, float tip_width, float stem_width, float thickness); + + // create an arrow with the given dimensions + // the origin of the arrow is in the center of the stem cap + // the arrow is contained in XY plane and has its main axis along the Y axis + GLModelInitializationData straight_arrow(float tip_width, float tip_height, float stem_width, float stem_height, float thickness); + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLModel_hpp_ + diff --git a/src/slic3r/GUI/GLShader.cpp b/src/slic3r/GUI/GLShader.cpp index c310760603..3c2612b45e 100644 --- a/src/slic3r/GUI/GLShader.cpp +++ b/src/slic3r/GUI/GLShader.cpp @@ -1,366 +1,348 @@ -#include - +#include "libslic3r/libslic3r.h" #include "GLShader.hpp" -#include "libslic3r/Utils.hpp" #include "3DScene.hpp" -#include +#include "libslic3r/Utils.hpp" -#include -#include -#include +#include +#include +#include + +#include namespace Slic3r { -GLShader::~GLShader() +GLShaderProgram::~GLShaderProgram() { - assert(fragment_program_id == 0); - assert(vertex_program_id == 0); - assert(shader_program_id == 0); + if (m_id > 0) + glsafe(::glDeleteProgram(m_id)); } -// A safe wrapper around glGetString to report a "N/A" string in case glGetString returns nullptr. -inline std::string gl_get_string_safe(GLenum param) +bool GLShaderProgram::init_from_files(const std::string& name, const ShaderFilenames& filenames) { - const char *value = (const char*)glGetString(param); - return std::string(value ? value : "N/A"); + auto load_from_file = [](const std::string& filename) { + std::string path = resources_dir() + "/shaders/" + filename; + boost::nowide::ifstream s(path, boost::nowide::ifstream::binary); + if (!s.good()) { + BOOST_LOG_TRIVIAL(error) << "Couldn't open file: '" << path << "'"; + return std::string(); + } + + s.seekg(0, s.end); + int file_length = static_cast(s.tellg()); + s.seekg(0, s.beg); + std::string source(file_length, '\0'); + s.read(source.data(), file_length); + if (!s.good()) { + BOOST_LOG_TRIVIAL(error) << "Error while loading file: '" << path << "'"; + return std::string(); + } + + s.close(); + return source; + }; + + ShaderSources sources = {}; + for (size_t i = 0; i < static_cast(EShaderType::Count); ++i) { + sources[i] = filenames[i].empty() ? std::string() : load_from_file(filenames[i]); + } + + bool valid = !sources[static_cast(EShaderType::Vertex)].empty() && !sources[static_cast(EShaderType::Fragment)].empty() && sources[static_cast(EShaderType::Compute)].empty(); + valid |= !sources[static_cast(EShaderType::Compute)].empty() && sources[static_cast(EShaderType::Vertex)].empty() && sources[static_cast(EShaderType::Fragment)].empty() && + sources[static_cast(EShaderType::Geometry)].empty() && sources[static_cast(EShaderType::TessEvaluation)].empty() && sources[static_cast(EShaderType::TessControl)].empty(); + + return valid ? init_from_texts(name, sources) : false; } -bool GLShader::load_from_text(const char *fragment_shader, const char *vertex_shader) +bool GLShaderProgram::init_from_texts(const std::string& name, const ShaderSources& sources) { - std::string gl_version = gl_get_string_safe(GL_VERSION); - int major = atoi(gl_version.c_str()); - //int minor = atoi(gl_version.c_str() + gl_version.find('.') + 1); - if (major < 2) { - // Cannot create a shader object on OpenGL 1.x. - // Form an error message. - std::string gl_vendor = gl_get_string_safe(GL_VENDOR); - std::string gl_renderer = gl_get_string_safe(GL_RENDERER); - std::string glsl_version = gl_get_string_safe(GL_SHADING_LANGUAGE_VERSION); - last_error = "Your computer does not support OpenGL shaders.\n"; -#ifdef _WIN32 - if (gl_vendor == "Microsoft Corporation" && gl_renderer == "GDI Generic") { - last_error = "Windows is using a software OpenGL renderer.\n" - "You are either connected over remote desktop,\n" - "or a hardware acceleration is not available.\n"; + auto shader_type_as_string = [](EShaderType type) { + switch (type) + { + case EShaderType::Vertex: { return "vertex"; } + case EShaderType::Fragment: { return "fragment"; } + case EShaderType::Geometry: { return "geometry"; } + case EShaderType::TessEvaluation: { return "tesselation evaluation"; } + case EShaderType::TessControl: { return "tesselation control"; } + case EShaderType::Compute: { return "compute"; } + default: { return "unknown"; } } -#endif - last_error += "GL version: " + gl_version + "\n"; - last_error += "vendor: " + gl_vendor + "\n"; - last_error += "renderer: " + gl_renderer + "\n"; - last_error += "GLSL version: " + glsl_version + "\n"; - return false; - } + }; - if (fragment_shader != nullptr) { - this->fragment_program_id = ::glCreateShader(GL_FRAGMENT_SHADER); - glcheck(); - if (this->fragment_program_id == 0) { - last_error = "glCreateShader(GL_FRAGMENT_SHADER) failed."; - return false; + auto create_shader = [](EShaderType type) { + GLuint id = 0; + switch (type) + { + case EShaderType::Vertex: { id = ::glCreateShader(GL_VERTEX_SHADER); glcheck(); break; } + case EShaderType::Fragment: { id = ::glCreateShader(GL_FRAGMENT_SHADER); glcheck(); break; } + case EShaderType::Geometry: { id = ::glCreateShader(GL_GEOMETRY_SHADER); glcheck(); break; } + case EShaderType::TessEvaluation: { id = ::glCreateShader(GL_TESS_EVALUATION_SHADER); glcheck(); break; } + case EShaderType::TessControl: { id = ::glCreateShader(GL_TESS_CONTROL_SHADER); glcheck(); break; } + case EShaderType::Compute: { id = ::glCreateShader(GL_COMPUTE_SHADER); glcheck(); break; } + default: { break; } } - GLint len = (GLint)strlen(fragment_shader); - glsafe(::glShaderSource(this->fragment_program_id, 1, &fragment_shader, &len)); - glsafe(::glCompileShader(this->fragment_program_id)); - GLint params; - glsafe(::glGetShaderiv(this->fragment_program_id, GL_COMPILE_STATUS, ¶ms)); - if (params == GL_FALSE) { - // Compilation failed. Get the log. - glsafe(::glGetShaderiv(this->fragment_program_id, GL_INFO_LOG_LENGTH, ¶ms)); - std::vector msg(params); - glsafe(::glGetShaderInfoLog(this->fragment_program_id, params, ¶ms, msg.data())); - this->last_error = std::string("Fragment shader compilation failed:\n") + msg.data(); - this->release(); - return false; + + return (id == 0) ? std::make_pair(false, GLuint(0)) : std::make_pair(true, id); + }; + + auto release_shaders = [](const std::array(EShaderType::Count)>& shader_ids) { + for (size_t i = 0; i < static_cast(EShaderType::Count); ++i) { + if (shader_ids[i] > 0) + glsafe(::glDeleteShader(shader_ids[i])); + } + }; + + assert(m_id == 0); + + m_name = name; + + std::array(EShaderType::Count)> shader_ids = { 0 }; + + for (size_t i = 0; i < static_cast(EShaderType::Count); ++i) { + const std::string& source = sources[i]; + if (!source.empty()) + { + EShaderType type = static_cast(i); + auto [result, id] = create_shader(type); + if (result) + shader_ids[i] = id; + else { + BOOST_LOG_TRIVIAL(error) << "glCreateShader() failed for " << shader_type_as_string(type) << " shader of shader program '" << name << "'"; + + // release shaders + release_shaders(shader_ids); + return false; + } + + const char* source_ptr = source.c_str(); + glsafe(::glShaderSource(id, 1, &source_ptr, nullptr)); + glsafe(::glCompileShader(id)); + GLint params; + glsafe(::glGetShaderiv(id, GL_COMPILE_STATUS, ¶ms)); + if (params == GL_FALSE) { + // Compilation failed. + glsafe(::glGetShaderiv(id, GL_INFO_LOG_LENGTH, ¶ms)); + std::vector msg(params); + glsafe(::glGetShaderInfoLog(id, params, ¶ms, msg.data())); + BOOST_LOG_TRIVIAL(error) << "Unable to compile " << shader_type_as_string(type) << " shader of shader program '" << name << "':\n" << msg.data(); + + // release shaders + release_shaders(shader_ids); + return false; + } } } - if (vertex_shader != nullptr) { - this->vertex_program_id = ::glCreateShader(GL_VERTEX_SHADER); - glcheck(); - if (this->vertex_program_id == 0) { - last_error = "glCreateShader(GL_VERTEX_SHADER) failed."; - this->release(); - return false; - } - GLint len = (GLint)strlen(vertex_shader); - glsafe(::glShaderSource(this->vertex_program_id, 1, &vertex_shader, &len)); - glsafe(::glCompileShader(this->vertex_program_id)); - GLint params; - glsafe(::glGetShaderiv(this->vertex_program_id, GL_COMPILE_STATUS, ¶ms)); - if (params == GL_FALSE) { - // Compilation failed. Get the log. - glsafe(::glGetShaderiv(this->vertex_program_id, GL_INFO_LOG_LENGTH, ¶ms)); - std::vector msg(params); - glsafe(::glGetShaderInfoLog(this->vertex_program_id, params, ¶ms, msg.data())); - this->last_error = std::string("Vertex shader compilation failed:\n") + msg.data(); - this->release(); - return false; - } - } - - // Link shaders - this->shader_program_id = ::glCreateProgram(); + m_id = ::glCreateProgram(); glcheck(); - if (this->shader_program_id == 0) { - last_error = "glCreateProgram() failed."; - this->release(); + if (m_id == 0) { + BOOST_LOG_TRIVIAL(error) << "glCreateProgram() failed for shader program '" << name << "'"; + + // release shaders + release_shaders(shader_ids); return false; } - if (this->fragment_program_id) - glsafe(::glAttachShader(this->shader_program_id, this->fragment_program_id)); - if (this->vertex_program_id) - glsafe(::glAttachShader(this->shader_program_id, this->vertex_program_id)); - glsafe(::glLinkProgram(this->shader_program_id)); + for (size_t i = 0; i < static_cast(EShaderType::Count); ++i) { + if (shader_ids[i] > 0) + glsafe(::glAttachShader(m_id, shader_ids[i])); + } + glsafe(::glLinkProgram(m_id)); GLint params; - glsafe(::glGetProgramiv(this->shader_program_id, GL_LINK_STATUS, ¶ms)); + glsafe(::glGetProgramiv(m_id, GL_LINK_STATUS, ¶ms)); if (params == GL_FALSE) { - // Linking failed. Get the log. - glsafe(::glGetProgramiv(this->shader_program_id, GL_INFO_LOG_LENGTH, ¶ms)); + // Linking failed. + glsafe(::glGetProgramiv(m_id, GL_INFO_LOG_LENGTH, ¶ms)); std::vector msg(params); - glsafe(::glGetProgramInfoLog(this->shader_program_id, params, ¶ms, msg.data())); - this->last_error = std::string("Shader linking failed:\n") + msg.data(); - this->release(); + glsafe(::glGetProgramInfoLog(m_id, params, ¶ms, msg.data())); + BOOST_LOG_TRIVIAL(error) << "Unable to link shader program '" << name << "':\n" << msg.data(); + + // release shaders + release_shaders(shader_ids); + + // release shader program + glsafe(::glDeleteProgram(m_id)); + m_id = 0; + return false; } - last_error.clear(); + // release shaders, they are no more needed + release_shaders(shader_ids); + return true; } -bool GLShader::load_from_file(const char* fragment_shader_filename, const char* vertex_shader_filename) +void GLShaderProgram::start_using() const { - const std::string& path = resources_dir() + "/shaders/"; - - boost::nowide::ifstream vs(path + std::string(vertex_shader_filename), boost::nowide::ifstream::binary); - if (!vs.good()) - return false; - - vs.seekg(0, vs.end); - int file_length = (int)vs.tellg(); - vs.seekg(0, vs.beg); - std::string vertex_shader(file_length, '\0'); - vs.read(vertex_shader.data(), file_length); - if (!vs.good()) - return false; - - vs.close(); - - boost::nowide::ifstream fs(path + std::string(fragment_shader_filename), boost::nowide::ifstream::binary); - if (!fs.good()) - return false; - - fs.seekg(0, fs.end); - file_length = (int)fs.tellg(); - fs.seekg(0, fs.beg); - std::string fragment_shader(file_length, '\0'); - fs.read(fragment_shader.data(), file_length); - if (!fs.good()) - return false; - - fs.close(); - - return load_from_text(fragment_shader.c_str(), vertex_shader.c_str()); + assert(m_id > 0); + glsafe(::glUseProgram(m_id)); } -void GLShader::release() -{ - if (this->shader_program_id) { - if (this->vertex_program_id) - glsafe(::glDetachShader(this->shader_program_id, this->vertex_program_id)); - if (this->fragment_program_id) - glsafe(::glDetachShader(this->shader_program_id, this->fragment_program_id)); - glsafe(::glDeleteProgram(this->shader_program_id)); - this->shader_program_id = 0; - } - - if (this->vertex_program_id) { - glsafe(::glDeleteShader(this->vertex_program_id)); - this->vertex_program_id = 0; - } - if (this->fragment_program_id) { - glsafe(::glDeleteShader(this->fragment_program_id)); - this->fragment_program_id = 0; - } -} - -void GLShader::enable() const -{ - glsafe(::glUseProgram(this->shader_program_id)); -} - -void GLShader::disable() const +void GLShaderProgram::stop_using() const { glsafe(::glUseProgram(0)); } -// Return shader vertex attribute ID -int GLShader::get_attrib_location(const char *name) const -{ - return this->shader_program_id ? glGetAttribLocation(this->shader_program_id, name) : -1; -} - -// Return shader uniform variable ID -int GLShader::get_uniform_location(const char *name) const -{ - return this->shader_program_id ? glGetUniformLocation(this->shader_program_id, name) : -1; -} - -bool GLShader::set_uniform(const char *name, float value) const -{ - int id = this->get_uniform_location(name); - if (id >= 0) { - glsafe(::glUniform1fARB(id, value)); - return true; - } - return false; -} - -bool GLShader::set_uniform(const char* name, const float* matrix) const +bool GLShaderProgram::set_uniform(const char* name, int value) const { int id = get_uniform_location(name); - if (id >= 0) - { - glsafe(::glUniformMatrix4fv(id, 1, GL_FALSE, (const GLfloat*)matrix)); + if (id >= 0) { + glsafe(::glUniform1i(id, static_cast(value))); return true; } return false; } -bool GLShader::set_uniform(const char* name, int value) const +bool GLShaderProgram::set_uniform(const char* name, bool value) const +{ + return set_uniform(name, value ? 1 : 0); +} + +bool GLShaderProgram::set_uniform(const char* name, float value) const { int id = get_uniform_location(name); - if (id >= 0) - { - glsafe(::glUniform1i(id, value)); + if (id >= 0) { + glsafe(::glUniform1f(id, static_cast(value))); return true; } return false; } -/* -# Set shader vector -sub SetVector +bool GLShaderProgram::set_uniform(const char* name, double value) const { - my($self,$var,@values) = @_; - - my $id = $self->Map($var); - return 'Unable to map $var' if (!defined($id)); - - my $count = scalar(@values); - eval('glUniform'.$count.'fARB($id,@values)'); - - return ''; + return set_uniform(name, static_cast(value)); } -# Set shader 4x4 matrix -sub SetMatrix +bool GLShaderProgram::set_uniform(const char* name, const std::array& value) const { - my($self,$var,$oga) = @_; - - my $id = $self->Map($var); - return 'Unable to map $var' if (!defined($id)); - - glUniformMatrix4fvARB_c($id,1,0,$oga->ptr()); - return ''; -} -*/ - -Shader::Shader() - : m_shader(nullptr) -{ -} - -Shader::~Shader() -{ - reset(); -} - -bool Shader::init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename) -{ - if (is_initialized()) + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniform2iv(id, 1, static_cast(value.data()))); return true; + } + return false; +} - m_shader = new GLShader(); - if (m_shader != nullptr) - { - if (!m_shader->load_from_file(fragment_shader_filename.c_str(), vertex_shader_filename.c_str())) - { - std::cout << "Compilaton of shader failed:" << std::endl; - std::cout << m_shader->last_error << std::endl; - reset(); - return false; +bool GLShaderProgram::set_uniform(const char* name, const std::array& value) const +{ + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniform3iv(id, 1, static_cast(value.data()))); + return true; + } + return false; +} + +bool GLShaderProgram::set_uniform(const char* name, const std::array& value) const +{ + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniform4iv(id, 1, static_cast(value.data()))); + return true; + } + return false; +} + +bool GLShaderProgram::set_uniform(const char* name, const std::array& value) const +{ + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniform2fv(id, 1, static_cast(value.data()))); + return true; + } + return false; +} + +bool GLShaderProgram::set_uniform(const char* name, const std::array& value) const +{ + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniform3fv(id, 1, static_cast(value.data()))); + return true; + } + return false; +} + +bool GLShaderProgram::set_uniform(const char* name, const std::array& value) const +{ + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniform4fv(id, 1, static_cast(value.data()))); + return true; + } + return false; +} + +bool GLShaderProgram::set_uniform(const char* name, const float* value, size_t size) const +{ + if (size == 1) + return set_uniform(name, value[0]); + else if (size < 5) { + int id = get_uniform_location(name); + if (id >= 0) { + if (size == 2) + glsafe(::glUniform2fv(id, 1, static_cast(value))); + else if (size == 3) + glsafe(::glUniform3fv(id, 1, static_cast(value))); + else + glsafe(::glUniform4fv(id, 1, static_cast(value))); + + return true; } } - - return true; + return false; } -bool Shader::is_initialized() const +bool GLShaderProgram::set_uniform(const char* name, const Transform3f& value) const { - return (m_shader != nullptr); -} - -bool Shader::start_using() const -{ - if (is_initialized()) - { - m_shader->enable(); + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniformMatrix4fv(id, 1, GL_FALSE, static_cast(value.matrix().data()))); return true; } - else - return false; + return false; } -void Shader::stop_using() const +bool GLShaderProgram::set_uniform(const char* name, const Transform3d& value) const { - if (m_shader != nullptr) - m_shader->disable(); + return set_uniform(name, value.cast()); } -int Shader::get_attrib_location(const std::string& name) const +bool GLShaderProgram::set_uniform(const char* name, const Matrix3f& value) const { - return (m_shader != nullptr) ? m_shader->get_attrib_location(name.c_str()) : -1; -} - -int Shader::get_uniform_location(const std::string& name) const -{ - return (m_shader != nullptr) ? m_shader->get_uniform_location(name.c_str()) : -1; -} - -void Shader::set_uniform(const std::string& name, float value) const -{ - if (m_shader != nullptr) - m_shader->set_uniform(name.c_str(), value); -} - -void Shader::set_uniform(const std::string& name, const float* matrix) const -{ - if (m_shader != nullptr) - m_shader->set_uniform(name.c_str(), matrix); -} - -void Shader::set_uniform(const std::string& name, bool value) const -{ - if (m_shader != nullptr) - m_shader->set_uniform(name.c_str(), value ? 1 : 0); -} - -unsigned int Shader::get_shader_program_id() const -{ - return (m_shader != nullptr) ? m_shader->shader_program_id : 0; -} - -void Shader::reset() -{ - if (m_shader != nullptr) - { - m_shader->release(); - delete m_shader; - m_shader = nullptr; + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniformMatrix3fv(id, 1, GL_FALSE, static_cast(value.data()))); + return true; } + return false; +} + +bool GLShaderProgram::set_uniform(const char* name, const Vec3f& value) const +{ + int id = get_uniform_location(name); + if (id >= 0) { + glsafe(::glUniform3fv(id, 1, static_cast(value.data()))); + return true; + } + return false; +} + +bool GLShaderProgram::set_uniform(const char* name, const Vec3d& value) const +{ + return set_uniform(name, static_cast(value.cast())); +} + +int GLShaderProgram::get_attrib_location(const char* name) const +{ + return (m_id > 0) ? ::glGetAttribLocation(m_id, name) : -1; +} + +int GLShaderProgram::get_uniform_location(const char* name) const +{ + return (m_id > 0) ? ::glGetUniformLocation(m_id, name) : -1; } } // namespace Slic3r diff --git a/src/slic3r/GUI/GLShader.hpp b/src/slic3r/GUI/GLShader.hpp index df2a23f15c..84fdf5ebad 100644 --- a/src/slic3r/GUI/GLShader.hpp +++ b/src/slic3r/GUI/GLShader.hpp @@ -1,71 +1,69 @@ #ifndef slic3r_GLShader_hpp_ #define slic3r_GLShader_hpp_ -#include "libslic3r/libslic3r.h" +#include +#include + #include "libslic3r/Point.hpp" namespace Slic3r { -class GLShader +class GLShaderProgram { public: - GLShader() : - fragment_program_id(0), - vertex_program_id(0), - shader_program_id(0) - {} - ~GLShader(); + enum class EShaderType + { + Vertex, + Fragment, + Geometry, + TessEvaluation, + TessControl, + Compute, + Count + }; - bool load_from_text(const char *fragment_shader, const char *vertex_shader); - bool load_from_file(const char* fragment_shader_filename, const char* vertex_shader_filename); - - void release(); - - int get_attrib_location(const char *name) const; - int get_uniform_location(const char *name) const; - - bool set_uniform(const char *name, float value) const; - bool set_uniform(const char* name, const float* matrix) const; - bool set_uniform(const char* name, int value) const; - - void enable() const; - void disable() const; - - unsigned int fragment_program_id; - unsigned int vertex_program_id; - unsigned int shader_program_id; - std::string last_error; -}; - -class Shader -{ - GLShader* m_shader; - -public: - Shader(); - ~Shader(); - - bool init(const std::string& vertex_shader_filename, const std::string& fragment_shader_filename); - - bool is_initialized() const; - - bool start_using() const; - void stop_using() const; - - int get_attrib_location(const std::string& name) const; - int get_uniform_location(const std::string& name) const; - - void set_uniform(const std::string& name, float value) const; - void set_uniform(const std::string& name, const float* matrix) const; - void set_uniform(const std::string& name, bool value) const; - - const GLShader* get_shader() const { return m_shader; } - unsigned int get_shader_program_id() const; + typedef std::array(EShaderType::Count)> ShaderFilenames; + typedef std::array(EShaderType::Count)> ShaderSources; private: - void reset(); + std::string m_name; + unsigned int m_id{ 0 }; + +public: + ~GLShaderProgram(); + + bool init_from_files(const std::string& name, const ShaderFilenames& filenames); + bool init_from_texts(const std::string& name, const ShaderSources& sources); + + const std::string& get_name() const { return m_name; } + unsigned int get_id() const { return m_id; } + + void start_using() const; + void stop_using() const; + + bool set_uniform(const char* name, int value) const; + bool set_uniform(const char* name, bool value) const; + bool set_uniform(const char* name, float value) const; + bool set_uniform(const char* name, double value) const; + bool set_uniform(const char* name, const std::array& value) const; + bool set_uniform(const char* name, const std::array& value) const; + bool set_uniform(const char* name, const std::array& value) const; + bool set_uniform(const char* name, const std::array& value) const; + bool set_uniform(const char* name, const std::array& value) const; + bool set_uniform(const char* name, const std::array& value) const; + bool set_uniform(const char* name, const float* value, size_t size) const; + bool set_uniform(const char* name, const Transform3f& value) const; + bool set_uniform(const char* name, const Transform3d& value) const; + bool set_uniform(const char* name, const Matrix3f& value) const; + bool set_uniform(const char* name, const Vec3f& value) const; + bool set_uniform(const char* name, const Vec3d& value) const; + + // returns -1 if not found + int get_attrib_location(const char* name) const; + // returns -1 if not found + int get_uniform_location(const char* name) const; }; -} +} // namespace Slic3r #endif /* slic3r_GLShader_hpp_ */ diff --git a/src/slic3r/GUI/GLShadersManager.cpp b/src/slic3r/GUI/GLShadersManager.cpp new file mode 100644 index 0000000000..1041faa3dc --- /dev/null +++ b/src/slic3r/GUI/GLShadersManager.cpp @@ -0,0 +1,75 @@ +#include "libslic3r/libslic3r.h" +#include "GLShadersManager.hpp" +#include "3DScene.hpp" +#include "GUI_App.hpp" + +#include +#include + +#include + +namespace Slic3r { + +std::pair GLShadersManager::init() +{ + std::string error; + + auto append_shader = [this, &error](const std::string& name, const GLShaderProgram::ShaderFilenames& filenames) { + m_shaders.push_back(std::make_unique()); + if (!m_shaders.back()->init_from_files(name, filenames)) { + error += name + "\n"; + // if any error happens while initializating the shader, we remove it from the list + m_shaders.pop_back(); + return false; + } + return true; + }; + + assert(m_shaders.empty()); + + bool valid = true; + + // used to render bed axes and model, selection hints, gcode sequential view marker model, preview shells + valid &= append_shader("gouraud_light", { "gouraud_light.vs", "gouraud_light.fs" }); + // used to render printbed + valid &= append_shader("printbed", { "printbed.vs", "printbed.fs" }); + // used to render options in gcode preview + valid &= append_shader("options_110", { "options_110.vs", "options_110.fs" }); + if (GUI::wxGetApp().is_glsl_version_greater_or_equal_to(1, 20)) + valid &= append_shader("options_120", { "options_120.vs", "options_120.fs" }); + // used to render extrusion and travel paths as lines in gcode preview + valid &= append_shader("toolpaths_lines", { "toolpaths_lines.vs", "toolpaths_lines.fs" }); + // used to render objects in 3d editor + valid &= append_shader("gouraud", { "gouraud.vs", "gouraud.fs" }); + // used to render variable layers heights in 3d editor + valid &= append_shader("variable_layer_height", { "variable_layer_height.vs", "variable_layer_height.fs" }); + + return { valid, error }; +} + +void GLShadersManager::shutdown() +{ + for (std::unique_ptr& shader : m_shaders) { + shader.reset(); + } +} + +GLShaderProgram* GLShadersManager::get_shader(const std::string& shader_name) +{ + auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [shader_name](std::unique_ptr& p) { return p->get_name() == shader_name; }); + return (it != m_shaders.end()) ? it->get() : nullptr; +} + +GLShaderProgram* GLShadersManager::get_current_shader() +{ + GLint id = 0; + glsafe(::glGetIntegerv(GL_CURRENT_PROGRAM, &id)); + if (id == 0) + return nullptr; + + auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [id](std::unique_ptr& p) { return static_cast(p->get_id()) == id; }); + return (it != m_shaders.end()) ? it->get() : nullptr; +} + +} // namespace Slic3r + diff --git a/src/slic3r/GUI/GLShadersManager.hpp b/src/slic3r/GUI/GLShadersManager.hpp new file mode 100644 index 0000000000..b2bbc140bd --- /dev/null +++ b/src/slic3r/GUI/GLShadersManager.hpp @@ -0,0 +1,30 @@ +#ifndef slic3r_GLShadersManager_hpp_ +#define slic3r_GLShadersManager_hpp_ + +#include "GLShader.hpp" + +#include +#include +#include + +namespace Slic3r { + +class GLShadersManager +{ + std::vector> m_shaders; + +public: + std::pair init(); + // call this method before to release the OpenGL context + void shutdown(); + + // returns nullptr if not found + GLShaderProgram* get_shader(const std::string& shader_name); + + // returns currently active shader, nullptr if none + GLShaderProgram* get_current_shader(); +}; + +} // namespace Slic3r + +#endif // slic3r_GLShadersManager_hpp_ diff --git a/src/slic3r/GUI/GLToolbar.cpp b/src/slic3r/GUI/GLToolbar.cpp index 4ab282b066..46371b037a 100644 --- a/src/slic3r/GUI/GLToolbar.cpp +++ b/src/slic3r/GUI/GLToolbar.cpp @@ -230,24 +230,13 @@ void GLToolbar::set_icons_size(float size) void GLToolbar::set_scale(float scale) { - if (m_layout.scale != scale) - { + if (m_layout.scale != scale) { m_layout.scale = scale; m_layout.dirty = true; m_icons_texture_dirty = true; } } -bool GLToolbar::is_enabled() const -{ - return m_enabled; -} - -void GLToolbar::set_enabled(bool enable) -{ - m_enabled = enable;//true; etFIXME -} - bool GLToolbar::add_item(const GLToolbarItem::Data& data) { GLToolbarItem* item = new GLToolbarItem(GLToolbarItem::Action, data); diff --git a/src/slic3r/GUI/GLToolbar.hpp b/src/slic3r/GUI/GLToolbar.hpp index 41c2735c9a..74e18de975 100644 --- a/src/slic3r/GUI/GLToolbar.hpp +++ b/src/slic3r/GUI/GLToolbar.hpp @@ -276,8 +276,8 @@ public: void set_icons_size(float size); void set_scale(float scale); - bool is_enabled() const; - void set_enabled(bool enable); + bool is_enabled() const { return m_enabled; } + void set_enabled(bool enable) { m_enabled = enable; } bool add_item(const GLToolbarItem::Data& data); bool add_separator(); diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index c119112c2c..6c76b62272 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -194,6 +194,8 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt config.set_key_value(opt_key, new ConfigOptionEnum(boost::any_cast(value))); else if(opt_key.compare("support_pillar_connection_mode") == 0) config.set_key_value(opt_key, new ConfigOptionEnum(boost::any_cast(value))); + else if(opt_key == "printhost_authorization_type") + config.set_key_value(opt_key, new ConfigOptionEnum(boost::any_cast(value))); } break; case coPoints:{ @@ -253,7 +255,7 @@ void warning_catcher(wxWindow* parent, const wxString& message) msg.ShowModal(); } -void create_combochecklist(wxComboCtrl* comboCtrl, std::string text, std::string items, bool initial_value) +void create_combochecklist(wxComboCtrl* comboCtrl, const std::string& text, const std::string& items) { if (comboCtrl == nullptr) return; @@ -264,41 +266,59 @@ void create_combochecklist(wxComboCtrl* comboCtrl, std::string text, std::string // On the other side, with this line the combo box popup cannot be closed by clicking on the combo button on Windows 10. comboCtrl->UseAltPopupWindow(); + int max_width = 0; + // the following line messes up the popup size the first time it is shown on wxWidgets 3.1.3 // comboCtrl->EnablePopupAnimation(false); comboCtrl->SetPopupControl(popup); - popup->SetStringValue(from_u8(text)); - popup->Bind(wxEVT_CHECKLISTBOX, [popup](wxCommandEvent& evt) { popup->OnCheckListBox(evt); }); - popup->Bind(wxEVT_LISTBOX, [popup](wxCommandEvent& evt) { popup->OnListBoxSelection(evt); }); + wxString title = from_u8(text); + max_width = std::max(max_width, 60 + comboCtrl->GetTextExtent(title).x); + popup->SetStringValue(title); + popup->Bind(wxEVT_CHECKLISTBOX, [popup](wxCommandEvent& evt) { popup->OnCheckListBox(evt); }); + popup->Bind(wxEVT_LISTBOX, [popup](wxCommandEvent& evt) { popup->OnListBoxSelection(evt); }); popup->Bind(wxEVT_KEY_DOWN, [popup](wxKeyEvent& evt) { popup->OnKeyEvent(evt); }); popup->Bind(wxEVT_KEY_UP, [popup](wxKeyEvent& evt) { popup->OnKeyEvent(evt); }); std::vector items_str; boost::split(items_str, items, boost::is_any_of("|"), boost::token_compress_off); - for (const std::string& item : items_str) { - popup->Append(from_u8(item)); - } + // each item must be composed by 2 parts + assert(items_str.size() %2 == 0); - for (unsigned int i = 0; i < popup->GetCount(); ++i) { - popup->Check(i, initial_value); - } - } + for (size_t i = 0; i < items_str.size(); i += 2) { + wxString label = from_u8(items_str[i]); + max_width = std::max(max_width, 60 + popup->GetTextExtent(label).x); + popup->Append(label); + popup->Check(i / 2, items_str[i + 1] == "1"); + } + + comboCtrl->SetMinClientSize(wxSize(max_width, -1)); + } } -int combochecklist_get_flags(wxComboCtrl* comboCtrl) +unsigned int combochecklist_get_flags(wxComboCtrl* comboCtrl) { - int flags = 0; + unsigned int flags = 0; - wxCheckListBoxComboPopup* popup = wxDynamicCast(comboCtrl->GetPopupControl(), wxCheckListBoxComboPopup); - if (popup != nullptr) { - for (unsigned int i = 0; i < popup->GetCount(); ++i) { - if (popup->IsChecked(i)) - flags |= 1 << i; - } - } + wxCheckListBoxComboPopup* popup = wxDynamicCast(comboCtrl->GetPopupControl(), wxCheckListBoxComboPopup); + if (popup != nullptr) { + for (unsigned int i = 0; i < popup->GetCount(); ++i) { + if (popup->IsChecked(i)) + flags |= 1 << i; + } + } - return flags; + return flags; +} + +void combochecklist_set_flags(wxComboCtrl* comboCtrl, unsigned int flags) +{ + wxCheckListBoxComboPopup* popup = wxDynamicCast(comboCtrl->GetPopupControl(), wxCheckListBoxComboPopup); + if (popup != nullptr) { + for (unsigned int i = 0; i < popup->GetCount(); ++i) { + popup->Check(i, (flags & (1 << i)) != 0); + } + } } AppConfig* get_app_config() diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index a54288df45..cf133971e3 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -49,13 +49,17 @@ inline void show_info(wxWindow* parent, const std::string& message,const std::st void warning_catcher(wxWindow* parent, const wxString& message); // Creates a wxCheckListBoxComboPopup inside the given wxComboCtrl, filled with the given text and items. -// Items are all initialized to the given value. -// Items must be separated by '|', for example "Item1|Item2|Item3", and so on. -void create_combochecklist(wxComboCtrl* comboCtrl, std::string text, std::string items, bool initial_value); +// Items data must be separated by '|', and contain the item name to be shown followed by its initial value (0 for false, 1 for true). +// For example "Item1|0|Item2|1|Item3|0", and so on. +void create_combochecklist(wxComboCtrl* comboCtrl, const std::string& text, const std::string& items); // Returns the current state of the items listed in the wxCheckListBoxComboPopup contained in the given wxComboCtrl, -// encoded inside an int. -int combochecklist_get_flags(wxComboCtrl* comboCtrl); +// encoded inside an unsigned int. +unsigned int combochecklist_get_flags(wxComboCtrl* comboCtrl); + +// Sets the current state of the items listed in the wxCheckListBoxComboPopup contained in the given wxComboCtrl, +// with the flags encoded in the given unsigned int. +void combochecklist_set_flags(wxComboCtrl* comboCtrl, unsigned int flags); // wxString conversions: diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index a7b562bd75..40c8f96e59 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -1,3 +1,4 @@ +#include "libslic3r/Technologies.hpp" #include "GUI_App.hpp" #include "GUI_ObjectList.hpp" #include "GUI_ObjectManipulation.hpp" @@ -28,17 +29,21 @@ #include #include +#include +#include +#include + #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/I18N.hpp" +#include "libslic3r/PresetBundle.hpp" #include "GUI.hpp" #include "GUI_Utils.hpp" -#include "AppConfig.hpp" -#include "PresetBundle.hpp" #include "3DScene.hpp" #include "MainFrame.hpp" #include "Plater.hpp" +#include "GLCanvas3D.hpp" #include "../Utils/PresetUpdater.hpp" #include "../Utils/PrintHost.hpp" @@ -54,6 +59,9 @@ #include "Mouse3DController.hpp" #include "RemovableDriveManager.hpp" #include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "UnsavedChangesDialog.hpp" +#include "PresetComboBoxes.hpp" #ifdef __WXMSW__ #include @@ -70,6 +78,231 @@ namespace GUI { class MainFrame; +class SplashScreen : public wxSplashScreen +{ +public: + SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition, bool is_decorated = false) + : wxSplashScreen(bitmap, splashStyle, milliseconds, nullptr, wxID_ANY, + wxDefaultPosition, wxDefaultSize, wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR ) + { + wxASSERT(bitmap.IsOk()); + m_main_bitmap = bitmap; + + if (!is_decorated) + Decorate(m_main_bitmap, pos, true); + + m_scale = get_display_scale(pos); + m_font = get_scaled_sys_font(get_splashscreen_display_scale_factor(pos)).Bold().Larger(); + + if (pos != wxDefaultPosition) { + this->SetPosition(pos); + this->CenterOnScreen(); + } + } + + void SetText(const wxString& text) + { + set_bitmap(m_main_bitmap); + if (!text.empty()) { + wxBitmap bitmap(m_main_bitmap); + + wxMemoryDC memDC; + memDC.SelectObject(bitmap); + + wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + memDC.SetFont(m_font); + memDC.SetTextForeground(wxColour(237, 107, 33)); + memDC.DrawText(text, int(m_scale * 45), int(m_scale * 200)); + + memDC.SelectObject(wxNullBitmap); + set_bitmap(bitmap); + } + } + + static bool Decorate(wxBitmap& bmp, wxPoint screen_pos = wxDefaultPosition, bool force_decor = false) + { + if (!bmp.IsOk()) + return false; + + float screen_sf = get_splashscreen_display_scale_factor(screen_pos); + float screen_scale = get_display_scale(screen_pos); + + if (screen_sf == 1.0) { + // it means that we have just one display or all displays have a same scale + // Scale bitmap for this display and continue the decoration + scale_bitmap(bmp, screen_scale); + } + else if (force_decor) { + // if we are here, it means that bitmap is already scaled for the main display + // and now we should just scale it th the secondary monitor and continue the decoration + scale_bitmap(bmp, screen_sf); + } + else { + // if screens have different scale and this function is called with force_decor == false + // then just rescale the bitmap for the main display scale + scale_bitmap(bmp, get_display_scale()); + return false; + // Decoration will be continued later, from the SplashScreen constructor + } + + // use a memory DC to draw directly onto the bitmap + wxMemoryDC memDc(bmp); + + // draw an dark grey box at the left of the splashscreen. + // this box will be 2/5 of the weight of the bitmap, and be at the left. + int banner_width = (bmp.GetWidth() / 5) * 2 - 2; + const wxRect banner_rect(wxPoint(0, (bmp.GetHeight() / 9) * 2), wxPoint(banner_width, bmp.GetHeight())); + wxDCBrushChanger bc(memDc, wxBrush(wxColour(51, 51, 51))); + wxDCPenChanger pc(memDc, wxPen(wxColour(51, 51, 51))); + memDc.DrawRectangle(banner_rect); + + wxFont sys_font = get_scaled_sys_font(screen_sf); + + // title +#if ENABLE_GCODE_VIEWER + wxString title_string = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; +#else + wxString title_string = SLIC3R_APP_NAME; +#endif // ENABLE_GCODE_VIEWER + + wxFont title_font = sys_font; + title_font.SetPointSize(3 * sys_font.GetPointSize()); + + // dynamically get the version to display + wxString version_string = _L("Version") + " " + std::string(SLIC3R_VERSION); + wxFont version_font = sys_font.Larger().Larger(); + + // create a copyright notice that uses the year that this file was compiled + wxString year(__DATE__); + wxString cr_symbol = wxString::FromUTF8("\xc2\xa9"); + wxString copyright_string = wxString::Format("%s 2016-%s Prusa Research.\n" + "%s 2011-2018 Alessandro Ranellucci.", + cr_symbol, year.Mid(year.Length() - 4), cr_symbol) + "\n\n"; + wxFont copyright_font = sys_font.Larger(); + + copyright_string += //"Slic3r" + _L("is licensed under the") + _L("GNU Affero General Public License, version 3") + "\n\n" + + _L("PrusaSlicer is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n\n" + + _L("Contributions by Henrik Brix Andersen, Nicolas Dandrimont, Mark Hindess, Petr Ledvina, Joseph Lenox, Y. Sapir, Mike Sheldrake, Vojtech Bubnik and numerous others.") + "\n\n" + + _L("Splash screen could be desabled from the \"Preferences\""); + + word_wrap_string(copyright_string, banner_width, screen_scale); + + wxCoord margin = int(screen_scale * 20); + + // draw the (orange) labels inside of our black box (at the left of the splashscreen) + memDc.SetTextForeground(wxColour(237, 107, 33)); + + memDc.SetFont(title_font); + memDc.DrawLabel(title_string, banner_rect.Deflate(margin, 0), wxALIGN_TOP | wxALIGN_LEFT); + + memDc.SetFont(version_font); + memDc.DrawLabel(version_string, banner_rect.Deflate(margin, 2 * margin), wxALIGN_TOP | wxALIGN_LEFT); + + memDc.SetFont(copyright_font); + memDc.DrawLabel(copyright_string, banner_rect.Deflate(margin, margin), wxALIGN_BOTTOM | wxALIGN_LEFT); + + return true; + } + +private: + wxBitmap m_main_bitmap; + wxFont m_font; + float m_scale {1.0}; + + void set_bitmap(wxBitmap& bmp) + { + m_window->SetBitmap(bmp); + m_window->Refresh(); + m_window->Update(); + } + + static float get_splashscreen_display_scale_factor(wxPoint pos = wxDefaultPosition) + { + if (wxDisplay::GetCount() == 1) + return 1.0; + + wxFrame main_screen_fr(nullptr, wxID_ANY, wxEmptyString); + wxFrame splash_screen_fr(nullptr, wxID_ANY, wxEmptyString, pos); + +#if ENABLE_WX_3_1_3_DPI_CHANGED_EVENT && !defined(__WXGTK__) + int main_dpi = get_dpi_for_window(&main_screen_fr); + int splash_dpi = get_dpi_for_window(&splash_screen_fr); + float sf = (float)splash_dpi / (float)main_dpi; +#else + // initialize default width_unit according to the width of the one symbol ("m") of the currently active font of this window. + float sf = (float)splash_screen_fr.GetTextExtent("m").x / (float)main_screen_fr.GetTextExtent("m").x; +#endif // ENABLE_WX_3_1_3_DPI_CHANGED_EVENT + + return sf; + } + + static float get_display_scale(wxPoint pos = wxDefaultPosition) + { + // pos equals to wxDefaultPosition, when display is main + wxFrame fr(nullptr, wxID_ANY, wxEmptyString, pos); + +#if ENABLE_WX_3_1_3_DPI_CHANGED_EVENT && !defined(__WXGTK__) + int dpi = get_dpi_for_window(&fr); + float scale = dpi != DPI_DEFAULT ? (float)dpi / DPI_DEFAULT : 1.0; +#else + // initialize default width_unit according to the width of the one symbol ("m") of the currently active font of this window. + float scale = 0.1 * std::max(10, fr.GetTextExtent("m").x - 1); +#endif // ENABLE_WX_3_1_3_DPI_CHANGED_EVENT + + return scale; + } + + static void scale_bitmap(wxBitmap& bmp, float scale) + { + if (scale == 1.0) + return; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) + return; + + int width = int(scale * image.GetWidth()); + int height = int(scale * image.GetHeight()); + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + bmp = wxBitmap(std::move(image)); + } + + static wxFont get_scaled_sys_font(float screen_sf) + { + wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + if (screen_sf != 1.0) + font.SetPointSize(int(screen_sf * (float)font.GetPointSize())); + + return font; + } + + static void word_wrap_string(wxString& input, int line_px_len, float scalef) + { + // calculate count od symbols in one line according to the scale + int line_len = int((float)line_px_len / (scalef * 10) + 0.5) + 10; + + int idx = -1; + int cur_len = 0; + for (size_t i = 0; i < input.Len(); i++) + { + cur_len++; + if (input[i] == ' ') + idx = i; + if (input[i] == '\n') + { + idx = -1; + cur_len = 0; + } + if (cur_len >= line_len && idx >= 0) + { + input[idx] = '\n'; + cur_len = static_cast(i) - idx; + } + } + } +}; + wxString file_wildcards(FileType file_type, const std::string &custom_extension) { static const std::string defaults[FT_SIZE] = { @@ -262,8 +495,15 @@ static void generic_exception_handle() IMPLEMENT_APP(GUI_App) +#if ENABLE_GCODE_VIEWER +GUI_App::GUI_App(EAppMode mode) +#else GUI_App::GUI_App() +#endif // ENABLE_GCODE_VIEWER : wxApp() +#if ENABLE_GCODE_VIEWER + , m_app_mode(mode) +#endif // ENABLE_GCODE_VIEWER , m_em_unit(10) , m_imgui(new ImGuiWrapper()) , m_wizard(nullptr) @@ -319,10 +559,22 @@ void GUI_App::init_app_config() if (!app_config) app_config = new AppConfig(); +#if ENABLE_GCODE_VIEWER + if (is_gcode_viewer()) + // disable config save to avoid to mess it up for the editor + app_config->enable_save(false); +#endif // ENABLE_GCODE_VIEWER + // load settings app_conf_exists = app_config->exists(); if (app_conf_exists) { - app_config->load(); + std::string error = app_config->load(); + if (!error.empty()) + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + AppConfig::config_path() + "\n\n" + error); } } @@ -349,18 +601,18 @@ bool GUI_App::on_init_inner() wxCHECK_MSG(wxDirExists(resources_dir), false, wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); - // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. + // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. // wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible // performance when working on high resolution multi-display setups. // wxSystemOptions::SetOption("msw.notebook.themed-background", 0); // Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; - + std::string msg = Http::tls_global_init(); std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); - + if (!msg.empty() && !ssl_accept) { wxRichMessageDialog dlg(nullptr, @@ -370,29 +622,74 @@ bool GUI_App::on_init_inner() if (dlg.ShowModal() != wxID_YES) return false; app_config->set("tls_cert_store_accepted", - dlg.IsCheckBoxChecked() ? "yes" : "no"); + dlg.IsCheckBoxChecked() ? "yes" : "no"); app_config->set("tls_accepted_cert_store_location", - dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); + dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); } - + app_config->set("version", SLIC3R_VERSION); app_config->save(); - +/* + if (wxImage::FindHandler(wxBITMAP_TYPE_JPEG) == nullptr) + wxImage::AddHandler(new wxJPEGHandler()); + if (wxImage::FindHandler(wxBITMAP_TYPE_PNG) == nullptr) + wxImage::AddHandler(new wxPNGHandler()); +*/ + wxInitAllImageHandlers(); + + SplashScreen* scrn = nullptr; + if (app_config->get("show_splash_screen") == "1") + { +#if ENABLE_GCODE_VIEWER + wxBitmap bmp(is_editor() ? from_u8(var("splashscreen.jpg")) : from_u8(var("splashscreen-gcodeviewer.jpg")), wxBITMAP_TYPE_JPEG); +#else + wxBitmap bmp(from_u8(var("splashscreen.jpg")), wxBITMAP_TYPE_JPEG); +#endif // ENABLE_GCODE_VIEWER + + // Detect position (display) to show the splash screen + // Now this position is equal to the mainframe position + wxPoint splashscreen_pos = wxDefaultPosition; + if (app_config->has("window_mainframe")) { + auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); + if (metrics) + splashscreen_pos = metrics->get_rect().GetPosition(); + } + + // try to decorate and/or scale the bitmap before splash screen creation + bool is_decorated = SplashScreen::Decorate(bmp, splashscreen_pos); + + // create splash screen with updated bmp + scrn = new SplashScreen(bmp.IsOk() ? bmp : create_scaled_bitmap("prusa_slicer_logo", nullptr, 400), + wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos, is_decorated); + scrn->SetText(_L("Loading configuration...")); + } + preset_bundle = new PresetBundle(); - + // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory // supplied as argument to --datadir; in that case we should still run the wizard preset_bundle->setup_directories(); -#ifdef __WXMSW__ - associate_3mf_files(); +#if ENABLE_GCODE_VIEWER + if (is_editor()) { +#endif // ENABLE_GCODE_VIEWER +#ifdef __WXMSW__ + associate_3mf_files(); #endif // __WXMSW__ - preset_updater = new PresetUpdater(); - Bind(EVT_SLIC3R_VERSION_ONLINE, [this](const wxCommandEvent &evt) { - app_config->set("version_online", into_u8(evt.GetString())); - app_config->save(); - }); + preset_updater = new PresetUpdater(); + Bind(EVT_SLIC3R_VERSION_ONLINE, [this](const wxCommandEvent& evt) { + app_config->set("version_online", into_u8(evt.GetString())); + app_config->save(); + if (this->plater_ != nullptr) { + if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(into_u8(evt.GetString()))) { + this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAviable, *(this->plater_->get_current_canvas3D())); + } + } + }); +#if ENABLE_GCODE_VIEWER + } +#endif // ENABLE_GCODE_VIEWER // initialize label colors and fonts init_label_colours(); @@ -420,8 +717,13 @@ bool GUI_App::on_init_inner() Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); // application frame - if (wxImage::FindHandler(wxBITMAP_TYPE_PNG) == nullptr) - wxImage::AddHandler(new wxPNGHandler()); +#if ENABLE_GCODE_VIEWER + if (scrn && is_editor()) +#else + if (scrn) +#endif // ENABLE_GCODE_VIEWER + scrn->SetText(_L("Creating settings tabs...")); + mainframe = new MainFrame(); // hide settings tabs after first Layout mainframe->select_tab(0); @@ -455,13 +757,20 @@ bool GUI_App::on_init_inner() static bool once = true; if (once) { once = false; - check_updates(false); +#if ENABLE_GCODE_VIEWER + if (preset_updater != nullptr) { +#endif // ENABLE_GCODE_VIEWER + check_updates(false); + + CallAfter([this] { + config_wizard_startup(); + preset_updater->slic3r_update_notify(); + preset_updater->sync(preset_bundle); + }); +#if ENABLE_GCODE_VIEWER + } +#endif // ENABLE_GCODE_VIEWER - CallAfter([this] { - config_wizard_startup(); - preset_updater->slic3r_update_notify(); - preset_updater->sync(preset_bundle); - }); #ifdef _WIN32 //sets window property to mainframe so other instances can indentify it OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); @@ -469,8 +778,16 @@ bool GUI_App::on_init_inner() } }); - load_current_presets(); - +#if ENABLE_GCODE_VIEWER + if (is_gcode_viewer()) { + mainframe->update_layout(); + if (plater_ != nullptr) + // ensure the selected technology is ptFFF + plater_->set_printer_technology(ptFFF); + } + else +#endif // ENABLE_GCODE_VIEWER + load_current_presets(); mainframe->Show(true); /* Temporary workaround for the correct behavior of the Scrolled sidebar panel: @@ -629,6 +946,27 @@ void GUI_App::set_auto_toolbar_icon_scale(float scale) const app_config->set("auto_toolbar_size", val); } +// check user printer_presets for the containing information about "Print Host upload" +void GUI_App::check_printer_presets() +{ + std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); + if (preset_names.empty()) + return; + + wxString msg_text = _L("You have next presets with saved options for \"Print Host upload\"") + ":"; + for (const std::string& preset_name : preset_names) + msg_text += "\n \"" + from_u8(preset_name) + "\","; + msg_text.RemoveLast(); + msg_text += "\n\n" + _L("But from this version of PrusaSlicer we don't show/use this information in Printer Settings.\n" + "Now, this information will be exposed in physical printers settings.") + "\n\n" + + _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" + "Note: This name can be changed later from the physical printers settings"); + + wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + + preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); +} + void GUI_App::recreate_GUI(const wxString& msg_name) { mainframe->shutdown(); @@ -754,6 +1092,20 @@ void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const dialog.GetPaths(input_files); } +#if ENABLE_GCODE_VIEWER +void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _(L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):")), + app_config->get_last_dir(), "", + file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} +#endif // ENABLE_GCODE_VIEWER + bool GUI_App::switch_language() { if (select_language()) { @@ -937,7 +1289,7 @@ bool GUI_App::load_language(wxString language, bool initial) m_imgui->set_language(into_u8(language_info->CanonicalName)); //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. wxSetlocale(LC_NUMERIC, "C"); - Preset::update_suffix_modified(); + Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); return true; } @@ -977,6 +1329,7 @@ void GUI_App::update_mode() tab->update_mode(); plater()->update_object_menu(); + plater()->canvas3D()->update_gizmos_on_off_state(); } void GUI_App::add_config_menu(wxMenuBar *menu) @@ -1061,34 +1414,21 @@ void GUI_App::add_config_menu(wxMenuBar *menu) break; case ConfigMenuPreferences: { -#if ENABLE_LAYOUT_NO_RESTART bool app_layout_changed = false; -#else - bool recreate_app = false; -#endif // ENABLE_LAYOUT_NO_RESTART { // the dialog needs to be destroyed before the call to recreate_GUI() // or sometimes the application crashes into wxDialogBase() destructor // so we put it into an inner scope PreferencesDialog dlg(mainframe); dlg.ShowModal(); -#if ENABLE_LAYOUT_NO_RESTART app_layout_changed = dlg.settings_layout_changed(); -#else - recreate_app = dlg.settings_layout_changed(); -#endif // ENABLE_LAYOUT_NO_RESTART } -#if ENABLE_LAYOUT_NO_RESTART if (app_layout_changed) { - mainframe->GetSizer()->Hide((size_t)0); + // hide full main_sizer for mainFrame + mainframe->GetSizer()->Show(false); mainframe->update_layout(); mainframe->select_tab(0); - mainframe->GetSizer()->Show((size_t)0); } -#else - if (recreate_app) - recreate_GUI(_L("Changing of the settings layout") + dots); -#endif // ENABLE_LAYOUT_NO_RESTART break; } case ConfigMenuLanguage: @@ -1135,29 +1475,66 @@ void GUI_App::add_config_menu(wxMenuBar *menu) // to notify the user whether he is aware that some preset changes will be lost. bool GUI_App::check_unsaved_changes(const wxString &header) { - wxString dirty; PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab *tab : tabs_list) + + bool has_unsaved_changes = false; + for (Tab* tab : tabs_list) if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) { - if (dirty.empty()) - dirty = tab->title(); - else - dirty += wxString(", ") + tab->title(); + has_unsaved_changes = true; + break; } - if (dirty.empty()) - // No changes, the application may close or reload presets. - return true; - // Ask the user. - wxString message; - if (! header.empty()) - message = header + "\n\n"; - message += _(L("The presets on the following tabs were modified")) + ": " + dirty + "\n\n" + _(L("Discard changes and continue anyway?")); - wxMessageDialog dialog(mainframe, - message, - wxString(SLIC3R_APP_NAME) + " - " + _(L("Unsaved Presets")), - wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); - return dialog.ShowModal() == wxID_YES; + if (has_unsaved_changes) + { + UnsavedChangesDialog dlg(header); + if (dlg.ShowModal() == wxID_CANCEL) + return false; + + if (dlg.save_preset()) // save selected changes + { + struct NameType + { + std::string name; + Preset::Type type {Preset::TYPE_INVALID}; + }; + + std::vector names_and_types; + + // for system/default/external presets we should take an edited name + std::vector types; + for (Tab* tab : tabs_list) + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + { + const Preset& preset = tab->get_presets()->get_edited_preset(); + if (preset.is_system || preset.is_default || preset.is_external) + types.emplace_back(preset.type); + + names_and_types.emplace_back(NameType{ preset.name, preset.type }); + } + + + if (!types.empty()) { + SavePresetDialog save_dlg(types); + if (save_dlg.ShowModal() != wxID_OK) + return false; + + for (NameType& nt : names_and_types) { + const std::string name = save_dlg.get_name(nt.type); + if (!name.empty()) + nt.name = name; + } + } + + for (const NameType& nt : names_and_types) + preset_bundle->save_changes_for_preset(nt.name, nt.type, dlg.get_unselected_options(nt.type)); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + } + } + + return true; } bool GUI_App::checked_tab(Tab* tab) @@ -1171,6 +1548,10 @@ bool GUI_App::checked_tab(Tab* tab) // Update UI / Tabs to reflect changes in the currently loaded presets void GUI_App::load_current_presets() { + // check printer_presets for the containing information about "Print Host upload" + // and create physical printer from it, if any exists + check_printer_presets(); + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); this->plater()->set_printer_technology(printer_technology); for (Tab *tab : tabs_list) @@ -1452,7 +1833,7 @@ void GUI_App::check_updates(const bool verbose) PresetUpdater::UpdateResult updater_result; try { - updater_result = preset_updater->config_update(app_config->orig_version()); + updater_result = preset_updater->config_update(app_config->orig_version(), verbose); if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { mainframe->Close(); } diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index c2b257f458..9bf470a42d 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -3,10 +3,10 @@ #include #include -#include "Preset.hpp" #include "ImGuiWrapper.hpp" #include "ConfigWizard.hpp" #include "OpenGLManager.hpp" +#include "libslic3r/Preset.hpp" #include #include @@ -86,10 +86,30 @@ class ConfigWizard; static wxString dots("…", wxConvUTF8); +// Does our wxWidgets version support markup? +// https://github.com/prusa3d/PrusaSlicer/issues/4282#issuecomment-634676371 +#if wxUSE_MARKUP && wxCHECK_VERSION(3, 1, 1) + #define SUPPORTS_MARKUP +#endif + class GUI_App : public wxApp { +#if ENABLE_GCODE_VIEWER +public: + enum class EAppMode : unsigned char + { + Editor, + GCodeViewer + }; + +private: +#endif // ENABLE_GCODE_VIEWER + bool m_initialized { false }; bool app_conf_exists{ false }; +#if ENABLE_GCODE_VIEWER + EAppMode m_app_mode{ EAppMode::Editor }; +#endif // ENABLE_GCODE_VIEWER wxColour m_color_label_modified; wxColour m_color_label_sys; @@ -119,13 +139,24 @@ class GUI_App : public wxApp std::unique_ptr m_single_instance_checker; std::string m_instance_hash_string; size_t m_instance_hash_int; + public: bool OnInit() override; bool initialized() const { return m_initialized; } +#if ENABLE_GCODE_VIEWER + explicit GUI_App(EAppMode mode = EAppMode::Editor); +#else GUI_App(); +#endif // ENABLE_GCODE_VIEWER ~GUI_App() override; +#if ENABLE_GCODE_VIEWER + EAppMode get_app_mode() const { return m_app_mode; } + bool is_editor() const { return m_app_mode == EAppMode::Editor; } + bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } +#endif // ENABLE_GCODE_VIEWER + static std::string get_gl_info(bool format_as_html, bool extensions); wxGLContext* init_glcontext(wxGLCanvas& canvas); bool init_opengl(); @@ -150,12 +181,17 @@ public: wxSize get_min_size() const; float toolbar_icon_scale(const bool is_limited = false) const; void set_auto_toolbar_icon_scale(float scale) const; + void check_printer_presets(); void recreate_GUI(const wxString& message); void system_info(); void keyboard_shortcuts(); void load_project(wxWindow *parent, wxString& input_file) const; void import_model(wxWindow *parent, wxArrayString& input_files) const; +#if ENABLE_GCODE_VIEWER + void load_gcode(wxWindow* parent, wxString& input_file) const; +#endif // ENABLE_GCODE_VIEWER + static bool catch_error(std::function cb, const std::string& err); void persist_window_geometry(wxTopLevelWindow *window, bool default_maximized = false); @@ -194,12 +230,15 @@ public: Plater* plater(); Model& model(); + AppConfig* app_config{ nullptr }; PresetBundle* preset_bundle{ nullptr }; PresetUpdater* preset_updater{ nullptr }; MainFrame* mainframe{ nullptr }; Plater* plater_{ nullptr }; + PresetUpdater* get_preset_updater() { return preset_updater; } + wxNotebook* tab_panel() const ; int extruders_cnt() const; int extruders_edited_cnt() const; @@ -227,6 +266,12 @@ public: void gcode_thumbnails_debug(); #endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + GLShaderProgram* get_shader(const std::string& shader_name) { return m_opengl_mgr.get_shader(shader_name); } + GLShaderProgram* get_current_shader() { return m_opengl_mgr.get_current_shader(); } + + bool is_gl_version_greater_or_equal_to(unsigned int major, unsigned int minor) const { return m_opengl_mgr.get_gl_info().is_version_greater_or_equal_to(major, minor); } + bool is_glsl_version_greater_or_equal_to(unsigned int major, unsigned int minor) const { return m_opengl_mgr.get_gl_info().is_glsl_version_greater_or_equal_to(major, minor); } + private: bool on_init_inner(); void init_app_config(); @@ -245,6 +290,6 @@ private: DECLARE_APP(GUI_App) } // GUI -} //Slic3r +} // Slic3r #endif // slic3r_GUI_App_hpp_ diff --git a/src/slic3r/GUI/GUI_ObjectLayers.cpp b/src/slic3r/GUI/GUI_ObjectLayers.cpp index b1a5512d4b..90a725fbfd 100644 --- a/src/slic3r/GUI/GUI_ObjectLayers.cpp +++ b/src/slic3r/GUI/GUI_ObjectLayers.cpp @@ -3,7 +3,7 @@ #include "OptionsGroup.hpp" #include "GUI_App.hpp" -#include "PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" #include "GLCanvas3D.hpp" #include "Plater.hpp" diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index c11ed66ab8..4973abca57 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1,4 +1,5 @@ #include "libslic3r/libslic3r.h" +#include "libslic3r/PresetBundle.hpp" #include "GUI_ObjectList.hpp" #include "GUI_ObjectManipulation.hpp" #include "GUI_ObjectLayers.hpp" @@ -7,7 +8,6 @@ #include "Plater.hpp" #include "OptionsGroup.hpp" -#include "PresetBundle.hpp" #include "Tab.hpp" #include "wxExtensions.hpp" #include "libslic3r/Model.hpp" @@ -88,12 +88,10 @@ ObjectList::ObjectList(wxWindow* parent) : { // Fill CATEGORY_ICON { - // Note: `this` isn't passed to create_scaled_bitmap() here because of bugs in the widget, - // see note in PresetBundle::load_compatible_bitmaps() - // ptFFF CATEGORY_ICON[L("Layers and Perimeters")] = create_scaled_bitmap("layers"); CATEGORY_ICON[L("Infill")] = create_scaled_bitmap("infill"); + CATEGORY_ICON[L("Ironing")] = create_scaled_bitmap("ironing"); CATEGORY_ICON[L("Support material")] = create_scaled_bitmap("support"); CATEGORY_ICON[L("Speed")] = create_scaled_bitmap("time"); CATEGORY_ICON[L("Extruders")] = create_scaled_bitmap("funnel"); @@ -279,7 +277,11 @@ void ObjectList::create_objects_ctrl() // column ItemName(Icon+Text) of the view control: // And Icon can be consisting of several bitmaps - AppendColumn(new wxDataViewColumn(_(L("Name")), new BitmapTextRenderer(this), + BitmapTextRenderer* bmp_text_renderer = new BitmapTextRenderer(); + bmp_text_renderer->set_can_create_editor_ctrl_function([this]() { + return m_objects_model->GetItemType(GetSelection()) & (itVolume | itObject); + }); + AppendColumn(new wxDataViewColumn(_L("Name"), bmp_text_renderer, colName, 20*em, wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE)); // column PrintableProperty (Icon) of the view control: @@ -287,11 +289,15 @@ void ObjectList::create_objects_ctrl() wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE); // column Extruder of the view control: - AppendColumn(new wxDataViewColumn(_(L("Extruder")), new BitmapChoiceRenderer(), + BitmapChoiceRenderer* bmp_choice_renderer = new BitmapChoiceRenderer(); + bmp_choice_renderer->set_can_create_editor_ctrl_function([this]() { + return m_objects_model->GetItemType(GetSelection()) & (itVolume | itLayer | itObject); + }); + AppendColumn(new wxDataViewColumn(_L("Extruder"), bmp_choice_renderer, colExtruder, 8*em, wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE)); // column ItemEditing of the view control: - AppendBitmapColumn(_(L("Editing")), colEditing, wxDATAVIEW_CELL_INERT, 3*em, + AppendBitmapColumn(_L("Editing"), colEditing, wxDATAVIEW_CELL_INERT, 3*em, wxALIGN_CENTER_HORIZONTAL, wxDATAVIEW_COL_RESIZABLE); // For some reason under OSX on 4K(5K) monitors in wxDataViewColumn constructor doesn't set width of column. @@ -350,7 +356,7 @@ void ObjectList::get_selection_indexes(std::vector& obj_idxs, std::vectorGetItemType(item); - assert(type & itObject | itInstance | itInstanceRoot); + assert(type & (itObject | itInstance | itInstanceRoot)); obj_idxs.emplace_back(type & itObject ? m_objects_model->GetIdByItem(item) : m_objects_model->GetIdByItem(m_objects_model->GetTopParent(item))); @@ -644,6 +650,7 @@ void ObjectList::msw_rescale_icons() // ptFFF CATEGORY_ICON[L("Layers and Perimeters")] = create_scaled_bitmap("layers"); CATEGORY_ICON[L("Infill")] = create_scaled_bitmap("infill"); + CATEGORY_ICON[L("Ironing")] = create_scaled_bitmap("ironing"); CATEGORY_ICON[L("Support material")] = create_scaled_bitmap("support"); CATEGORY_ICON[L("Speed")] = create_scaled_bitmap("time"); CATEGORY_ICON[L("Extruders")] = create_scaled_bitmap("funnel"); @@ -2210,7 +2217,7 @@ void ObjectList::load_shape_object(const std::string& type_name) load_mesh_object(mesh, _(L("Shape")) + "-" + _(type_name)); } -void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name) +void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center) { // Add mesh to model as a new object Model& model = wxGetApp().plater()->model(); @@ -2220,6 +2227,7 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name #endif /* _DEBUG */ std::vector object_idxs; + auto bb = mesh.bounding_box(); ModelObject* new_object = model.add_object(); new_object->name = into_u8(name); new_object->add_instance(); // each object should have at list one instance @@ -2229,13 +2237,17 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name // set a default extruder value, since user can't add it manually new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); new_object->invalidate_bounding_box(); - - new_object->center_around_origin(); + new_object->translate(-bb.center()); + + if (center) { + const BoundingBoxf bed_shape = wxGetApp().plater()->bed_shape_bb(); + new_object->instances[0]->set_offset(Slic3r::to_3d(bed_shape.center().cast(), -new_object->origin_translation(2))); + } else { + new_object->instances[0]->set_offset(bb.center()); + } + new_object->ensure_on_bed(); - - const BoundingBoxf bed_shape = wxGetApp().plater()->bed_shape_bb(); - new_object->instances[0]->set_offset(Slic3r::to_3d(bed_shape.center().cast(), -new_object->origin_translation(2))); - + object_idxs.push_back(model.objects.size() - 1); #ifdef _DEBUG check_model_ids_validity(model); @@ -2362,8 +2374,9 @@ void ObjectList::del_layers_from_object(const int obj_idx) bool ObjectList::del_subobject_from_object(const int obj_idx, const int idx, const int type) { - if (obj_idx == 1000) - // Cannot delete a wipe tower. + assert(idx >= 0); + if (obj_idx == 1000 || idx<0) + // Cannot delete a wipe tower or volume with negative id return false; ModelObject* object = (*m_objects)[obj_idx]; diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index aa5264b07d..9f7dcd2475 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -294,7 +294,7 @@ public: void load_part(ModelObject* model_object, std::vector> &volumes_info, ModelVolumeType type); void load_generic_subobject(const std::string& type_name, const ModelVolumeType type); void load_shape_object(const std::string &type_name); - void load_mesh_object(const TriangleMesh &mesh, const wxString &name); + void load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center = true); void del_object(const int obj_idx); void del_subobject_item(wxDataViewItem& item); void del_settings_from_config(const wxDataViewItem& parent_item); diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 2c35fc316d..7243e8c73a 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -6,7 +6,7 @@ #include "OptionsGroup.hpp" #include "GUI_App.hpp" #include "wxExtensions.hpp" -#include "PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/Geometry.hpp" #include "Selection.hpp" diff --git a/src/slic3r/GUI/GUI_ObjectSettings.cpp b/src/slic3r/GUI/GUI_ObjectSettings.cpp index ef78123a4c..398cd51d45 100644 --- a/src/slic3r/GUI/GUI_ObjectSettings.cpp +++ b/src/slic3r/GUI/GUI_ObjectSettings.cpp @@ -4,8 +4,8 @@ #include "OptionsGroup.hpp" #include "GUI_App.hpp" #include "wxExtensions.hpp" -#include "PresetBundle.hpp" #include "Plater.hpp" +#include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" #include diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index c1e8b4c33c..8ea54c6f18 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -1,5 +1,7 @@ #include "libslic3r/libslic3r.h" +#if !ENABLE_GCODE_VIEWER #include "libslic3r/GCode/PreviewData.hpp" +#endif // !ENABLE_GCODE_VIEWER #include "GUI_Preview.hpp" #include "GUI_App.hpp" #include "GUI.hpp" @@ -8,9 +10,12 @@ #include "BackgroundSlicingProcess.hpp" #include "OpenGLManager.hpp" #include "GLCanvas3D.hpp" -#include "PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" #include "DoubleSlider.hpp" #include "Plater.hpp" +#if ENABLE_GCODE_VIEWER +#include "MainFrame.hpp" +#endif // ENABLE_GCODE_VIEWER #include #include @@ -169,28 +174,51 @@ void View3D::render() m_canvas->set_as_dirty(); } +#if ENABLE_GCODE_VIEWER +Preview::Preview( + wxWindow* parent, Model* model, DynamicPrintConfig* config, + BackgroundSlicingProcess* process, GCodeProcessor::Result* gcode_result, std::function schedule_background_process_func) +#else Preview::Preview( wxWindow* parent, Model* model, DynamicPrintConfig* config, BackgroundSlicingProcess* process, GCodePreviewData* gcode_preview_data, std::function schedule_background_process_func) +#endif // ENABLE_GCODE_VIEWER : m_canvas_widget(nullptr) , m_canvas(nullptr) +#if ENABLE_GCODE_VIEWER + , m_left_sizer(nullptr) + , m_layers_slider_sizer(nullptr) + , m_bottom_toolbar_panel(nullptr) +#else , m_double_slider_sizer(nullptr) +#endif // ENABLE_GCODE_VIEWER , m_label_view_type(nullptr) , m_choice_view_type(nullptr) - , m_label_show_features(nullptr) + , m_label_show(nullptr) , m_combochecklist_features(nullptr) +#if ENABLE_GCODE_VIEWER + , m_combochecklist_features_pos(0) + , m_combochecklist_options(nullptr) +#else , m_checkbox_travel(nullptr) , m_checkbox_retractions(nullptr) , m_checkbox_unretractions(nullptr) , m_checkbox_shells(nullptr) , m_checkbox_legend(nullptr) +#endif // ENABLE_GCODE_VIEWER , m_config(config) , m_process(process) +#if ENABLE_GCODE_VIEWER + , m_gcode_result(gcode_result) +#else , m_gcode_preview_data(gcode_preview_data) +#endif // ENABLE_GCODE_VIEWER , m_number_extruders(1) , m_preferred_color_mode("feature") , m_loaded(false) +#if !ENABLE_GCODE_VIEWER , m_enabled(false) +#endif // !ENABLE_GCODE_VIEWER , m_schedule_background_process(schedule_background_process_func) #ifdef __linux__ , m_volumes_cleanup_required(false) @@ -198,7 +226,9 @@ Preview::Preview( { if (init(parent, model)) { +#if !ENABLE_GCODE_VIEWER show_hide_ui_elements("none"); +#endif // !ENABLE_GCODE_VIEWER load_print(); } } @@ -208,6 +238,15 @@ bool Preview::init(wxWindow* parent, Model* model) if (!Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 /* disable wxTAB_TRAVERSAL */)) return false; +#if ENABLE_GCODE_VIEWER + // to match the background of the sliders +#ifdef _WIN32 + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#else + SetBackgroundColour(GetParent()->GetBackgroundColour()); +#endif // _WIN32 +#endif // ENABLE_GCODE_VIEWER + m_canvas_widget = OpenGLManager::create_wxglcanvas(*this); if (m_canvas_widget == nullptr) return false; @@ -222,51 +261,120 @@ bool Preview::init(wxWindow* parent, Model* model) m_canvas->enable_legend_texture(true); m_canvas->enable_dynamic_background(true); +#if ENABLE_GCODE_VIEWER + m_layers_slider_sizer = create_layers_slider_sizer(); + + m_bottom_toolbar_panel = new wxPanel(this); + m_label_view_type = new wxStaticText(m_bottom_toolbar_panel, wxID_ANY, _L("View")); + m_choice_view_type = new wxChoice(m_bottom_toolbar_panel, wxID_ANY); +#else m_double_slider_sizer = new wxBoxSizer(wxHORIZONTAL); create_double_slider(); - m_label_view_type = new wxStaticText(this, wxID_ANY, _(L("View"))); + m_label_view_type = new wxStaticText(this, wxID_ANY, _L("View")); m_choice_view_type = new wxChoice(this, wxID_ANY); - m_choice_view_type->Append(_(L("Feature type"))); - m_choice_view_type->Append(_(L("Height"))); - m_choice_view_type->Append(_(L("Width"))); - m_choice_view_type->Append(_(L("Speed"))); - m_choice_view_type->Append(_(L("Fan speed"))); - m_choice_view_type->Append(_(L("Volumetric flow rate"))); - m_choice_view_type->Append(_(L("Tool"))); - m_choice_view_type->Append(_(L("Color Print"))); +#endif // ENABLE_GCODE_VIEWER + m_choice_view_type->Append(_L("Feature type")); + m_choice_view_type->Append(_L("Height")); + m_choice_view_type->Append(_L("Width")); + m_choice_view_type->Append(_L("Speed")); + m_choice_view_type->Append(_L("Fan speed")); + m_choice_view_type->Append(_L("Volumetric flow rate")); + m_choice_view_type->Append(_L("Tool")); + m_choice_view_type->Append(_L("Color Print")); m_choice_view_type->SetSelection(0); - m_label_show_features = new wxStaticText(this, wxID_ANY, _(L("Show"))); +#if ENABLE_GCODE_VIEWER + m_label_show = new wxStaticText(m_bottom_toolbar_panel, wxID_ANY, _L("Show")); +#else + m_label_show = new wxStaticText(this, wxID_ANY, _L("Show")); +#endif // ENABLE_GCODE_VIEWER m_combochecklist_features = new wxComboCtrl(); - m_combochecklist_features->Create(this, wxID_ANY, _(L("Feature types")), wxDefaultPosition, wxSize(15 * wxGetApp().em_unit(), -1), wxCB_READONLY); - std::string feature_text = GUI::into_u8(_(L("Feature types"))); +#if ENABLE_GCODE_VIEWER + m_combochecklist_features->Create(m_bottom_toolbar_panel, wxID_ANY, _L("Feature types"), wxDefaultPosition, wxDefaultSize, wxCB_READONLY); +#else + m_combochecklist_features->Create(this, wxID_ANY, _L("Feature types"), wxDefaultPosition, wxDefaultSize, wxCB_READONLY); +#endif // ENABLE_GCODE_VIEWER std::string feature_items = GUI::into_u8( - _(L("Perimeter")) + "|" + - _(L("External perimeter")) + "|" + - _(L("Overhang perimeter")) + "|" + - _(L("Internal infill")) + "|" + - _(L("Solid infill")) + "|" + - _(L("Top solid infill")) + "|" + - _(L("Bridge infill")) + "|" + - _(L("Gap fill")) + "|" + - _(L("Skirt")) + "|" + - _(L("Support material")) + "|" + - _(L("Support material interface")) + "|" + - _(L("Wipe tower")) + "|" + - _(L("Custom")) +#if ENABLE_GCODE_VIEWER + _L("Unknown") + "|1|" + +#endif // ENABLE_GCODE_VIEWER + _L("Perimeter") + "|1|" + + _L("External perimeter") + "|1|" + + _L("Overhang perimeter") + "|1|" + + _L("Internal infill") + "|1|" + + _L("Solid infill") + "|1|" + + _L("Top solid infill") + "|1|" + + _L("Ironing") + "|1|" + + _L("Bridge infill") + "|1|" + + _L("Gap fill") + "|1|" + + _L("Skirt") + "|1|" + + _L("Support material") + "|1|" + + _L("Support material interface") + "|1|" + + _L("Wipe tower") + "|1|" + + _L("Custom") + "|1" ); - Slic3r::GUI::create_combochecklist(m_combochecklist_features, feature_text, feature_items, true); + Slic3r::GUI::create_combochecklist(m_combochecklist_features, GUI::into_u8(_L("Feature types")), feature_items); +#if ENABLE_GCODE_VIEWER + m_combochecklist_options = new wxComboCtrl(); + m_combochecklist_options->Create(m_bottom_toolbar_panel, wxID_ANY, _L("Options"), wxDefaultPosition, wxDefaultSize, wxCB_READONLY); + std::string options_items = GUI::into_u8( + get_option_type_string(OptionType::Travel) + "|0|" + + get_option_type_string(OptionType::Retractions) + "|0|" + + get_option_type_string(OptionType::Unretractions) + "|0|" + + get_option_type_string(OptionType::ToolChanges) + "|0|" + + get_option_type_string(OptionType::ColorChanges) + "|0|" + + get_option_type_string(OptionType::PausePrints) + "|0|" + + get_option_type_string(OptionType::CustomGCodes) + "|0|" + + get_option_type_string(OptionType::Shells) + "|0|" + + get_option_type_string(OptionType::ToolMarker) + "|0|" + + get_option_type_string(OptionType::Legend) + "|1" +); + Slic3r::GUI::create_combochecklist(m_combochecklist_options, GUI::into_u8(_L("Options")), options_items); +#else m_checkbox_travel = new wxCheckBox(this, wxID_ANY, _(L("Travel"))); m_checkbox_retractions = new wxCheckBox(this, wxID_ANY, _(L("Retractions"))); m_checkbox_unretractions = new wxCheckBox(this, wxID_ANY, _(L("Unretractions"))); m_checkbox_shells = new wxCheckBox(this, wxID_ANY, _(L("Shells"))); m_checkbox_legend = new wxCheckBox(this, wxID_ANY, _(L("Legend"))); m_checkbox_legend->SetValue(true); +#endif // ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER + m_left_sizer = new wxBoxSizer(wxVERTICAL); + m_left_sizer->Add(m_canvas_widget, 1, wxALL | wxEXPAND, 0); + + wxBoxSizer* right_sizer = new wxBoxSizer(wxVERTICAL); + right_sizer->Add(m_layers_slider_sizer, 1, wxEXPAND, 0); + + m_moves_slider = new DoubleSlider::Control(m_bottom_toolbar_panel, wxID_ANY, 0, 0, 0, 100, wxDefaultPosition, wxSize(-1, 3 * GetTextExtent("m").y), wxSL_HORIZONTAL); + m_moves_slider->SetDrawMode(DoubleSlider::dmSequentialGCodeView); + + wxBoxSizer* bottom_toolbar_sizer = new wxBoxSizer(wxHORIZONTAL); + bottom_toolbar_sizer->AddSpacer(5); + bottom_toolbar_sizer->Add(m_label_view_type, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + bottom_toolbar_sizer->Add(m_choice_view_type, 0, wxALIGN_CENTER_VERTICAL, 0); + bottom_toolbar_sizer->AddSpacer(5); + bottom_toolbar_sizer->Add(m_label_show, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 5); + bottom_toolbar_sizer->Add(m_combochecklist_options, 0, wxALIGN_CENTER_VERTICAL, 0); + // change the following number if editing the layout of the bottom toolbar sizer. It is used into update_bottom_toolbar() + m_combochecklist_features_pos = 6; + bottom_toolbar_sizer->Add(m_combochecklist_features, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5); + bottom_toolbar_sizer->Hide(m_combochecklist_features); + bottom_toolbar_sizer->AddSpacer(5); + bottom_toolbar_sizer->Add(m_moves_slider, 1, wxALL | wxEXPAND, 0); + m_bottom_toolbar_panel->SetSizer(bottom_toolbar_sizer); + + m_left_sizer->Add(m_bottom_toolbar_panel, 0, wxALL | wxEXPAND, 0); + m_left_sizer->Hide(m_bottom_toolbar_panel); + + wxBoxSizer* main_sizer = new wxBoxSizer(wxHORIZONTAL); + main_sizer->Add(m_left_sizer, 1, wxALL | wxEXPAND, 0); + main_sizer->Add(right_sizer, 0, wxALL | wxEXPAND, 0); +#else wxBoxSizer* top_sizer = new wxBoxSizer(wxHORIZONTAL); top_sizer->Add(m_canvas_widget, 1, wxALL | wxEXPAND, 0); top_sizer->Add(m_double_slider_sizer, 0, wxEXPAND, 0); @@ -275,7 +383,7 @@ bool Preview::init(wxWindow* parent, Model* model) bottom_sizer->Add(m_label_view_type, 0, wxALIGN_CENTER_VERTICAL, 5); bottom_sizer->Add(m_choice_view_type, 0, wxEXPAND | wxALL, 5); bottom_sizer->AddSpacer(10); - bottom_sizer->Add(m_label_show_features, 0, wxALIGN_CENTER_VERTICAL, 5); + bottom_sizer->Add(m_label_show, 0, wxALIGN_CENTER_VERTICAL, 5); bottom_sizer->Add(m_combochecklist_features, 0, wxEXPAND | wxALL, 5); bottom_sizer->AddSpacer(20); bottom_sizer->Add(m_checkbox_travel, 0, wxEXPAND | wxALL, 5); @@ -291,6 +399,7 @@ bool Preview::init(wxWindow* parent, Model* model) wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); main_sizer->Add(top_sizer, 1, wxALL | wxEXPAND, 0); main_sizer->Add(bottom_sizer, 0, wxALL | wxEXPAND, 0); +#endif // ENABLE_GCODE_VIEWER SetSizer(main_sizer); SetMinSize(GetSize()); @@ -298,6 +407,7 @@ bool Preview::init(wxWindow* parent, Model* model) bind_event_handlers(); +#if !ENABLE_GCODE_VIEWER // sets colors for gcode preview extrusion roles std::vector extrusion_roles_colors = { "Perimeter", "FFFF66", @@ -315,6 +425,7 @@ bool Preview::init(wxWindow* parent, Model* model) "Custom", "28CC94" }; m_gcode_preview_data->set_extrusion_paths_colors(extrusion_roles_colors); +#endif // !ENABLE_GCODE_VIEWER return true; } @@ -344,17 +455,24 @@ void Preview::set_number_extruders(unsigned int number_extruders) int tool_idx = m_choice_view_type->FindString(_(L("Tool"))); int type = (number_extruders > 1) ? tool_idx /* color by a tool number */ : 0; // color by a feature type m_choice_view_type->SetSelection(type); +#if ENABLE_GCODE_VIEWER + if ((0 <= type) && (type < static_cast(GCodeViewer::EViewType::Count))) + m_canvas->set_gcode_view_preview_type(static_cast(type)); +#else if ((0 <= type) && (type < (int)GCodePreviewData::Extrusion::Num_View_Types)) m_gcode_preview_data->extrusion.view_type = (GCodePreviewData::Extrusion::EViewType)type; +#endif // ENABLE_GCODE_VIEWER m_preferred_color_mode = (type == tool_idx) ? "tool_or_feature" : "feature"; } } +#if !ENABLE_GCODE_VIEWER void Preview::set_enabled(bool enabled) { m_enabled = enabled; } +#endif // !ENABLE_GCODE_VIEWER void Preview::bed_shape_changed() { @@ -381,6 +499,9 @@ void Preview::load_print(bool keep_z_range) else if (tech == ptSLA) load_print_as_sla(); +#if ENABLE_GCODE_VIEWER + update_bottom_toolbar(); +#endif // ENABLE_GCODE_VIEWER Layout(); } @@ -402,7 +523,9 @@ void Preview::reload_print(bool keep_volumes) !keep_volumes) { m_canvas->reset_volumes(); +#if !ENABLE_GCODE_VIEWER m_canvas->reset_legend_texture(); +#endif // !ENABLE_GCODE_VIEWER m_loaded = false; #ifdef __linux__ m_volumes_cleanup_required = false; @@ -425,7 +548,12 @@ void Preview::refresh_print() void Preview::msw_rescale() { // rescale slider +#if ENABLE_GCODE_VIEWER + if (m_layers_slider != nullptr) m_layers_slider->msw_rescale(); + if (m_moves_slider != nullptr) m_moves_slider->msw_rescale(); +#else if (m_slider) m_slider->msw_rescale(); +#endif // ENABLE_GCODE_VIEWER // rescale warning legend on the canvas get_canvas3d()->msw_rescale(); @@ -434,28 +562,47 @@ void Preview::msw_rescale() refresh_print(); } +#if ENABLE_GCODE_VIEWER +void Preview::move_layers_slider(wxKeyEvent& evt) +{ + if (m_layers_slider != nullptr) m_layers_slider->OnKeyDown(evt); +} +#else void Preview::move_double_slider(wxKeyEvent& evt) { - if (m_slider) + if (m_slider) m_slider->OnKeyDown(evt); } +#endif // ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER +void Preview::edit_layers_slider(wxKeyEvent& evt) +{ + if (m_layers_slider != nullptr) m_layers_slider->OnChar(evt); +} +#else void Preview::edit_double_slider(wxKeyEvent& evt) { - if (m_slider) + if (m_slider) m_slider->OnChar(evt); } +#endif // ENABLE_GCODE_VIEWER void Preview::bind_event_handlers() { this->Bind(wxEVT_SIZE, &Preview::on_size, this); m_choice_view_type->Bind(wxEVT_CHOICE, &Preview::on_choice_view_type, this); m_combochecklist_features->Bind(wxEVT_CHECKLISTBOX, &Preview::on_combochecklist_features, this); +#if ENABLE_GCODE_VIEWER + m_combochecklist_options->Bind(wxEVT_CHECKLISTBOX, &Preview::on_combochecklist_options, this); + m_moves_slider->Bind(wxEVT_SCROLL_CHANGED, &Preview::on_moves_slider_scroll_changed, this); +#else m_checkbox_travel->Bind(wxEVT_CHECKBOX, &Preview::on_checkbox_travel, this); m_checkbox_retractions->Bind(wxEVT_CHECKBOX, &Preview::on_checkbox_retractions, this); m_checkbox_unretractions->Bind(wxEVT_CHECKBOX, &Preview::on_checkbox_unretractions, this); m_checkbox_shells->Bind(wxEVT_CHECKBOX, &Preview::on_checkbox_shells, this); m_checkbox_legend->Bind(wxEVT_CHECKBOX, &Preview::on_checkbox_legend, this); +#endif // ENABLE_GCODE_VIEWER } void Preview::unbind_event_handlers() @@ -463,19 +610,25 @@ void Preview::unbind_event_handlers() this->Unbind(wxEVT_SIZE, &Preview::on_size, this); m_choice_view_type->Unbind(wxEVT_CHOICE, &Preview::on_choice_view_type, this); m_combochecklist_features->Unbind(wxEVT_CHECKLISTBOX, &Preview::on_combochecklist_features, this); +#if ENABLE_GCODE_VIEWER + m_combochecklist_options->Unbind(wxEVT_CHECKLISTBOX, &Preview::on_combochecklist_options, this); + m_moves_slider->Unbind(wxEVT_SCROLL_CHANGED, &Preview::on_moves_slider_scroll_changed, this); +#else m_checkbox_travel->Unbind(wxEVT_CHECKBOX, &Preview::on_checkbox_travel, this); m_checkbox_retractions->Unbind(wxEVT_CHECKBOX, &Preview::on_checkbox_retractions, this); m_checkbox_unretractions->Unbind(wxEVT_CHECKBOX, &Preview::on_checkbox_unretractions, this); m_checkbox_shells->Unbind(wxEVT_CHECKBOX, &Preview::on_checkbox_shells, this); m_checkbox_legend->Unbind(wxEVT_CHECKBOX, &Preview::on_checkbox_legend, this); +#endif // ENABLE_GCODE_VIEWER } +#if !ENABLE_GCODE_VIEWER void Preview::show_hide_ui_elements(const std::string& what) { bool enable = (what == "full"); - m_label_show_features->Enable(enable); + m_label_show->Enable(enable); m_combochecklist_features->Enable(enable); - m_checkbox_travel->Enable(enable); + m_checkbox_travel->Enable(enable); m_checkbox_retractions->Enable(enable); m_checkbox_unretractions->Enable(enable); m_checkbox_shells->Enable(enable); @@ -486,7 +639,7 @@ void Preview::show_hide_ui_elements(const std::string& what) m_choice_view_type->Enable(enable); bool visible = (what != "none"); - m_label_show_features->Show(visible); + m_label_show->Show(visible); m_combochecklist_features->Show(visible); m_checkbox_travel->Show(visible); m_checkbox_retractions->Show(visible); @@ -496,26 +649,37 @@ void Preview::show_hide_ui_elements(const std::string& what) m_label_view_type->Show(visible); m_choice_view_type->Show(visible); } +#endif // !ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER +void Preview::hide_layers_slider() +{ + m_layers_slider_sizer->Hide((size_t)0); + Layout(); +} +#else void Preview::reset_sliders(bool reset_all) { m_enabled = false; -// reset_double_slider(); + // reset_double_slider(); if (reset_all) m_double_slider_sizer->Hide((size_t)0); else m_double_slider_sizer->GetItem(size_t(0))->GetSizer()->Hide(1); } +#endif // ENABLE_GCODE_VIEWER +#if !ENABLE_GCODE_VIEWER void Preview::update_sliders(const std::vector& layers_z, bool keep_z_range) { m_enabled = true; - update_double_slider(layers_z, keep_z_range); + m_double_slider_sizer->Show((size_t)0); Layout(); } +#endif // !ENABLE_GCODE_VIEWER void Preview::on_size(wxSizeEvent& evt) { @@ -527,19 +691,56 @@ void Preview::on_choice_view_type(wxCommandEvent& evt) { m_preferred_color_mode = (m_choice_view_type->GetStringSelection() == L("Tool")) ? "tool" : "feature"; int selection = m_choice_view_type->GetCurrentSelection(); +#if ENABLE_GCODE_VIEWER + if (0 <= selection && selection < static_cast(GCodeViewer::EViewType::Count)) + m_canvas->set_toolpath_view_type(static_cast(selection)); + + refresh_print(); +#else if ((0 <= selection) && (selection < (int)GCodePreviewData::Extrusion::Num_View_Types)) m_gcode_preview_data->extrusion.view_type = (GCodePreviewData::Extrusion::EViewType)selection; reload_print(); +#endif // ENABLE_GCODE_VIEWER } void Preview::on_combochecklist_features(wxCommandEvent& evt) { - int flags = Slic3r::GUI::combochecklist_get_flags(m_combochecklist_features); - m_gcode_preview_data->extrusion.role_flags = (unsigned int)flags; + unsigned int flags = Slic3r::GUI::combochecklist_get_flags(m_combochecklist_features); +#if ENABLE_GCODE_VIEWER + m_canvas->set_toolpath_role_visibility_flags(flags); +#else + m_gcode_preview_data->extrusion.role_flags = flags; +#endif // ENABLE_GCODE_VIEWER refresh_print(); } +#if ENABLE_GCODE_VIEWER +void Preview::on_combochecklist_options(wxCommandEvent& evt) +{ + auto xored = [](unsigned int flags1, unsigned int flags2, unsigned int flag) { + auto is_flag_set = [](unsigned int flags, unsigned int flag) { + return (flags & (1 << flag)) != 0; + }; + return !is_flag_set(flags1, flag) != !is_flag_set(flags2, flag); + }; + + unsigned int curr_flags = m_canvas->get_gcode_options_visibility_flags(); + unsigned int new_flags = Slic3r::GUI::combochecklist_get_flags(m_combochecklist_options); + if (curr_flags == new_flags) + return; + + m_canvas->set_gcode_options_visibility_from_flags(new_flags); + + bool skip_refresh = xored(curr_flags, new_flags, static_cast(OptionType::Shells)) || + xored(curr_flags, new_flags, static_cast(OptionType::ToolMarker)); + + if (!skip_refresh) + refresh_print(); + else + m_canvas->set_as_dirty(); +} +#else void Preview::on_checkbox_travel(wxCommandEvent& evt) { m_gcode_preview_data->travel.is_visible = m_checkbox_travel->IsChecked(); @@ -571,8 +772,9 @@ void Preview::on_checkbox_legend(wxCommandEvent& evt) m_canvas->enable_legend_texture(m_checkbox_legend->IsChecked()); m_canvas_widget->Refresh(); } +#endif // ENABLE_GCODE_VIEWER -void Preview::update_view_type(bool slice_completed) +void Preview::update_view_type(bool keep_volumes) { const DynamicPrintConfig& config = wxGetApp().preset_bundle->project_config; @@ -586,34 +788,112 @@ void Preview::update_view_type(bool slice_completed) int type = m_choice_view_type->FindString(choice); if (m_choice_view_type->GetSelection() != type) { m_choice_view_type->SetSelection(type); +#if ENABLE_GCODE_VIEWER + if ((0 <= type) && (type < static_cast(GCodeViewer::EViewType::Count))) + m_canvas->set_gcode_view_preview_type(static_cast(type)); +#else if (0 <= type && type < (int)GCodePreviewData::Extrusion::Num_View_Types) m_gcode_preview_data->extrusion.view_type = (GCodePreviewData::Extrusion::EViewType)type; +#endif // ENABLE_GCODE_VIEWER m_preferred_color_mode = "feature"; } +#if ENABLE_GCODE_VIEWER + reload_print(keep_volumes); +#else reload_print(); +#endif // ENABLE_GCODE_VIEWER } +#if ENABLE_GCODE_VIEWER +void Preview::update_bottom_toolbar() +{ + combochecklist_set_flags(m_combochecklist_features, m_canvas->get_toolpath_role_visibility_flags()); + combochecklist_set_flags(m_combochecklist_options, m_canvas->get_gcode_options_visibility_flags()); + + // updates visibility of features combobox + if (m_bottom_toolbar_panel->IsShown()) + { + wxSizer* sizer = m_bottom_toolbar_panel->GetSizer(); + bool show = !m_canvas->is_gcode_legend_enabled() || m_canvas->get_gcode_view_type() != GCodeViewer::EViewType::FeatureType; + + if (show) + { + if (sizer->GetItem(m_combochecklist_features) == nullptr) + { + sizer->Insert(m_combochecklist_features_pos, m_combochecklist_features, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5); + sizer->Show(m_combochecklist_features); + sizer->Layout(); + Refresh(); + } + } + else + { + if (sizer->GetItem(m_combochecklist_features) != nullptr) + { + sizer->Hide(m_combochecklist_features); + sizer->Detach(m_combochecklist_features); + sizer->Layout(); + Refresh(); + } + } + } +} +#endif // ENABLE_GCODE_VIEWER + +#if ENABLE_GCODE_VIEWER +wxBoxSizer* Preview::create_layers_slider_sizer() +{ + wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); + m_layers_slider = new DoubleSlider::Control(this, wxID_ANY, 0, 0, 0, 100); + + m_layers_slider->SetDrawMode(wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA, + wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("complete_objects")); + + sizer->Add(m_layers_slider, 0, wxEXPAND, 0); + + // sizer, m_canvas_widget + m_canvas_widget->Bind(wxEVT_KEY_DOWN, &Preview::update_layers_slider_from_canvas, this); + m_canvas_widget->Bind(wxEVT_KEY_UP, [this](wxKeyEvent& event) { + if (event.GetKeyCode() == WXK_SHIFT) + m_layers_slider->UseDefaultColors(true); + event.Skip(); + }); + + m_layers_slider->Bind(wxEVT_SCROLL_CHANGED, &Preview::on_layers_slider_scroll_changed, this); + + Bind(DoubleSlider::wxCUSTOMEVT_TICKSCHANGED, [this](wxEvent&) { + Model& model = wxGetApp().plater()->model(); + model.custom_gcode_per_print_z = m_layers_slider->GetTicksValues(); + m_schedule_background_process(); + + update_view_type(false); + }); + + return sizer; +} +#else void Preview::create_double_slider() { m_slider = new DoubleSlider::Control(this, wxID_ANY, 0, 0, 0, 100); + bool sla_print_technology = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA; bool sequential_print = wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("complete_objects"); m_slider->SetDrawMode(sla_print_technology, sequential_print); m_double_slider_sizer->Add(m_slider, 0, wxEXPAND, 0); + // sizer, m_canvas_widget m_canvas_widget->Bind(wxEVT_KEY_DOWN, &Preview::update_double_slider_from_canvas, this); m_canvas_widget->Bind(wxEVT_KEY_UP, [this](wxKeyEvent& event) { if (event.GetKeyCode() == WXK_SHIFT) m_slider->UseDefaultColors(true); event.Skip(); - }); + }); m_slider->Bind(wxEVT_SCROLL_CHANGED, &Preview::on_sliders_scroll_changed, this); - Bind(DoubleSlider::wxCUSTOMEVT_TICKSCHANGED, [this](wxEvent&) { Model& model = wxGetApp().plater()->model(); model.custom_gcode_per_print_z = m_slider->GetTicksValues(); @@ -622,6 +902,7 @@ void Preview::create_double_slider() update_view_type(false); }); } +#endif // ENABLE_GCODE_VIEWER // Find an index of a value in a sorted vector, which is in . // Returns -1 if there is no such member. @@ -650,8 +931,12 @@ static int find_close_layer_idx(const std::vector& zs, double &z, double return -1; } +#if ENABLE_GCODE_VIEWER +void Preview::check_layers_slider_values(std::vector& ticks_from_model, const std::vector& layers_z) +#else void Preview::check_slider_values(std::vector& ticks_from_model, const std::vector& layers_z) +#endif // ENABLE_GCODE_VIEWER { // All ticks that would end up outside the slider range should be erased. // TODO: this should be placed into more appropriate part of code, @@ -668,12 +953,66 @@ void Preview::check_slider_values(std::vector& ticks_from_mod m_schedule_background_process(); } -void Preview::update_double_slider(const std::vector& layers_z, bool keep_z_range) +#if ENABLE_GCODE_VIEWER +void Preview::update_layers_slider(const std::vector& layers_z, bool keep_z_range) +{ + // Save the initial slider span. + double z_low = m_layers_slider->GetLowerValueD(); + double z_high = m_layers_slider->GetHigherValueD(); + bool was_empty = m_layers_slider->GetMaxValue() == 0; + + bool force_sliders_full_range = was_empty; + if (!keep_z_range) + { + bool span_changed = layers_z.empty() || std::abs(layers_z.back() - m_layers_slider->GetMaxValueD()) > DoubleSlider::epsilon()/*1e-6*/; + force_sliders_full_range |= span_changed; + } + bool snap_to_min = force_sliders_full_range || m_layers_slider->is_lower_at_min(); + bool snap_to_max = force_sliders_full_range || m_layers_slider->is_higher_at_max(); + + // Detect and set manipulation mode for double slider + update_layers_slider_mode(); + + CustomGCode::Info& ticks_info_from_model = wxGetApp().plater()->model().custom_gcode_per_print_z; + check_layers_slider_values(ticks_info_from_model.gcodes, layers_z); + + m_layers_slider->SetSliderValues(layers_z); + assert(m_layers_slider->GetMinValue() == 0); + m_layers_slider->SetMaxValue(layers_z.empty() ? 0 : layers_z.size() - 1); + + int idx_low = 0; + int idx_high = m_layers_slider->GetMaxValue(); + if (!layers_z.empty()) { + if (!snap_to_min) { + int idx_new = find_close_layer_idx(layers_z, z_low, DoubleSlider::epsilon()/*1e-6*/); + if (idx_new != -1) + idx_low = idx_new; + } + if (!snap_to_max) { + int idx_new = find_close_layer_idx(layers_z, z_high, DoubleSlider::epsilon()/*1e-6*/); + if (idx_new != -1) + idx_high = idx_new; + } + } + m_layers_slider->SetSelectionSpan(idx_low, idx_high); + m_layers_slider->SetTicksValues(ticks_info_from_model); + + bool sla_print_technology = wxGetApp().plater()->printer_technology() == ptSLA; + bool sequential_print = wxGetApp().preset_bundle->prints.get_edited_preset().config.opt_bool("complete_objects"); + m_layers_slider->SetDrawMode(sla_print_technology, sequential_print); + m_layers_slider->SetExtruderColors(wxGetApp().plater()->get_extruder_colors_from_plater_config()); + + m_layers_slider_sizer->Show((size_t)0); + Layout(); +} +#else +void Preview::update_double_slider(const std::vector & layers_z, bool keep_z_range) { // Save the initial slider span. double z_low = m_slider->GetLowerValueD(); double z_high = m_slider->GetHigherValueD(); bool was_empty = m_slider->GetMaxValue() == 0; + bool force_sliders_full_range = was_empty; if (!keep_z_range) { @@ -681,27 +1020,27 @@ void Preview::update_double_slider(const std::vector& layers_z, bool kee force_sliders_full_range |= span_changed; } bool snap_to_min = force_sliders_full_range || m_slider->is_lower_at_min(); - bool snap_to_max = force_sliders_full_range || m_slider->is_higher_at_max(); + bool snap_to_max = force_sliders_full_range || m_slider->is_higher_at_max(); // Detect and set manipulation mode for double slider update_double_slider_mode(); - CustomGCode::Info &ticks_info_from_model = wxGetApp().plater()->model().custom_gcode_per_print_z; + CustomGCode::Info& ticks_info_from_model = wxGetApp().plater()->model().custom_gcode_per_print_z; check_slider_values(ticks_info_from_model.gcodes, layers_z); m_slider->SetSliderValues(layers_z); assert(m_slider->GetMinValue() == 0); m_slider->SetMaxValue(layers_z.empty() ? 0 : layers_z.size() - 1); - int idx_low = 0; + int idx_low = 0; int idx_high = m_slider->GetMaxValue(); - if (! layers_z.empty()) { - if (! snap_to_min) { + if (!layers_z.empty()) { + if (!snap_to_min) { int idx_new = find_close_layer_idx(layers_z, z_low, DoubleSlider::epsilon()/*1e-6*/); if (idx_new != -1) idx_low = idx_new; } - if (! snap_to_max) { + if (!snap_to_max) { int idx_new = find_close_layer_idx(layers_z, z_high, DoubleSlider::epsilon()/*1e-6*/); if (idx_new != -1) idx_high = idx_new; @@ -717,8 +1056,13 @@ void Preview::update_double_slider(const std::vector& layers_z, bool kee m_slider->SetExtruderColors(wxGetApp().plater()->get_extruder_colors_from_plater_config()); } +#endif // ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER +void Preview::update_layers_slider_mode() +#else void Preview::update_double_slider_mode() +#endif // ENABLE_GCODE_VIEWER { // true -> single-extruder printer profile OR // multi-extruder printer profile , but whole model is printed by only one extruder @@ -767,16 +1111,70 @@ void Preview::update_double_slider_mode() } } +#if ENABLE_GCODE_VIEWER + m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder); +#else m_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder); +#endif // ENABLE_GCODE_VIEWER } +#if ENABLE_GCODE_VIEWER +void Preview::reset_layers_slider() +{ + m_layers_slider->SetHigherValue(0); + m_layers_slider->SetLowerValue(0); +} +#else void Preview::reset_double_slider() { m_slider->SetHigherValue(0); m_slider->SetLowerValue(0); } +#endif // ENABLE_GCODE_VIEWER -void Preview::update_double_slider_from_canvas(wxKeyEvent& event) +#if ENABLE_GCODE_VIEWER +void Preview::update_layers_slider_from_canvas(wxKeyEvent& event) +{ + if (event.HasModifiers()) { + event.Skip(); + return; + } + + const auto key = event.GetKeyCode(); + + if (key == 'U' || key == 'D') { + const int new_pos = key == 'U' ? m_layers_slider->GetHigherValue() + 1 : m_layers_slider->GetHigherValue() - 1; + m_layers_slider->SetHigherValue(new_pos); + if (event.ShiftDown() || m_layers_slider->is_one_layer()) m_layers_slider->SetLowerValue(m_layers_slider->GetHigherValue()); + } + else if (key == 'S') + m_layers_slider->ChangeOneLayerLock(); + else if (key == WXK_SHIFT) + m_layers_slider->UseDefaultColors(false); + else + event.Skip(); +} + +void Preview::update_moves_slider() +{ + const GCodeViewer::SequentialView& view = m_canvas->get_gcode_sequential_view(); + // this should not be needed, but it is here to try to prevent rambling crashes on Mac Asan + if (view.endpoints.last < view.endpoints.first) + return; + + std::vector values(view.endpoints.last - view.endpoints.first + 1); + unsigned int count = 0; + for (unsigned int i = view.endpoints.first; i <= view.endpoints.last; ++i) + { + values[count++] = static_cast(i + 1); + } + + m_moves_slider->SetSliderValues(values); + m_moves_slider->SetMaxValue(view.endpoints.last - view.endpoints.first); + m_moves_slider->SetSelectionSpan(view.current.first - view.endpoints.first, view.current.last - view.endpoints.first); +} +#else +void Preview::update_double_slider_from_canvas(wxKeyEvent & event) { if (event.HasModifiers()) { event.Skip(); @@ -802,9 +1200,16 @@ void Preview::update_double_slider_from_canvas(wxKeyEvent& event) else event.Skip(); } +#endif // ENABLE_GCODE_VIEWER void Preview::load_print_as_fff(bool keep_z_range) { +#if ENABLE_GCODE_VIEWER + if (wxGetApp().mainframe == nullptr) + // avoid proessing while mainframe is being constructed + return; +#endif // ENABLE_GCODE_VIEWER + if (m_loaded || m_process->current_printer_technology() != ptFFF) return; @@ -828,10 +1233,21 @@ void Preview::load_print_as_fff(bool keep_z_range) } } +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor() && !has_layers) +#else if (! has_layers) +#endif // ENABLE_GCODE_VIEWER { +#if ENABLE_GCODE_VIEWER + hide_layers_slider(); + m_left_sizer->Hide(m_bottom_toolbar_panel); + m_left_sizer->Layout(); + Refresh(); +#else reset_sliders(true); m_canvas->reset_legend_texture(); +#endif // ENABLE_GCODE_VIEWER m_canvas_widget->Refresh(); return; } @@ -844,26 +1260,46 @@ void Preview::load_print_as_fff(bool keep_z_range) int tool_idx = m_choice_view_type->FindString(_(L("Tool"))); int type = (number_extruders > 1) ? tool_idx /* color by a tool number */ : 0; // color by a feature type m_choice_view_type->SetSelection(type); +#if ENABLE_GCODE_VIEWER + if (0 <= type && type < static_cast(GCodeViewer::EViewType::Count)) + m_canvas->set_gcode_view_preview_type(static_cast(type)); +#else if ((0 <= type) && (type < (int)GCodePreviewData::Extrusion::Num_View_Types)) m_gcode_preview_data->extrusion.view_type = (GCodePreviewData::Extrusion::EViewType)type; +#endif // ENABLE_GCODE_VIEWER // If the->SetSelection changed the following line, revert it to "decide yourself". m_preferred_color_mode = "tool_or_feature"; } +#if ENABLE_GCODE_VIEWER + GCodeViewer::EViewType gcode_view_type = m_canvas->get_gcode_view_preview_type(); + bool gcode_preview_data_valid = !m_gcode_result->moves.empty(); +#else bool gcode_preview_data_valid = print->is_step_done(psGCodeExport) && ! m_gcode_preview_data->empty(); +#endif // ENABLE_GCODE_VIEWER // Collect colors per extruder. std::vector colors; std::vector color_print_values = {}; // set color print values, if it si selected "ColorPrint" view type +#if ENABLE_GCODE_VIEWER + if (gcode_view_type == GCodeViewer::EViewType::ColorPrint) +#else if (m_gcode_preview_data->extrusion.view_type == GCodePreviewData::Extrusion::ColorPrint) +#endif // ENABLE_GCODE_VIEWER { colors = wxGetApp().plater()->get_colors_for_color_print(); +#if !ENABLE_GCODE_VIEWER colors.push_back("#808080"); // gray color for pause print or custom G-code +#endif // !ENABLE_GCODE_VIEWER if (!gcode_preview_data_valid) color_print_values = wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes; } +#if ENABLE_GCODE_VIEWER + else if (gcode_preview_data_valid || gcode_view_type == GCodeViewer::EViewType::Tool) +#else else if (gcode_preview_data_valid || (m_gcode_preview_data->extrusion.view_type == GCodePreviewData::Extrusion::Tool) ) +#endif // ENABLE_GCODE_VIEWER { colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(); color_print_values.clear(); @@ -871,24 +1307,52 @@ void Preview::load_print_as_fff(bool keep_z_range) if (IsShown()) { +#if ENABLE_GCODE_VIEWER + std::vector zs; +#endif // ENABLE_GCODE_VIEWER + m_canvas->set_selected_extruder(0); if (gcode_preview_data_valid) { // Load the real G-code preview. +#if ENABLE_GCODE_VIEWER + m_canvas->load_gcode_preview(*m_gcode_result); + m_canvas->refresh_gcode_preview(*m_gcode_result, colors); + m_left_sizer->Show(m_bottom_toolbar_panel); + m_left_sizer->Layout(); + Refresh(); + zs = m_canvas->get_gcode_layers_zs(); +#else m_canvas->load_gcode_preview(*m_gcode_preview_data, colors); +#endif // ENABLE_GCODE_VIEWER m_loaded = true; } else { // Load the initial preview based on slices, not the final G-code. m_canvas->load_preview(colors, color_print_values); +#if ENABLE_GCODE_VIEWER + m_left_sizer->Hide(m_bottom_toolbar_panel); + m_left_sizer->Layout(); + Refresh(); + zs = m_canvas->get_volumes_print_zs(true); +#endif // ENABLE_GCODE_VIEWER } +#if !ENABLE_GCODE_VIEWER show_hide_ui_elements(gcode_preview_data_valid ? "full" : "simple"); - // recalculates zs and update sliders accordingly std::vector zs = m_canvas->get_current_print_zs(true); +#endif // !ENABLE_GCODE_VIEWER if (zs.empty()) { // all layers filtered out +#if ENABLE_GCODE_VIEWER + hide_layers_slider(); +#else reset_sliders(true); +#endif // ENABLE_GCODE_VIEWER m_canvas_widget->Refresh(); } else +#if ENABLE_GCODE_VIEWER + update_layers_slider(zs, keep_z_range); +#else update_sliders(zs, keep_z_range); +#endif // ENABLE_GCODE_VIEWER } } @@ -916,43 +1380,98 @@ void Preview::load_print_as_sla() n_layers = (unsigned int)zs.size(); if (n_layers == 0) { +#if ENABLE_GCODE_VIEWER + hide_layers_slider(); +#else reset_sliders(true); +#endif // ENABLE_GCODE_VIEWER m_canvas_widget->Refresh(); } if (IsShown()) { m_canvas->load_sla_preview(); +#if ENABLE_GCODE_VIEWER + m_left_sizer->Hide(m_bottom_toolbar_panel); + m_left_sizer->Hide(m_bottom_toolbar_panel); + m_left_sizer->Layout(); + Refresh(); +#else show_hide_ui_elements("none"); +#endif // ENABLE_GCODE_VIEWER if (n_layers > 0) +#if ENABLE_GCODE_VIEWER + update_layers_slider(zs); +#else update_sliders(zs); +#endif // ENABLE_GCODE_VIEWER m_loaded = true; } } +#if ENABLE_GCODE_VIEWER +void Preview::on_layers_slider_scroll_changed(wxCommandEvent& event) +#else void Preview::on_sliders_scroll_changed(wxCommandEvent& event) +#endif // ENABLE_GCODE_VIEWER { if (IsShown()) { PrinterTechnology tech = m_process->current_printer_technology(); if (tech == ptFFF) { +#if ENABLE_GCODE_VIEWER + m_canvas->set_toolpaths_z_range({ m_layers_slider->GetLowerValueD(), m_layers_slider->GetHigherValueD() }); + m_canvas->set_as_dirty(); +#else m_canvas->set_toolpaths_range(m_slider->GetLowerValueD() - 1e-6, m_slider->GetHigherValueD() + 1e-6); m_canvas->render(); m_canvas->set_use_clipping_planes(false); +#endif // ENABLE_GCODE_VIEWER } else if (tech == ptSLA) { +#if ENABLE_GCODE_VIEWER + m_canvas->set_clipping_plane(0, ClippingPlane(Vec3d::UnitZ(), -m_layers_slider->GetLowerValueD())); + m_canvas->set_clipping_plane(1, ClippingPlane(-Vec3d::UnitZ(), m_layers_slider->GetHigherValueD())); + m_canvas->set_use_clipping_planes(m_layers_slider->GetHigherValue() != 0); +#else m_canvas->set_clipping_plane(0, ClippingPlane(Vec3d::UnitZ(), -m_slider->GetLowerValueD())); m_canvas->set_clipping_plane(1, ClippingPlane(-Vec3d::UnitZ(), m_slider->GetHigherValueD())); m_canvas->set_use_clipping_planes(m_slider->GetHigherValue() != 0); +#endif // ENABLE_GCODE_VIEWER m_canvas->render(); } } } +#if ENABLE_GCODE_VIEWER +void Preview::on_moves_slider_scroll_changed(wxCommandEvent& event) +{ + m_canvas->update_gcode_sequential_view_current(static_cast(m_moves_slider->GetLowerValueD() - 1.0), static_cast(m_moves_slider->GetHigherValueD() - 1.0)); + m_canvas->render(); +} + +wxString Preview::get_option_type_string(OptionType type) const +{ + switch (type) + { + case OptionType::Travel: { return _L("Travel"); } + case OptionType::Retractions: { return _L("Retractions"); } + case OptionType::Unretractions: { return _L("Unretractions"); } + case OptionType::ToolChanges: { return _L("Tool changes"); } + case OptionType::ColorChanges: { return _L("Color changes"); } + case OptionType::PausePrints: { return _L("Pause prints"); } + case OptionType::CustomGCodes: { return _L("Custom GCodes"); } + case OptionType::Shells: { return _L("Shells"); } + case OptionType::ToolMarker: { return _L("Tool marker"); } + case OptionType::Legend: { return _L("Legend/Estimated printing time"); } + default: { return ""; } + } +} +#endif // ENABLE_GCODE_VIEWER } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GUI_Preview.hpp b/src/slic3r/GUI/GUI_Preview.hpp index bbf2774b89..c0a457d9cd 100644 --- a/src/slic3r/GUI/GUI_Preview.hpp +++ b/src/slic3r/GUI/GUI_Preview.hpp @@ -7,7 +7,9 @@ #include "libslic3r/CustomGCode.hpp" #include - +#if ENABLE_GCODE_VIEWER +#include "libslic3r/GCode/GCodeProcessor.hpp" +#endif // ENABLE_GCODE_VIEWER class wxNotebook; class wxGLCanvas; @@ -23,7 +25,9 @@ namespace Slic3r { class DynamicPrintConfig; class Print; class BackgroundSlicingProcess; +#if !ENABLE_GCODE_VIEWER class GCodePreviewData; +#endif // !ENABLE_GCODE_VIEWER class Model; namespace DoubleSlider { @@ -79,20 +83,35 @@ class Preview : public wxPanel { wxGLCanvas* m_canvas_widget; GLCanvas3D* m_canvas; +#if ENABLE_GCODE_VIEWER + wxBoxSizer* m_left_sizer; + wxBoxSizer* m_layers_slider_sizer; + wxPanel* m_bottom_toolbar_panel; +#else wxBoxSizer* m_double_slider_sizer; +#endif // ENABLE_GCODE_VIEWER wxStaticText* m_label_view_type; wxChoice* m_choice_view_type; - wxStaticText* m_label_show_features; + wxStaticText* m_label_show; wxComboCtrl* m_combochecklist_features; +#if ENABLE_GCODE_VIEWER + size_t m_combochecklist_features_pos; + wxComboCtrl* m_combochecklist_options; +#else wxCheckBox* m_checkbox_travel; wxCheckBox* m_checkbox_retractions; wxCheckBox* m_checkbox_unretractions; wxCheckBox* m_checkbox_shells; wxCheckBox* m_checkbox_legend; +#endif // ENABLE_GCODE_VIEWER DynamicPrintConfig* m_config; BackgroundSlicingProcess* m_process; +#if ENABLE_GCODE_VIEWER + GCodeProcessor::Result* m_gcode_result; +#else GCodePreviewData* m_gcode_preview_data; +#endif // ENABLE_GCODE_VIEWER #ifdef __linux__ // We are getting mysterious crashes on Linux in gtk due to OpenGL context activation GH #1874 #1955. @@ -107,13 +126,39 @@ class Preview : public wxPanel std::string m_preferred_color_mode; bool m_loaded; +#if !ENABLE_GCODE_VIEWER bool m_enabled; +#endif // !ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER + DoubleSlider::Control* m_layers_slider{ nullptr }; + DoubleSlider::Control* m_moves_slider{ nullptr }; +#else DoubleSlider::Control* m_slider {nullptr}; +#endif // ENABLE_GCODE_VIEWER public: - Preview(wxWindow* parent, Model* model, DynamicPrintConfig* config, +#if ENABLE_GCODE_VIEWER + enum class OptionType : unsigned int + { + Travel, + Retractions, + Unretractions, + ToolChanges, + ColorChanges, + PausePrints, + CustomGCodes, + Shells, + ToolMarker, + Legend + }; + +Preview(wxWindow* parent, Model* model, DynamicPrintConfig* config, BackgroundSlicingProcess* process, + GCodeProcessor::Result* gcode_result, std::function schedule_background_process = []() {}); +#else +Preview(wxWindow* parent, Model* model, DynamicPrintConfig* config, BackgroundSlicingProcess* process, GCodePreviewData* gcode_preview_data, std::function schedule_background_process = []() {}); +#endif // ENABLE_GCODE_VIEWER virtual ~Preview(); wxGLCanvas* get_wxglcanvas() { return m_canvas_widget; } @@ -122,7 +167,9 @@ public: void set_as_dirty(); void set_number_extruders(unsigned int number_extruders); +#if !ENABLE_GCODE_VIEWER void set_enabled(bool enabled); +#endif // !ENABLE_GCODE_VIEWER void bed_shape_changed(); void select_view(const std::string& direction); void set_drop_target(wxDropTarget* target); @@ -132,47 +179,82 @@ public: void refresh_print(); void msw_rescale(); +#if ENABLE_GCODE_VIEWER + void move_layers_slider(wxKeyEvent& evt); + void edit_layers_slider(wxKeyEvent& evt); +#else void move_double_slider(wxKeyEvent& evt); void edit_double_slider(wxKeyEvent& evt); +#endif // ENABLE_GCODE_VIEWER - void update_view_type(bool slice_completed); + void update_view_type(bool keep_volumes); bool is_loaded() const { return m_loaded; } +#if ENABLE_GCODE_VIEWER + void update_bottom_toolbar(); + void update_moves_slider(); + void hide_layers_slider(); +#endif // ENABLE_GCODE_VIEWER + private: bool init(wxWindow* parent, Model* model); void bind_event_handlers(); void unbind_event_handlers(); +#if !ENABLE_GCODE_VIEWER void show_hide_ui_elements(const std::string& what); void reset_sliders(bool reset_all); void update_sliders(const std::vector& layers_z, bool keep_z_range = false); +#endif // !ENABLE_GCODE_VIEWER void on_size(wxSizeEvent& evt); void on_choice_view_type(wxCommandEvent& evt); void on_combochecklist_features(wxCommandEvent& evt); +#if ENABLE_GCODE_VIEWER + void on_combochecklist_options(wxCommandEvent& evt); +#else void on_checkbox_travel(wxCommandEvent& evt); void on_checkbox_retractions(wxCommandEvent& evt); void on_checkbox_unretractions(wxCommandEvent& evt); void on_checkbox_shells(wxCommandEvent& evt); void on_checkbox_legend(wxCommandEvent& evt); +#endif // ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER + // Create/Update/Reset double slider on 3dPreview + wxBoxSizer* create_layers_slider_sizer(); + void check_layers_slider_values(std::vector& ticks_from_model, + const std::vector& layers_z); + void reset_layers_slider(); + void update_layers_slider(const std::vector& layers_z, bool keep_z_range = false); + void update_layers_slider_mode(); + // update vertical DoubleSlider after keyDown in canvas + void update_layers_slider_from_canvas(wxKeyEvent& event); +#else // Create/Update/Reset double slider on 3dPreview void create_double_slider(); - void check_slider_values(std::vector &ticks_from_model, - const std::vector &layers_z); + void check_slider_values(std::vector& ticks_from_model, + const std::vector& layers_z); void reset_double_slider(); void update_double_slider(const std::vector& layers_z, bool keep_z_range = false); void update_double_slider_mode(); // update DoubleSlider after keyDown in canvas void update_double_slider_from_canvas(wxKeyEvent& event); +#endif // ENABLE_GCODE_VIEWER void load_print_as_fff(bool keep_z_range = false); void load_print_as_sla(); +#if ENABLE_GCODE_VIEWER + void on_layers_slider_scroll_changed(wxCommandEvent& event); + void on_moves_slider_scroll_changed(wxCommandEvent& event); + wxString get_option_type_string(OptionType type) const; +#else void on_sliders_scroll_changed(wxCommandEvent& event); +#endif // ENABLE_GCODE_VIEWER }; } // namespace GUI diff --git a/src/slic3r/GUI/GUI_Utils.hpp b/src/slic3r/GUI/GUI_Utils.hpp index 2737b3edbf..1c88de5707 100644 --- a/src/slic3r/GUI/GUI_Utils.hpp +++ b/src/slic3r/GUI/GUI_Utils.hpp @@ -92,10 +92,13 @@ public: #ifndef __WXOSX__ // Don't call SetFont under OSX to avoid name cutting in ObjectList this->SetFont(m_normal_font); #endif - // initialize default width_unit according to the width of the one symbol ("m") of the currently active font of this window. -#if ENABLE_WX_3_1_3_DPI_CHANGED_EVENT + + // Linux specific issue : get_dpi_for_window(this) still doesn't responce to the Display's scale in new wxWidgets(3.1.3). + // So, calculate the m_em_unit value from the font size, as before +#if ENABLE_WX_3_1_3_DPI_CHANGED_EVENT && !defined(__WXGTK__) m_em_unit = std::max(10, 10.0f * m_scale_factor); #else + // initialize default width_unit according to the width of the one symbol ("m") of the currently active font of this window. m_em_unit = std::max(10, this->GetTextExtent("m").x - 1); #endif // ENABLE_WX_3_1_3_DPI_CHANGED_EVENT @@ -110,13 +113,8 @@ public: if (!m_can_rescale) return; -#if ENABLE_LAYOUT_NO_RESTART if (m_force_rescale || is_new_scale_factor()) rescale(wxRect()); -#else - if (is_new_scale_factor()) - rescale(wxRect()); -#endif // ENABLE_LAYOUT_NO_RESTART }); #else this->Bind(EVT_DPI_CHANGED_SLICER, [this](const DpiChangedEvent& evt) { @@ -127,13 +125,8 @@ public: if (!m_can_rescale) return; -#if ENABLE_LAYOUT_NO_RESTART if (m_force_rescale || is_new_scale_factor()) rescale(evt.rect); -#else - if (is_new_scale_factor()) - rescale(evt.rect); -#endif // ENABLE_LAYOUT_NO_RESTART }); #endif // wxVERSION_EQUAL_OR_GREATER_THAN @@ -175,9 +168,7 @@ public: int em_unit() const { return m_em_unit; } // int font_size() const { return m_font_size; } const wxFont& normal_font() const { return m_normal_font; } -#if ENABLE_LAYOUT_NO_RESTART void enable_force_rescale() { m_force_rescale = true; } -#endif // ENABLE_LAYOUT_NO_RESTART protected: virtual void on_dpi_changed(const wxRect &suggested_rect) = 0; @@ -191,9 +182,7 @@ private: wxFont m_normal_font; float m_prev_scale_factor; bool m_can_rescale{ true }; -#if ENABLE_LAYOUT_NO_RESTART bool m_force_rescale{ false }; -#endif // ENABLE_LAYOUT_NO_RESTART int m_new_font_point_size; @@ -233,17 +222,13 @@ private: { this->Freeze(); -#if ENABLE_LAYOUT_NO_RESTART && wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - if (m_force_rescale) { -#endif // ENABLE_LAYOUT_NO_RESTART - // rescale fonts of all controls - scale_controls_fonts(this, m_new_font_point_size); - // rescale current window font - scale_win_font(this, m_new_font_point_size); -#if ENABLE_LAYOUT_NO_RESTART && wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - m_force_rescale = false; - } -#endif // ENABLE_LAYOUT_NO_RESTART + m_force_rescale = false; +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) + // rescale fonts of all controls + scale_controls_fonts(this, m_new_font_point_size); + // rescale current window font + scale_win_font(this, m_new_font_point_size); +#endif // wxVERSION_EQUAL_OR_GREATER_THAN // set normal application font as a current window font m_normal_font = this->GetFont(); @@ -401,6 +386,15 @@ public: std::ostream& operator<<(std::ostream &os, const WindowMetrics& metrics); +#if ENABLE_GCODE_VIEWER +inline int hex_digit_to_int(const char c) +{ + return + (c >= '0' && c <= '9') ? int(c - '0') : + (c >= 'A' && c <= 'F') ? int(c - 'A') + 10 : + (c >= 'a' && c <= 'f') ? int(c - 'a') + 10 : -1; +} +#endif // ENABLE_GCODE_VIEWER }} diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index 3ab58c2585..44f0a69729 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -124,7 +124,6 @@ public: void set_state(EState state) { m_state = state; on_set_state(); } int get_shortcut_key() const { return m_shortcut_key; } - void set_shortcut_key(int key) { m_shortcut_key = key; } const std::string& get_icon_filename() const { return m_icon_filename; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index cd42857247..6b3456b60e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -1,41 +1,40 @@ -// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. #include "GLGizmoFdmSupports.hpp" + +#include "libslic3r/Model.hpp" + +//#include "slic3r/GUI/3DScene.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" -#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/Plater.hpp" + #include -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/PresetBundle.hpp" -#include "slic3r/GUI/Camera.hpp" -#include "slic3r/GUI/Plater.hpp" -#include "libslic3r/Model.hpp" - - namespace Slic3r { + namespace GUI { -static constexpr size_t MaxVertexBuffers = 50; -GLGizmoFdmSupports::GLGizmoFdmSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) - , m_quadric(nullptr) + +void GLGizmoFdmSupports::on_shutdown() { - m_clipping_plane.reset(new ClippingPlane()); - m_quadric = ::gluNewQuadric(); - if (m_quadric != nullptr) - // using GLU_FILL does not work when the instance's transformation - // contains mirroring (normals are reverted) - ::gluQuadricDrawStyle(m_quadric, GLU_FILL); + if (m_setting_angle) { + m_setting_angle = false; + m_parent.use_slope(false); + } } -GLGizmoFdmSupports::~GLGizmoFdmSupports() + + +std::string GLGizmoFdmSupports::on_get_name() const { - if (m_quadric != nullptr) - ::gluDeleteQuadric(m_quadric); + return (_(L("FDM Support Editing")) + " [L]").ToUTF8().data(); } + + bool GLGizmoFdmSupports::on_init() { m_shortcut_key = WXK_CONTROL_L; @@ -49,44 +48,12 @@ bool GLGizmoFdmSupports::on_init() m_desc["block"] = _L("Block supports"); m_desc["remove_caption"] = _L("Shift + Left mouse button") + ": "; m_desc["remove"] = _L("Remove selection"); - m_desc["remove_all"] = _L("Remove all"); + m_desc["remove_all"] = _L("Remove all selection"); return true; } -void GLGizmoFdmSupports::activate_internal_undo_redo_stack(bool activate) -{ - if (activate && ! m_internal_stack_active) { - Plater::TakeSnapshot(wxGetApp().plater(), _L("FDM gizmo turned on")); - wxGetApp().plater()->enter_gizmos_stack(); - m_internal_stack_active = true; - } - if (! activate && m_internal_stack_active) { - wxGetApp().plater()->leave_gizmos_stack(); - Plater::TakeSnapshot(wxGetApp().plater(), _L("FDM gizmo turned off")); - m_internal_stack_active = false; - } -} - -void GLGizmoFdmSupports::set_fdm_support_data(ModelObject* model_object, const Selection& selection) -{ - if (m_state != On) - return; - - const ModelObject* mo = m_c->selection_info() ? m_c->selection_info()->model_object() : nullptr; - - if (mo && selection.is_from_single_instance() - && (m_schedule_update || mo->id() != m_old_mo_id || mo->volumes.size() != m_old_volumes_size)) - { - update_from_model_object(); - m_old_mo_id = mo->id(); - m_old_volumes_size = mo->volumes.size(); - m_schedule_update = false; - } -} - - void GLGizmoFdmSupports::on_render() const { @@ -95,527 +62,15 @@ void GLGizmoFdmSupports::on_render() const glsafe(::glEnable(GL_BLEND)); glsafe(::glEnable(GL_DEPTH_TEST)); - render_triangles(selection); + if (! m_setting_angle) + render_triangles(selection); + m_c->object_clipper()->render_cut(); render_cursor_circle(); glsafe(::glDisable(GL_BLEND)); } -void GLGizmoFdmSupports::render_triangles(const Selection& selection) const -{ - if (m_setting_angle) - return; - - const ModelObject* mo = m_c->selection_info()->model_object(); - - glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); - ScopeGuard offset_fill_guard([]() { glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); } ); - glsafe(::glPolygonOffset(-1.0, 1.0)); - - // Take care of the clipping plane. The normal of the clipping plane is - // saved with opposite sign than we need to pass to OpenGL (FIXME) - bool clipping_plane_active = m_c->object_clipper()->get_position() != 0.; - if (clipping_plane_active) { - const ClippingPlane* clp = m_c->object_clipper()->get_clipping_plane(); - double clp_data[4]; - memcpy(clp_data, clp->get_data(), 4 * sizeof(double)); - for (int i=0; i<3; ++i) - clp_data[i] = -1. * clp_data[i]; - - glsafe(::glClipPlane(GL_CLIP_PLANE0, (GLdouble*)clp_data)); - glsafe(::glEnable(GL_CLIP_PLANE0)); - } - - int mesh_id = -1; - for (const ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - - ++mesh_id; - - const Transform3d trafo_matrix = - mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * - mv->get_matrix(); - - bool is_left_handed = trafo_matrix.matrix().determinant() < 0.; - if (is_left_handed) - glsafe(::glFrontFace(GL_CW)); - - glsafe(::glPushMatrix()); - glsafe(::glMultMatrixd(trafo_matrix.data())); - - // Now render both enforcers and blockers. - for (int i=0; i<2; ++i) { - glsafe(::glColor4f(i ? 1.f : 0.2f, 0.2f, i ? 0.2f : 1.0f, 0.5f)); - for (const GLIndexedVertexArray& iva : m_ivas[mesh_id][i]) { - if (iva.has_VBOs()) - iva.render(); - } - } - glsafe(::glPopMatrix()); - if (is_left_handed) - glsafe(::glFrontFace(GL_CCW)); - } - if (clipping_plane_active) - glsafe(::glDisable(GL_CLIP_PLANE0)); -} - - -void GLGizmoFdmSupports::render_cursor_circle() const -{ - const Camera& camera = wxGetApp().plater()->get_camera(); - float zoom = (float)camera.get_zoom(); - float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f; - - Size cnv_size = m_parent.get_canvas_size(); - float cnv_half_width = 0.5f * (float)cnv_size.get_width(); - float cnv_half_height = 0.5f * (float)cnv_size.get_height(); - if ((cnv_half_width == 0.0f) || (cnv_half_height == 0.0f)) - return; - Vec2d mouse_pos(m_parent.get_local_mouse_position()(0), m_parent.get_local_mouse_position()(1)); - Vec2d center(mouse_pos(0) - cnv_half_width, cnv_half_height - mouse_pos(1)); - center = center * inv_zoom; - - glsafe(::glLineWidth(1.5f)); - float color[3]; - color[0] = 0.f; - color[1] = 1.f; - color[2] = 0.3f; - glsafe(::glColor3fv(color)); - glsafe(::glDisable(GL_DEPTH_TEST)); - - glsafe(::glPushMatrix()); - glsafe(::glLoadIdentity()); - // ensure that the circle is renderered inside the frustrum - glsafe(::glTranslated(0.0, 0.0, -(camera.get_near_z() + 0.5))); - // ensure that the overlay fits the frustrum near z plane - double gui_scale = camera.get_gui_scale(); - glsafe(::glScaled(gui_scale, gui_scale, 1.0)); - - glsafe(::glPushAttrib(GL_ENABLE_BIT)); - glsafe(::glLineStipple(4, 0xAAAA)); - glsafe(::glEnable(GL_LINE_STIPPLE)); - - ::glBegin(GL_LINE_LOOP); - for (double angle=0; angle<2*M_PI; angle+=M_PI/20.) - ::glVertex2f(GLfloat(center.x()+m_cursor_radius*cos(angle)), GLfloat(center.y()+m_cursor_radius*sin(angle))); - glsafe(::glEnd()); - - glsafe(::glPopAttrib()); - glsafe(::glPopMatrix()); -} - - -void GLGizmoFdmSupports::update_model_object() const -{ - ModelObject* mo = m_c->selection_info()->model_object(); - int idx = -1; - for (ModelVolume* mv : mo->volumes) { - ++idx; - if (! mv->is_model_part()) - continue; - for (int i=0; im_supported_facets.set_facet(i, m_selected_facets[idx][i]); - } -} - - -void GLGizmoFdmSupports::update_from_model_object() -{ - wxBusyCursor wait; - - const ModelObject* mo = m_c->selection_info()->model_object(); - size_t num_of_volumes = 0; - for (const ModelVolume* mv : mo->volumes) - if (mv->is_model_part()) - ++num_of_volumes; - m_selected_facets.resize(num_of_volumes); - - m_ivas.clear(); - m_ivas.resize(num_of_volumes); - for (size_t i=0; ivolumes) { - if (! mv->is_model_part()) - continue; - - ++volume_id; - - // This mesh does not account for the possible Z up SLA offset. - const TriangleMesh* mesh = &mv->mesh(); - - m_selected_facets[volume_id].assign(mesh->its.indices.size(), FacetSupportType::NONE); - - // Load current state from ModelVolume. - for (FacetSupportType type : {FacetSupportType::ENFORCER, FacetSupportType::BLOCKER}) { - const std::vector& list = mv->m_supported_facets.get_facets(type); - for (int i : list) - m_selected_facets[volume_id][i] = type; - } - update_vertex_buffers(mesh, volume_id, FacetSupportType::ENFORCER); - update_vertex_buffers(mesh, volume_id, FacetSupportType::BLOCKER); - } -} - - - -bool GLGizmoFdmSupports::is_mesh_point_clipped(const Vec3d& point) const -{ - if (m_c->object_clipper()->get_position() == 0.) - return false; - - auto sel_info = m_c->selection_info(); - int active_inst = m_c->selection_info()->get_active_instance(); - const ModelInstance* mi = sel_info->model_object()->instances[active_inst]; - const Transform3d& trafo = mi->get_transformation().get_matrix(); - - Vec3d transformed_point = trafo * point; - transformed_point(2) += sel_info->get_sla_shift(); - return m_c->object_clipper()->get_clipping_plane()->is_point_clipped(transformed_point); -} - - -// Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event. -// The gizmo has an opportunity to react - if it does, it should return true so that the Canvas3D is -// aware that the event was reacted to and stops trying to make different sense of it. If the gizmo -// concludes that the event was not intended for it, it should return false. -bool GLGizmoFdmSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) -{ - if (action == SLAGizmoEventType::MouseWheelUp - || action == SLAGizmoEventType::MouseWheelDown) { - if (control_down) { - double pos = m_c->object_clipper()->get_position(); - pos = action == SLAGizmoEventType::MouseWheelDown - ? std::max(0., pos - 0.01) - : std::min(1., pos + 0.01); - m_c->object_clipper()->set_position(pos, true); - return true; - } - else if (alt_down) { - m_cursor_radius = action == SLAGizmoEventType::MouseWheelDown - ? std::max(m_cursor_radius - CursorRadiusStep, CursorRadiusMin) - : std::min(m_cursor_radius + CursorRadiusStep, CursorRadiusMax); - m_parent.set_as_dirty(); - return true; - } - } - - if (action == SLAGizmoEventType::ResetClippingPlane) { - m_c->object_clipper()->set_position(-1., false); - return true; - } - - if (action == SLAGizmoEventType::LeftDown - || action == SLAGizmoEventType::RightDown - || (action == SLAGizmoEventType::Dragging && m_button_down != Button::None)) { - - FacetSupportType new_state = FacetSupportType::NONE; - if (! shift_down) { - if (action == SLAGizmoEventType::Dragging) - new_state = m_button_down == Button::Left - ? FacetSupportType::ENFORCER - : FacetSupportType::BLOCKER; - else - new_state = action == SLAGizmoEventType::LeftDown - ? FacetSupportType::ENFORCER - : FacetSupportType::BLOCKER; - } - - const Camera& camera = wxGetApp().plater()->get_camera(); - const Selection& selection = m_parent.get_selection(); - const ModelObject* mo = m_c->selection_info()->model_object(); - const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; - const Transform3d& instance_trafo = mi->get_transformation().get_matrix(); - - std::vector>> hit_positions_and_facet_ids; - bool clipped_mesh_was_hit = false; - - Vec3f normal = Vec3f::Zero(); - Vec3f hit = Vec3f::Zero(); - size_t facet = 0; - Vec3f closest_hit = Vec3f::Zero(); - double closest_hit_squared_distance = std::numeric_limits::max(); - size_t closest_facet = 0; - int closest_hit_mesh_id = -1; - - // Transformations of individual meshes - std::vector trafo_matrices; - - int mesh_id = -1; - // Cast a ray on all meshes, pick the closest hit and save it for the respective mesh - for (const ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - - ++mesh_id; - - trafo_matrices.push_back(instance_trafo * mv->get_matrix()); - hit_positions_and_facet_ids.push_back(std::vector>()); - - if (m_c->raycaster()->raycasters()[mesh_id]->unproject_on_mesh( - mouse_position, - trafo_matrices[mesh_id], - camera, - hit, - normal, - m_clipping_plane.get(), - &facet)) - { - // In case this hit is clipped, skip it. - if (is_mesh_point_clipped(hit.cast())) { - clipped_mesh_was_hit = true; - continue; - } - - // Is this hit the closest to the camera so far? - double hit_squared_distance = (camera.get_position()-trafo_matrices[mesh_id]*hit.cast()).squaredNorm(); - if (hit_squared_distance < closest_hit_squared_distance) { - closest_hit_squared_distance = hit_squared_distance; - closest_facet = facet; - closest_hit_mesh_id = mesh_id; - closest_hit = hit; - } - } - } - - bool dragging_while_painting = (action == SLAGizmoEventType::Dragging && m_button_down != Button::None); - - // The mouse button click detection is enabled when there is a valid hit - // or when the user clicks the clipping plane. Missing the object entirely - // shall not capture the mouse. - if (closest_hit_mesh_id != -1 || clipped_mesh_was_hit) { - if (m_button_down == Button::None) - m_button_down = ((action == SLAGizmoEventType::LeftDown) ? Button::Left : Button::Right); - } - - if (closest_hit_mesh_id == -1) { - // In case we have no valid hit, we can return. The event will - // be stopped in following two cases: - // 1. clicking the clipping plane - // 2. dragging while painting (to prevent scene rotations and moving the object) - return clipped_mesh_was_hit - || dragging_while_painting; - } - - // Now propagate the hits - mesh_id = -1; - const TriangleMesh* mesh = nullptr; - for (const ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - ++mesh_id; - if (mesh_id == closest_hit_mesh_id) { - mesh = &mv->mesh(); - break; - } - } - - bool update_both = false; - - const Transform3d& trafo_matrix = trafo_matrices[mesh_id]; - - // Calculate how far can a point be from the line (in mesh coords). - // FIXME: The scaling of the mesh can be non-uniform. - const Vec3d sf = Geometry::Transformation(trafo_matrix).get_scaling_factor(); - const float avg_scaling = (sf(0) + sf(1) + sf(2))/3.; - const float limit = pow(m_cursor_radius/avg_scaling , 2.f); - - const std::pair& hit_and_facet = { closest_hit, closest_facet }; - - // Calculate direction from camera to the hit (in mesh coords): - Vec3f dir = ((trafo_matrix.inverse() * camera.get_position()).cast() - hit_and_facet.first).normalized(); - - // A lambda to calculate distance from the centerline: - auto squared_distance_from_line = [&hit_and_facet, &dir](const Vec3f& point) -> float { - Vec3f diff = hit_and_facet.first - point; - return (diff - diff.dot(dir) * dir).squaredNorm(); - }; - - // A lambda to determine whether this facet is potentionally visible (still can be obscured) - auto faces_camera = [&dir, &mesh](const size_t& facet) -> bool { - return (mesh->stl.facet_start[facet].normal.dot(dir) > 0.); - }; - // Now start with the facet the pointer points to and check all adjacent facets. - std::vector facets_to_select{hit_and_facet.second}; - std::vector visited(m_selected_facets[mesh_id].size(), false); // keep track of facets we already processed - size_t facet_idx = 0; // index into facets_to_select - while (facet_idx < facets_to_select.size()) { - size_t facet = facets_to_select[facet_idx]; - if (! visited[facet]) { - // check all three vertices and in case they're close enough, - // add neighboring facets to be proccessed later - for (size_t i=0; i<3; ++i) { - float dist = squared_distance_from_line( - mesh->its.vertices[mesh->its.indices[facet](i)]); - if (dist < limit) { - for (int n=0; n<3; ++n) { - if (faces_camera(mesh->stl.neighbors_start[facet].neighbor[n])) - facets_to_select.push_back(mesh->stl.neighbors_start[facet].neighbor[n]); - } - } - } - visited[facet] = true; - } - ++facet_idx; - } - - std::vector new_facets; - new_facets.reserve(facets_to_select.size()); - - // Now just select all facets that passed and remember which - // ones have really changed state. - for (size_t next_facet : facets_to_select) { - FacetSupportType& facet = m_selected_facets[mesh_id][next_facet]; - - if (facet != new_state) { - if (facet != FacetSupportType::NONE) { - // this triangle is currently in the other VBA. - // Both VBAs need to be refreshed. - update_both = true; - } - facet = new_state; - new_facets.push_back(next_facet); - } - } - - if (! new_facets.empty()) { - if (new_state != FacetSupportType::NONE) { - // append triangles into the respective VBA - update_vertex_buffers(mesh, mesh_id, new_state, &new_facets); - if (update_both) { - auto other = new_state == FacetSupportType::ENFORCER - ? FacetSupportType::BLOCKER - : FacetSupportType::ENFORCER; - update_vertex_buffers(mesh, mesh_id, other); // regenerate the other VBA - } - } - else { - update_vertex_buffers(mesh, mesh_id, FacetSupportType::ENFORCER); - update_vertex_buffers(mesh, mesh_id, FacetSupportType::BLOCKER); - } - } - - return true; - } - - if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::RightUp) - && m_button_down != Button::None) { - // Take snapshot and update ModelVolume data. - wxString action_name = shift_down - ? _L("Remove selection") - : (m_button_down == Button::Left - ? _L("Add supports") - : _L("Block supports")); - activate_internal_undo_redo_stack(true); - Plater::TakeSnapshot(wxGetApp().plater(), action_name); - update_model_object(); - - m_button_down = Button::None; - return true; - } - - return false; -} - - -void GLGizmoFdmSupports::update_vertex_buffers(const TriangleMesh* mesh, - int mesh_id, - FacetSupportType type, - const std::vector* new_facets) -{ - std::vector& ivas = m_ivas[mesh_id][type == FacetSupportType::ENFORCER ? 0 : 1]; - - // lambda to push facet into vertex buffer - auto push_facet = [this, &mesh, &mesh_id](size_t idx, GLIndexedVertexArray& iva) { - for (int i=0; i<3; ++i) - iva.push_geometry( - mesh->its.vertices[mesh->its.indices[idx](i)].cast(), - m_c->raycaster()->raycasters()[mesh_id]->get_triangle_normal(idx).cast() - ); - size_t num = iva.triangle_indices_size; - iva.push_triangle(num, num+1, num+2); - }; - - - if (ivas.size() == MaxVertexBuffers || ! new_facets) { - // If there are too many or they should be regenerated, make one large - // GLVertexBufferArray. - ivas.clear(); // destructors release geometry - ivas.push_back(GLIndexedVertexArray()); - - bool pushed = false; - for (size_t facet_idx=0; facet_idxempty()) - ivas.back().finalize_geometry(true); - else - ivas.pop_back(); - } - -} - - -void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool overwrite, bool block) -{ - float threshold = (M_PI/180.)*threshold_deg; - const Selection& selection = m_parent.get_selection(); - const ModelObject* mo = m_c->selection_info()->model_object(); - const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; - - int mesh_id = -1; - for (const ModelVolume* mv : mo->volumes) { - if (! mv->is_model_part()) - continue; - - ++mesh_id; - - const Transform3d trafo_matrix = mi->get_matrix(true) * mv->get_matrix(true); - Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); - Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); - - float dot_limit = limit.dot(down); - - // Now calculate dot product of vert_direction and facets' normals. - int idx = -1; - for (const stl_facet& facet : mv->mesh().stl.facet_start) { - ++idx; - if (facet.normal.dot(down) > dot_limit && (overwrite || m_selected_facets[mesh_id][idx] == FacetSupportType::NONE)) - m_selected_facets[mesh_id][idx] = block - ? FacetSupportType::BLOCKER - : FacetSupportType::ENFORCER; - } - update_vertex_buffers(&mv->mesh(), mesh_id, FacetSupportType::ENFORCER); - update_vertex_buffers(&mv->mesh(), mesh_id, FacetSupportType::BLOCKER); - } - - activate_internal_undo_redo_stack(true); - - Plater::TakeSnapshot(wxGetApp().plater(), block ? _L("Block supports by angle") - : _L("Add supports by angle")); - update_model_object(); - m_parent.set_as_dirty(); - m_setting_angle = false; -} void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_limit) @@ -650,10 +105,7 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l window_width = std::max(window_width, button_width); auto draw_text_with_caption = [this, &caption_max](const wxString& caption, const wxString& text) { - static const ImVec4 ORANGE(1.0f, 0.49f, 0.22f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_Text, ORANGE); - m_imgui->text(caption); - ImGui::PopStyleColor(); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, caption); ImGui::SameLine(caption_max); m_imgui->text(text); }; @@ -670,18 +122,18 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l ImGui::SameLine(); if (m_imgui->button(m_desc.at("remove_all"))) { + Plater::TakeSnapshot(wxGetApp().plater(), wxString(_L("Reset selection"))); ModelObject* mo = m_c->selection_info()->model_object(); int idx = -1; for (ModelVolume* mv : mo->volumes) { - ++idx; if (mv->is_model_part()) { - m_selected_facets[idx].assign(m_selected_facets[idx].size(), FacetSupportType::NONE); - mv->m_supported_facets.clear(); - update_vertex_buffers(&mv->mesh(), idx, FacetSupportType::ENFORCER); - update_vertex_buffers(&mv->mesh(), idx, FacetSupportType::BLOCKER); - m_parent.set_as_dirty(); + ++idx; + m_triangle_selectors[idx]->reset(); } } + + update_model_object(); + m_parent.set_as_dirty(); } const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; @@ -723,12 +175,6 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l } m_imgui->end(); - if (m_setting_angle) { - m_parent.show_slope(false); - m_parent.set_slope_range({90.f - m_angle_threshold_deg, 90.f - m_angle_threshold_deg}); - m_parent.use_slope(true); - m_parent.set_as_dirty(); - } } else { std::string name = "Autoset custom supports"; @@ -737,12 +183,11 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l ImGui::SameLine(); if (m_imgui->slider_float("", &m_angle_threshold_deg, 0.f, 90.f, "%.f")) m_parent.set_slope_range({90.f - m_angle_threshold_deg, 90.f - m_angle_threshold_deg}); - m_imgui->checkbox(wxString("Overwrite already selected facets"), m_overwrite_selected); if (m_imgui->button("Enforce")) - select_facets_by_angle(m_angle_threshold_deg, m_overwrite_selected, false); + select_facets_by_angle(m_angle_threshold_deg, false); ImGui::SameLine(); if (m_imgui->button("Block")) - select_facets_by_angle(m_angle_threshold_deg, m_overwrite_selected, true); + select_facets_by_angle(m_angle_threshold_deg, true); ImGui::SameLine(); if (m_imgui->button("Cancel")) m_setting_angle = false; @@ -754,105 +199,98 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l } } -bool GLGizmoFdmSupports::on_is_activable() const + + +void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { + float threshold = (M_PI/180.)*threshold_deg; const Selection& selection = m_parent.get_selection(); + const ModelObject* mo = m_c->selection_info()->model_object(); + const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; - if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptFFF - || !selection.is_single_full_instance()) - return false; + int mesh_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; - // Check that none of the selected volumes is outside. Only SLA auxiliaries (supports) are allowed outside. - const Selection::IndicesList& list = selection.get_volume_idxs(); - for (const auto& idx : list) - if (selection.get_volume(idx)->is_outside) - return false; + ++mesh_id; - return true; -} + const Transform3d trafo_matrix = mi->get_matrix(true) * mv->get_matrix(true); + Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); + Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); -bool GLGizmoFdmSupports::on_is_selectable() const -{ - return (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF ); -} + float dot_limit = limit.dot(down); -std::string GLGizmoFdmSupports::on_get_name() const -{ - return (_(L("FDM Support Editing")) + " [L]").ToUTF8().data(); -} - - -CommonGizmosDataID GLGizmoFdmSupports::on_get_requirements() const -{ - return CommonGizmosDataID( - int(CommonGizmosDataID::SelectionInfo) - | int(CommonGizmosDataID::InstancesHider) - | int(CommonGizmosDataID::Raycaster) - | int(CommonGizmosDataID::HollowedMesh) - | int(CommonGizmosDataID::ObjectClipper) - | int(CommonGizmosDataID::SupportsClipper)); -} - - -void GLGizmoFdmSupports::on_set_state() -{ - if (m_state == m_old_state) - return; - - if (m_state == On && m_old_state != On) { // the gizmo was just turned on - if (! m_parent.get_gizmos_manager().is_serializing()) { - wxGetApp().CallAfter([this]() { - activate_internal_undo_redo_stack(true); - }); + // Now calculate dot product of vert_direction and facets' normals. + int idx = -1; + for (const stl_facet& facet : mv->mesh().stl.facet_start) { + ++idx; + if (facet.normal.dot(down) > dot_limit) + m_triangle_selectors[mesh_id]->set_facet(idx, + block + ? EnforcerBlockerType::BLOCKER + : EnforcerBlockerType::ENFORCER); } } - if (m_state == Off && m_old_state != Off) { // the gizmo was just turned Off - // we are actually shutting down - if (m_setting_angle) { - m_setting_angle = false; - m_parent.use_slope(false); - } - activate_internal_undo_redo_stack(false); - m_old_mo_id = -1; - m_ivas.clear(); - m_selected_facets.clear(); + + activate_internal_undo_redo_stack(true); + + Plater::TakeSnapshot(wxGetApp().plater(), block ? _L("Block supports by angle") + : _L("Add supports by angle")); + update_model_object(); + m_parent.set_as_dirty(); + m_setting_angle = false; +} + + + +void GLGizmoFdmSupports::update_model_object() const +{ + bool updated = false; + ModelObject* mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + ++idx; + updated |= mv->m_supported_facets.set(*m_triangle_selectors[idx].get()); } - m_old_state = m_state; + + if (updated) + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); } -void GLGizmoFdmSupports::on_start_dragging() +void GLGizmoFdmSupports::update_from_model_object() { + wxBusyCursor wait; + const ModelObject* mo = m_c->selection_info()->model_object(); + m_triangle_selectors.clear(); + + int volume_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + + ++volume_id; + + // This mesh does not account for the possible Z up SLA offset. + const TriangleMesh* mesh = &mv->mesh(); + + m_triangle_selectors.emplace_back(std::make_unique(*mesh)); + m_triangle_selectors.back()->deserialize(mv->m_supported_facets.get_data()); + } } -void GLGizmoFdmSupports::on_stop_dragging() + +PainterGizmoType GLGizmoFdmSupports::get_painter_type() const { - + return PainterGizmoType::FDM_SUPPORTS; } - -void GLGizmoFdmSupports::on_load(cereal::BinaryInputArchive&) -{ - // We should update the gizmo from current ModelObject, but it is not - // possible at this point. That would require having updated selection and - // common gizmos data, which is not done at this point. Instead, save - // a flag to do the update in set_fdm_support_data, which will be called - // soon after. - m_schedule_update = true; -} - - - -void GLGizmoFdmSupports::on_save(cereal::BinaryOutputArchive&) const -{ - -} - - - } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index c4f5b153ec..0c39992f0b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -1,111 +1,47 @@ #ifndef slic3r_GLGizmoFdmSupports_hpp_ #define slic3r_GLGizmoFdmSupports_hpp_ -#include "GLGizmoBase.hpp" - -#include "slic3r/GUI/3DScene.hpp" - -#include "libslic3r/ObjectID.hpp" - -#include - +#include "GLGizmoPainterBase.hpp" namespace Slic3r { -enum class FacetSupportType : int8_t; - namespace GUI { -enum class SLAGizmoEventType : unsigned char; -class ClippingPlane; - -class GLGizmoFdmSupports : public GLGizmoBase +class GLGizmoFdmSupports : public GLGizmoPainterBase { -private: - ObjectID m_old_mo_id; - size_t m_old_volumes_size = 0; - - GLUquadricObj* m_quadric; - - float m_cursor_radius = 2.f; - static constexpr float CursorRadiusMin = 0.f; - static constexpr float CursorRadiusMax = 8.f; - static constexpr float CursorRadiusStep = 0.2f; - - // For each model-part volume, store a list of statuses of - // individual facets (one of the enum values above). - std::vector> m_selected_facets; - - // Vertex buffer arrays for each model-part volume. There is a vector of - // arrays so that adding triangles can be done without regenerating all - // other triangles. Enforcers and blockers are of course separate. - std::vector, 2>> m_ivas; - - void update_vertex_buffers(const TriangleMesh* mesh, - int mesh_id, - FacetSupportType type, // enforcers / blockers - const std::vector* new_facets = nullptr); // nullptr -> regenerate all - - public: - GLGizmoFdmSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); - ~GLGizmoFdmSupports() override; - void set_fdm_support_data(ModelObject* model_object, const Selection& selection); - bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); + GLGizmoFdmSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoPainterBase(parent, icon_filename, sprite_id) {} +protected: + void on_render_input_window(float x, float y, float bottom_limit) override; + std::string on_get_name() const override; private: bool on_init() override; void on_render() const override; void on_render_for_picking() const override {} - void render_triangles(const Selection& selection) const; - void render_cursor_circle() const; + void update_model_object() const override; + void update_from_model_object() override; - void update_model_object() const; - void update_from_model_object(); - void activate_internal_undo_redo_stack(bool activate); + void on_opening() override {} + void on_shutdown() override; + PainterGizmoType get_painter_type() const override; - void select_facets_by_angle(float threshold, bool overwrite, bool block); - bool m_overwrite_selected = false; + void select_facets_by_angle(float threshold, bool block); float m_angle_threshold_deg = 45.f; - - bool is_mesh_point_clipped(const Vec3d& point) const; - - float m_clipping_plane_distance = 0.f; - std::unique_ptr m_clipping_plane; bool m_setting_angle = false; - bool m_internal_stack_active = false; - bool m_schedule_update = false; // This map holds all translated description texts, so they can be easily referenced during layout calculations // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. std::map m_desc; - - enum class Button { - None, - Left, - Right - }; - - Button m_button_down = Button::None; - EState m_old_state = Off; // to be able to see that the gizmo has just been closed (see on_set_state) - -protected: - void on_set_state() override; - void on_start_dragging() override; - void on_stop_dragging() override; - void on_render_input_window(float x, float y, float bottom_limit) override; - std::string on_get_name() const override; - bool on_is_activable() const override; - bool on_is_selectable() const override; - void on_load(cereal::BinaryInputArchive& ar) override; - void on_save(cereal::BinaryOutputArchive& ar) const override; - CommonGizmosDataID on_get_requirements() const override; }; + } // namespace GUI } // namespace Slic3r + #endif // slic3r_GLGizmoFdmSupports_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index 658db64cab..04ada52536 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -9,7 +9,7 @@ #include "slic3r/GUI/GUI_ObjectSettings.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" @@ -130,7 +130,7 @@ void GLGizmoHollow::render_points(const Selection& selection, bool picking) cons const sla::DrainHole& drain_hole = drain_holes[i]; const bool& point_selected = m_selected[i]; - if (is_mesh_point_clipped((drain_hole.pos+HoleStickOutLength*drain_hole.normal).cast())) + if (is_mesh_point_clipped(drain_hole.pos.cast())) continue; // First decide about the color of the point. @@ -174,10 +174,10 @@ void GLGizmoHollow::render_points(const Selection& selection, bool picking) cons glsafe(::glRotated(aa.angle() * (180. / M_PI), aa.axis()(0), aa.axis()(1), aa.axis()(2))); glsafe(::glPushMatrix()); glsafe(::glTranslated(0., 0., -drain_hole.height)); - ::gluCylinder(m_quadric, drain_hole.radius, drain_hole.radius, drain_hole.height, 24, 1); - glsafe(::glTranslated(0., 0., drain_hole.height)); + ::gluCylinder(m_quadric, drain_hole.radius, drain_hole.radius, drain_hole.height + sla::HoleStickOutLength, 24, 1); + glsafe(::glTranslated(0., 0., drain_hole.height + sla::HoleStickOutLength)); ::gluDisk(m_quadric, 0.0, drain_hole.radius, 24, 1); - glsafe(::glTranslated(0., 0., -drain_hole.height)); + glsafe(::glTranslated(0., 0., -drain_hole.height - sla::HoleStickOutLength)); glsafe(::glRotatef(180.f, 1.f, 0.f, 0.f)); ::gluDisk(m_quadric, 0.0, drain_hole.radius, 24, 1); glsafe(::glPopMatrix()); @@ -307,13 +307,8 @@ bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_pos if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Add drainage hole"))); - Vec3d scaling = mo->instances[active_inst]->get_scaling_factor(); - Vec3f normal_transformed(pos_and_normal.second(0)/scaling(0), - pos_and_normal.second(1)/scaling(1), - pos_and_normal.second(2)/scaling(2)); - - mo->sla_drain_holes.emplace_back(pos_and_normal.first + HoleStickOutLength * pos_and_normal.second/* normal_transformed.normalized()*/, - -pos_and_normal.second, m_new_hole_radius, m_new_hole_height); + mo->sla_drain_holes.emplace_back(pos_and_normal.first, + -pos_and_normal.second, m_new_hole_radius, m_new_hole_height); m_selected.push_back(false); assert(m_selected.size() == mo->sla_drain_holes.size()); m_parent.set_as_dirty(); @@ -447,7 +442,7 @@ void GLGizmoHollow::on_update(const UpdateData& data) std::pair pos_and_normal; if (! unproject_on_mesh(data.mouse_pos.cast(), pos_and_normal)) return; - drain_holes[m_hover_id].pos = pos_and_normal.first + HoleStickOutLength * pos_and_normal.second; + drain_holes[m_hover_id].pos = pos_and_normal.first; drain_holes[m_hover_id].normal = -pos_and_normal.second; } } @@ -661,9 +656,7 @@ RENDER_AGAIN: m_imgui->text(m_desc["hole_depth"]); ImGui::SameLine(diameter_slider_left); - m_new_hole_height -= HoleStickOutLength; ImGui::SliderFloat(" ", &m_new_hole_height, 0.f, 10.f, "%.1f mm"); - m_new_hole_height += HoleStickOutLength; clicked |= ImGui::IsItemClicked(); edited |= ImGui::IsItemEdited(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp new file mode 100644 index 0000000000..ed98bf71d5 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -0,0 +1,620 @@ +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoPainterBase.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" + +#include + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Camera.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Model.hpp" + + + +namespace Slic3r { +namespace GUI { + + +GLGizmoPainterBase::GLGizmoPainterBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) +{ + m_clipping_plane.reset(new ClippingPlane()); +} + + + +void GLGizmoPainterBase::activate_internal_undo_redo_stack(bool activate) +{ + if (activate && ! m_internal_stack_active) { + wxString str = get_painter_type() == PainterGizmoType::FDM_SUPPORTS + ? _L("Supports gizmo turned on") + : _L("Seam gizmo turned on"); + Plater::TakeSnapshot(wxGetApp().plater(), str); + wxGetApp().plater()->enter_gizmos_stack(); + m_internal_stack_active = true; + } + if (! activate && m_internal_stack_active) { + wxString str = get_painter_type() == PainterGizmoType::SEAM + ? _L("Seam gizmo turned off") + : _L("Supports gizmo turned off"); + wxGetApp().plater()->leave_gizmos_stack(); + Plater::TakeSnapshot(wxGetApp().plater(), str); + m_internal_stack_active = false; + } +} + + + +void GLGizmoPainterBase::set_painter_gizmo_data(const Selection& selection) +{ + if (m_state != On) + return; + + const ModelObject* mo = m_c->selection_info() ? m_c->selection_info()->model_object() : nullptr; + + if (mo && selection.is_from_single_instance() + && (m_schedule_update || mo->id() != m_old_mo_id || mo->volumes.size() != m_old_volumes_size)) + { + update_from_model_object(); + m_old_mo_id = mo->id(); + m_old_volumes_size = mo->volumes.size(); + m_schedule_update = false; + } +} + + + +void GLGizmoPainterBase::render_triangles(const Selection& selection) const +{ + const ModelObject* mo = m_c->selection_info()->model_object(); + + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + ScopeGuard offset_fill_guard([]() { glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); } ); + glsafe(::glPolygonOffset(-1.0, 1.0)); + + // Take care of the clipping plane. The normal of the clipping plane is + // saved with opposite sign than we need to pass to OpenGL (FIXME) + bool clipping_plane_active = m_c->object_clipper()->get_position() != 0.; + if (clipping_plane_active) { + const ClippingPlane* clp = m_c->object_clipper()->get_clipping_plane(); + double clp_data[4]; + memcpy(clp_data, clp->get_data(), 4 * sizeof(double)); + for (int i=0; i<3; ++i) + clp_data[i] = -1. * clp_data[i]; + + glsafe(::glClipPlane(GL_CLIP_PLANE0, (GLdouble*)clp_data)); + glsafe(::glEnable(GL_CLIP_PLANE0)); + } + + int mesh_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + + ++mesh_id; + + const Transform3d trafo_matrix = + mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * + mv->get_matrix(); + + bool is_left_handed = trafo_matrix.matrix().determinant() < 0.; + if (is_left_handed) + glsafe(::glFrontFace(GL_CW)); + + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixd(trafo_matrix.data())); + + m_triangle_selectors[mesh_id]->render(m_imgui); + + glsafe(::glPopMatrix()); + if (is_left_handed) + glsafe(::glFrontFace(GL_CCW)); + } + if (clipping_plane_active) + glsafe(::glDisable(GL_CLIP_PLANE0)); +} + + +void GLGizmoPainterBase::render_cursor_circle() const +{ + const Camera& camera = wxGetApp().plater()->get_camera(); + float zoom = (float)camera.get_zoom(); + float inv_zoom = (zoom != 0.0f) ? 1.0f / zoom : 0.0f; + + Size cnv_size = m_parent.get_canvas_size(); + float cnv_half_width = 0.5f * (float)cnv_size.get_width(); + float cnv_half_height = 0.5f * (float)cnv_size.get_height(); + if ((cnv_half_width == 0.0f) || (cnv_half_height == 0.0f)) + return; + Vec2d mouse_pos(m_parent.get_local_mouse_position()(0), m_parent.get_local_mouse_position()(1)); + Vec2d center(mouse_pos(0) - cnv_half_width, cnv_half_height - mouse_pos(1)); + center = center * inv_zoom; + + glsafe(::glLineWidth(1.5f)); + float color[3]; + color[0] = 0.f; + color[1] = 1.f; + color[2] = 0.3f; + glsafe(::glColor3fv(color)); + glsafe(::glDisable(GL_DEPTH_TEST)); + + glsafe(::glPushMatrix()); + glsafe(::glLoadIdentity()); + // ensure that the circle is renderered inside the frustrum + glsafe(::glTranslated(0.0, 0.0, -(camera.get_near_z() + 0.5))); + // ensure that the overlay fits the frustrum near z plane + double gui_scale = camera.get_gui_scale(); + glsafe(::glScaled(gui_scale, gui_scale, 1.0)); + + glsafe(::glPushAttrib(GL_ENABLE_BIT)); + glsafe(::glLineStipple(4, 0xAAAA)); + glsafe(::glEnable(GL_LINE_STIPPLE)); + + ::glBegin(GL_LINE_LOOP); + for (double angle=0; angle<2*M_PI; angle+=M_PI/20.) + ::glVertex2f(GLfloat(center.x()+m_cursor_radius*cos(angle)), GLfloat(center.y()+m_cursor_radius*sin(angle))); + glsafe(::glEnd()); + + glsafe(::glPopAttrib()); + glsafe(::glPopMatrix()); +} + + + +bool GLGizmoPainterBase::is_mesh_point_clipped(const Vec3d& point) const +{ + if (m_c->object_clipper()->get_position() == 0.) + return false; + + auto sel_info = m_c->selection_info(); + int active_inst = m_c->selection_info()->get_active_instance(); + const ModelInstance* mi = sel_info->model_object()->instances[active_inst]; + const Transform3d& trafo = mi->get_transformation().get_matrix(); + + Vec3d transformed_point = trafo * point; + transformed_point(2) += sel_info->get_sla_shift(); + return m_c->object_clipper()->get_clipping_plane()->is_point_clipped(transformed_point); +} + + +// Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event. +// The gizmo has an opportunity to react - if it does, it should return true so that the Canvas3D is +// aware that the event was reacted to and stops trying to make different sense of it. If the gizmo +// concludes that the event was not intended for it, it should return false. +bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) +{ + if (action == SLAGizmoEventType::MouseWheelUp + || action == SLAGizmoEventType::MouseWheelDown) { + if (control_down) { + double pos = m_c->object_clipper()->get_position(); + pos = action == SLAGizmoEventType::MouseWheelDown + ? std::max(0., pos - 0.01) + : std::min(1., pos + 0.01); + m_c->object_clipper()->set_position(pos, true); + return true; + } + else if (alt_down) { + m_cursor_radius = action == SLAGizmoEventType::MouseWheelDown + ? std::max(m_cursor_radius - CursorRadiusStep, CursorRadiusMin) + : std::min(m_cursor_radius + CursorRadiusStep, CursorRadiusMax); + m_parent.set_as_dirty(); + return true; + } + } + + if (action == SLAGizmoEventType::ResetClippingPlane) { + m_c->object_clipper()->set_position(-1., false); + return true; + } + + if (action == SLAGizmoEventType::LeftDown + || action == SLAGizmoEventType::RightDown + || (action == SLAGizmoEventType::Dragging && m_button_down != Button::None)) { + + if (m_triangle_selectors.empty()) + return false; + + EnforcerBlockerType new_state = EnforcerBlockerType::NONE; + if (! shift_down) { + if (action == SLAGizmoEventType::Dragging) + new_state = m_button_down == Button::Left + ? EnforcerBlockerType::ENFORCER + : EnforcerBlockerType::BLOCKER; + else + new_state = action == SLAGizmoEventType::LeftDown + ? EnforcerBlockerType::ENFORCER + : EnforcerBlockerType::BLOCKER; + } + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Selection& selection = m_parent.get_selection(); + const ModelObject* mo = m_c->selection_info()->model_object(); + const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; + const Transform3d& instance_trafo = mi->get_transformation().get_matrix(); + + // List of mouse positions that will be used as seeds for painting. + std::vector mouse_positions{mouse_position}; + + // In case current mouse position is far from the last one, + // add several positions from between into the list, so there + // are no gaps in the painted region. + { + if (m_last_mouse_position == Vec2d::Zero()) + m_last_mouse_position = mouse_position; + // resolution describes minimal distance limit using circle radius + // as a unit (e.g., 2 would mean the patches will be touching). + double resolution = 0.7; + double diameter_px = resolution * m_cursor_radius * camera.get_zoom(); + int patches_in_between = int(((mouse_position - m_last_mouse_position).norm() - diameter_px) / diameter_px); + if (patches_in_between > 0) { + Vec2d diff = (mouse_position - m_last_mouse_position)/(patches_in_between+1); + for (int i=1; i<=patches_in_between; ++i) + mouse_positions.emplace_back(m_last_mouse_position + i*diff); + } + } + m_last_mouse_position = Vec2d::Zero(); // only actual hits should be saved + + // Now "click" into all the prepared points and spill paint around them. + for (const Vec2d& mp : mouse_positions) { + std::vector>> hit_positions_and_facet_ids; + bool clipped_mesh_was_hit = false; + + Vec3f normal = Vec3f::Zero(); + Vec3f hit = Vec3f::Zero(); + size_t facet = 0; + Vec3f closest_hit = Vec3f::Zero(); + double closest_hit_squared_distance = std::numeric_limits::max(); + size_t closest_facet = 0; + int closest_hit_mesh_id = -1; + + // Transformations of individual meshes + std::vector trafo_matrices; + + int mesh_id = -1; + // Cast a ray on all meshes, pick the closest hit and save it for the respective mesh + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + + ++mesh_id; + + trafo_matrices.push_back(instance_trafo * mv->get_matrix()); + hit_positions_and_facet_ids.push_back(std::vector>()); + + if (m_c->raycaster()->raycasters()[mesh_id]->unproject_on_mesh( + mp, + trafo_matrices[mesh_id], + camera, + hit, + normal, + m_clipping_plane.get(), + &facet)) + { + // In case this hit is clipped, skip it. + if (is_mesh_point_clipped(hit.cast())) { + clipped_mesh_was_hit = true; + continue; + } + + // Is this hit the closest to the camera so far? + double hit_squared_distance = (camera.get_position()-trafo_matrices[mesh_id]*hit.cast()).squaredNorm(); + if (hit_squared_distance < closest_hit_squared_distance) { + closest_hit_squared_distance = hit_squared_distance; + closest_facet = facet; + closest_hit_mesh_id = mesh_id; + closest_hit = hit; + } + } + } + + bool dragging_while_painting = (action == SLAGizmoEventType::Dragging && m_button_down != Button::None); + + // The mouse button click detection is enabled when there is a valid hit + // or when the user clicks the clipping plane. Missing the object entirely + // shall not capture the mouse. + if (closest_hit_mesh_id != -1 || clipped_mesh_was_hit) { + if (m_button_down == Button::None) + m_button_down = ((action == SLAGizmoEventType::LeftDown) ? Button::Left : Button::Right); + } + + if (closest_hit_mesh_id == -1) { + // In case we have no valid hit, we can return. The event will + // be stopped in following two cases: + // 1. clicking the clipping plane + // 2. dragging while painting (to prevent scene rotations and moving the object) + return clipped_mesh_was_hit + || dragging_while_painting; + } + + // Find respective mesh id. + mesh_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + ++mesh_id; + if (mesh_id == closest_hit_mesh_id) + break; + } + + const Transform3d& trafo_matrix = trafo_matrices[mesh_id]; + + // Calculate how far can a point be from the line (in mesh coords). + // FIXME: The scaling of the mesh can be non-uniform. + const Vec3d sf = Geometry::Transformation(trafo_matrix).get_scaling_factor(); + const float avg_scaling = (sf(0) + sf(1) + sf(2))/3.; + const float limit = m_cursor_radius/avg_scaling; + + // Calculate direction from camera to the hit (in mesh coords): + Vec3f camera_pos = (trafo_matrix.inverse() * camera.get_position()).cast(); + Vec3f dir = (closest_hit - camera_pos).normalized(); + + assert(mesh_id < int(m_triangle_selectors.size())); + m_triangle_selectors[mesh_id]->select_patch(closest_hit, closest_facet, camera_pos, + dir, limit, new_state); + m_last_mouse_position = mouse_position; + } + + return true; + } + + if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::RightUp) + && m_button_down != Button::None) { + // Take snapshot and update ModelVolume data. + wxString action_name; + if (get_painter_type() == PainterGizmoType::FDM_SUPPORTS) { + if (shift_down) + action_name = _L("Remove selection"); + else { + if (m_button_down == Button::Left) + action_name = _L("Add supports"); + else + action_name = _L("Block supports"); + } + } + if (get_painter_type() == PainterGizmoType::SEAM) { + if (shift_down) + action_name = _L("Remove selection"); + else { + if (m_button_down == Button::Left) + action_name = _L("Enforce seam"); + else + action_name = _L("Block seam"); + } + } + + activate_internal_undo_redo_stack(true); + Plater::TakeSnapshot(wxGetApp().plater(), action_name); + update_model_object(); + + m_button_down = Button::None; + m_last_mouse_position = Vec2d::Zero(); + return true; + } + + return false; +} + + + +bool GLGizmoPainterBase::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptFFF + || !selection.is_single_full_instance()) + return false; + + // Check that none of the selected volumes is outside. Only SLA auxiliaries (supports) are allowed outside. + const Selection::IndicesList& list = selection.get_volume_idxs(); + for (const auto& idx : list) + if (selection.get_volume(idx)->is_outside) + return false; + + return true; +} + +bool GLGizmoPainterBase::on_is_selectable() const +{ + return (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF + && wxGetApp().get_mode() != comSimple ); +} + + +CommonGizmosDataID GLGizmoPainterBase::on_get_requirements() const +{ + return CommonGizmosDataID( + int(CommonGizmosDataID::SelectionInfo) + | int(CommonGizmosDataID::InstancesHider) + | int(CommonGizmosDataID::Raycaster) + | int(CommonGizmosDataID::ObjectClipper)); +} + + +void GLGizmoPainterBase::on_set_state() +{ + if (m_state == m_old_state) + return; + + if (m_state == On && m_old_state != On) { // the gizmo was just turned on + on_opening(); + if (! m_parent.get_gizmos_manager().is_serializing()) { + wxGetApp().CallAfter([this]() { + activate_internal_undo_redo_stack(true); + }); + } + } + if (m_state == Off && m_old_state != Off) { // the gizmo was just turned Off + // we are actually shutting down + on_shutdown(); + activate_internal_undo_redo_stack(false); + m_old_mo_id = -1; + //m_iva.release_geometry(); + m_triangle_selectors.clear(); + } + m_old_state = m_state; +} + + + +void GLGizmoPainterBase::on_load(cereal::BinaryInputArchive&) +{ + // We should update the gizmo from current ModelObject, but it is not + // possible at this point. That would require having updated selection and + // common gizmos data, which is not done at this point. Instead, save + // a flag to do the update in set_painter_gizmo_data, which will be called + // soon after. + m_schedule_update = true; +} + + + +void TriangleSelectorGUI::render(ImGuiWrapper* imgui) +{ + int enf_cnt = 0; + int blc_cnt = 0; + + m_iva_enforcers.release_geometry(); + m_iva_blockers.release_geometry(); + + for (const Triangle& tr : m_triangles) { + if (! tr.valid || tr.is_split() || tr.get_state() == EnforcerBlockerType::NONE) + continue; + + GLIndexedVertexArray& va = tr.get_state() == EnforcerBlockerType::ENFORCER + ? m_iva_enforcers + : m_iva_blockers; + int& cnt = tr.get_state() == EnforcerBlockerType::ENFORCER + ? enf_cnt + : blc_cnt; + + for (int i=0; i<3; ++i) + va.push_geometry(double(m_vertices[tr.verts_idxs[i]].v[0]), + double(m_vertices[tr.verts_idxs[i]].v[1]), + double(m_vertices[tr.verts_idxs[i]].v[2]), + 0., 0., 1.); + va.push_triangle(cnt, + cnt+1, + cnt+2); + cnt += 3; + } + + m_iva_enforcers.finalize_geometry(true); + m_iva_blockers.finalize_geometry(true); + + if (m_iva_enforcers.has_VBOs()) { + ::glColor4f(0.f, 0.f, 1.f, 0.2f); + m_iva_enforcers.render(); + } + + + if (m_iva_blockers.has_VBOs()) { + ::glColor4f(1.f, 0.f, 0.f, 0.2f); + m_iva_blockers.render(); + } + + +#ifdef PRUSASLICER_TRIANGLE_SELECTOR_DEBUG + if (imgui) + render_debug(imgui); + else + assert(false); // If you want debug output, pass ptr to ImGuiWrapper. +#endif +} + + + +#ifdef PRUSASLICER_TRIANGLE_SELECTOR_DEBUG +void TriangleSelectorGUI::render_debug(ImGuiWrapper* imgui) +{ + imgui->begin(std::string("TriangleSelector dialog (DEV ONLY)"), + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + static float edge_limit = 1.f; + imgui->text("Edge limit (mm): "); + imgui->slider_float("", &edge_limit, 0.1f, 8.f); + set_edge_limit(edge_limit); + imgui->checkbox("Show split triangles: ", m_show_triangles); + imgui->checkbox("Show invalid triangles: ", m_show_invalid); + + int valid_triangles = m_triangles.size() - m_invalid_triangles; + imgui->text("Valid triangles: " + std::to_string(valid_triangles) + + "/" + std::to_string(m_triangles.size())); + imgui->text("Vertices: " + std::to_string(m_vertices.size())); + if (imgui->button("Force garbage collection")) + garbage_collect(); + + if (imgui->button("Serialize - deserialize")) { + auto map = serialize(); + deserialize(map); + } + + imgui->end(); + + if (! m_show_triangles) + return; + + enum vtype { + ORIGINAL = 0, + SPLIT, + INVALID + }; + + for (auto& va : m_varrays) + va.release_geometry(); + + std::array cnts; + + ::glScalef(1.01f, 1.01f, 1.01f); + + for (int tr_id=0; tr_idpush_geometry(double(m_vertices[tr.verts_idxs[i]].v[0]), + double(m_vertices[tr.verts_idxs[i]].v[1]), + double(m_vertices[tr.verts_idxs[i]].v[2]), + 0., 0., 1.); + va->push_triangle(*cnt, + *cnt+1, + *cnt+2); + *cnt += 3; + } + + ::glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); + for (vtype i : {ORIGINAL, SPLIT, INVALID}) { + GLIndexedVertexArray& va = m_varrays[i]; + va.finalize_geometry(true); + if (va.has_VBOs()) { + switch (i) { + case ORIGINAL : ::glColor3f(0.f, 0.f, 1.f); break; + case SPLIT : ::glColor3f(1.f, 0.f, 0.f); break; + case INVALID : ::glColor3f(1.f, 1.f, 0.f); break; + } + va.render(); + } + } + ::glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); +} +#endif + + + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp new file mode 100644 index 0000000000..b3e2b65f1f --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -0,0 +1,123 @@ +#ifndef slic3r_GLGizmoPainterBase_hpp_ +#define slic3r_GLGizmoPainterBase_hpp_ + +#include "GLGizmoBase.hpp" + +#include "slic3r/GUI/3DScene.hpp" + +#include "libslic3r/ObjectID.hpp" +#include "libslic3r/TriangleSelector.hpp" + +#include + + + + +namespace Slic3r { + +enum class EnforcerBlockerType : int8_t; + +namespace GUI { + +enum class SLAGizmoEventType : unsigned char; +class ClippingPlane; + +enum class PainterGizmoType { + FDM_SUPPORTS, + SEAM +}; + + +class TriangleSelectorGUI : public TriangleSelector { +public: + explicit TriangleSelectorGUI(const TriangleMesh& mesh) + : TriangleSelector(mesh) {} + + // Render current selection. Transformation matrices are supposed + // to be already set. + void render(ImGuiWrapper* imgui = nullptr); + +#ifdef PRUSASLICER_TRIANGLE_SELECTOR_DEBUG + void render_debug(ImGuiWrapper* imgui); + bool m_show_triangles{false}; + bool m_show_invalid{false}; +#endif + +private: + GLIndexedVertexArray m_iva_enforcers; + GLIndexedVertexArray m_iva_blockers; + std::array m_varrays; +}; + + +// Following class is a base class for a gizmo with ability to paint on mesh +// using circular blush (such as FDM supports gizmo and seam painting gizmo). +// The purpose is not to duplicate code related to mesh painting. +class GLGizmoPainterBase : public GLGizmoBase +{ +private: + ObjectID m_old_mo_id; + size_t m_old_volumes_size = 0; + +public: + GLGizmoPainterBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + ~GLGizmoPainterBase() override {} + void set_painter_gizmo_data(const Selection& selection); + bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); + +protected: + void render_triangles(const Selection& selection) const; + void render_cursor_circle() const; + virtual void update_model_object() const = 0; + virtual void update_from_model_object() = 0; + void activate_internal_undo_redo_stack(bool activate); + + float m_cursor_radius = 2.f; + static constexpr float CursorRadiusMin = 0.4f; // cannot be zero + static constexpr float CursorRadiusMax = 8.f; + static constexpr float CursorRadiusStep = 0.2f; + + // For each model-part volume, store status and division of the triangles. + std::vector> m_triangle_selectors; + + +private: + bool is_mesh_point_clipped(const Vec3d& point) const; + + float m_clipping_plane_distance = 0.f; + std::unique_ptr m_clipping_plane; + + bool m_internal_stack_active = false; + bool m_schedule_update = false; + Vec2d m_last_mouse_position = Vec2d::Zero(); + + enum class Button { + None, + Left, + Right + }; + + Button m_button_down = Button::None; + EState m_old_state = Off; // to be able to see that the gizmo has just been closed (see on_set_state) + +protected: + void on_set_state() override; + void on_start_dragging() override {} + void on_stop_dragging() override {} + + virtual void on_opening() = 0; + virtual void on_shutdown() = 0; + virtual PainterGizmoType get_painter_type() const = 0; + + bool on_is_activable() const override; + bool on_is_selectable() const override; + void on_load(cereal::BinaryInputArchive& ar) override; + void on_save(cereal::BinaryOutputArchive& ar) const override {} + CommonGizmosDataID on_get_requirements() const override; +}; + + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoPainterBase_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index f3e5656860..77366c6335 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -1,9 +1,15 @@ // Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. #include "GLGizmoRotate.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" #include +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "libslic3r/PresetBundle.hpp" + +#include "libslic3r/SLA/Rotfinder.hpp" namespace Slic3r { namespace GUI { @@ -194,6 +200,64 @@ void GLGizmoRotate::on_render_for_picking() const glsafe(::glPopMatrix()); } + + +GLGizmoRotate3D::RotoptimzeWindow::RotoptimzeWindow(ImGuiWrapper * imgui, + State & state, + const Alignment &alignment) + : m_imgui{imgui} +{ + imgui->begin(_L("Rotation"), ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoCollapse); + + // adjust window position to avoid overlap the view toolbar + float win_h = ImGui::GetWindowHeight(); + float x = alignment.x, y = alignment.y; + y = std::min(y, alignment.bottom_limit - win_h); + ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); + + static constexpr const char * button_txt = L("Optimize orientation"); + static constexpr const char * slider_txt = L("Accuracy"); + + float button_width = imgui->calc_text_size(_(button_txt)).x; + ImGui::PushItemWidth(100.); + //if (imgui->button(_(button_txt))) { + if (ImGui::ArrowButton(_(button_txt).c_str(), ImGuiDir_Down)){ + std::cout << "Blip" << std::endl; + } + + ImGui::SliderFloat(_(slider_txt).c_str(), &state.accuracy, 0.01f, 1.f, "%.1f"); + + static const std::vector options = { + _L("Least supports").ToStdString(), + _L("Suface quality").ToStdString() + }; + +// if (imgui->combo(_L("Choose method"), options, state.method) ) { +// std::cout << "method: " << state.method << std::endl; +// } + + +} + +GLGizmoRotate3D::RotoptimzeWindow::~RotoptimzeWindow() +{ + m_imgui->end(); +} + +void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limit) +{ + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + return; + +// TODO: + +// m_rotoptimizewin_state.mobj = ?; +// RotoptimzeWindow popup{m_imgui, m_rotoptimizewin_state, {x, y, bottom_limit}}; + +} + void GLGizmoRotate::render_circle() const { ::glBegin(GL_LINE_LOOP); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index 7365a20c36..c418c4b316 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -52,12 +52,12 @@ public: std::string get_tooltip() const override; protected: - virtual bool on_init(); - virtual std::string on_get_name() const { return ""; } - virtual void on_start_dragging(); - virtual void on_update(const UpdateData& data); - virtual void on_render() const; - virtual void on_render_for_picking() const; + bool on_init() override; + std::string on_get_name() const override { return ""; } + void on_start_dragging() override; + void on_update(const UpdateData& data) override; + void on_render() const override; + void on_render_for_picking() const override; private: void render_circle() const; @@ -94,46 +94,79 @@ public: } protected: - virtual bool on_init(); - virtual std::string on_get_name() const; - virtual void on_set_state() + bool on_init() override; + std::string on_get_name() const override; + void on_set_state() override { for (GLGizmoRotate& g : m_gizmos) g.set_state(m_state); } - virtual void on_set_hover_id() + void on_set_hover_id() override { for (int i = 0; i < 3; ++i) m_gizmos[i].set_hover_id((m_hover_id == i) ? 0 : -1); } - virtual void on_enable_grabber(unsigned int id) + void on_enable_grabber(unsigned int id) override { if (id < 3) m_gizmos[id].enable_grabber(0); } - virtual void on_disable_grabber(unsigned int id) + void on_disable_grabber(unsigned int id) override { if (id < 3) m_gizmos[id].disable_grabber(0); } - virtual bool on_is_activable() const; - virtual void on_start_dragging(); - virtual void on_stop_dragging(); - virtual void on_update(const UpdateData& data) + bool on_is_activable() const override; + void on_start_dragging() override; + void on_stop_dragging() override; + void on_update(const UpdateData& data) override { for (GLGizmoRotate& g : m_gizmos) { g.update(data); } } - virtual void on_render() const; - virtual void on_render_for_picking() const + void on_render() const override; + void on_render_for_picking() const override { for (const GLGizmoRotate& g : m_gizmos) { g.render_for_picking(); } } + + void on_render_input_window(float x, float y, float bottom_limit) override; + +private: + + class RotoptimzeWindow { + ImGuiWrapper *m_imgui = nullptr; + + public: + + struct State { + enum Metods { mMinSupportPoints, mLegacy }; + + float accuracy = 1.f; + int method = mMinSupportPoints; + ModelObject *mobj = nullptr; + }; + + struct Alignment { float x, y, bottom_limit; }; + + RotoptimzeWindow(ImGuiWrapper * imgui, + State & state, + const Alignment &bottom_limit); + + ~RotoptimzeWindow(); + + RotoptimzeWindow(const RotoptimzeWindow&) = delete; + RotoptimzeWindow(RotoptimzeWindow &&) = delete; + RotoptimzeWindow& operator=(const RotoptimzeWindow &) = delete; + RotoptimzeWindow& operator=(RotoptimzeWindow &&) = delete; + }; + + RotoptimzeWindow::State m_rotoptimizewin_state = {}; }; } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp new file mode 100644 index 0000000000..d0edfba131 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp @@ -0,0 +1,215 @@ +#include "GLGizmoSeam.hpp" + +#include "libslic3r/Model.hpp" + +//#include "slic3r/GUI/3DScene.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/Plater.hpp" + + +#include + + +namespace Slic3r { + +namespace GUI { + + + +bool GLGizmoSeam::on_init() +{ + m_shortcut_key = WXK_CONTROL_P; + + m_desc["clipping_of_view"] = _L("Clipping of view") + ": "; + m_desc["reset_direction"] = _L("Reset direction"); + m_desc["cursor_size"] = _L("Cursor size") + ": "; + m_desc["enforce_caption"] = _L("Left mouse button") + ": "; + m_desc["enforce"] = _L("Enforce seam"); + m_desc["block_caption"] = _L("Right mouse button") + " "; + m_desc["block"] = _L("Block seam"); + m_desc["remove_caption"] = _L("Shift + Left mouse button") + ": "; + m_desc["remove"] = _L("Remove selection"); + m_desc["remove_all"] = _L("Remove all selection"); + + return true; +} + + + +std::string GLGizmoSeam::on_get_name() const +{ + return (_(L("Seam Editing")) + " [P]").ToUTF8().data(); +} + + + +void GLGizmoSeam::on_render() const +{ + const Selection& selection = m_parent.get_selection(); + + glsafe(::glEnable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + render_triangles(selection); + + m_c->object_clipper()->render_cut(); + render_cursor_circle(); + + glsafe(::glDisable(GL_BLEND)); +} + + + +void GLGizmoSeam::on_render_input_window(float x, float y, float bottom_limit) +{ + if (! m_c->selection_info()->model_object()) + return; + + const float approx_height = m_imgui->scaled(18.0f); + y = std::min(y, bottom_limit - approx_height); + m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); + + + m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + + // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: + const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); + const float cursor_slider_left = m_imgui->calc_text_size(m_desc.at("cursor_size")).x + m_imgui->scaled(1.f); + const float button_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); + const float minimal_slider_width = m_imgui->scaled(4.f); + + float caption_max = 0.f; + float total_text_max = 0.; + for (const std::string& t : {"enforce", "block", "remove"}) { + caption_max = std::max(caption_max, m_imgui->calc_text_size(m_desc.at(t+"_caption")).x); + total_text_max = std::max(total_text_max, caption_max + m_imgui->calc_text_size(m_desc.at(t)).x); + } + caption_max += m_imgui->scaled(1.f); + total_text_max += m_imgui->scaled(1.f); + + float window_width = minimal_slider_width + std::max(cursor_slider_left, clipping_slider_left); + window_width = std::max(window_width, total_text_max); + window_width = std::max(window_width, button_width); + + auto draw_text_with_caption = [this, &caption_max](const wxString& caption, const wxString& text) { + static const ImVec4 ORANGE(1.0f, 0.49f, 0.22f, 1.0f); + m_imgui->text_colored(ORANGE, caption); + ImGui::SameLine(caption_max); + m_imgui->text(text); + }; + + for (const std::string& t : {"enforce", "block", "remove"}) + draw_text_with_caption(m_desc.at(t + "_caption"), m_desc.at(t)); + + m_imgui->text(""); + + if (m_imgui->button(m_desc.at("remove_all"))) { + Plater::TakeSnapshot(wxGetApp().plater(), wxString(_L("Reset selection"))); + ModelObject* mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume* mv : mo->volumes) { + if (mv->is_model_part()) { + ++idx; + m_triangle_selectors[idx]->reset(); + } + } + + update_model_object(); + m_parent.set_as_dirty(); + } + + const float max_tooltip_width = ImGui::GetFontSize() * 20.0f; + + m_imgui->text(m_desc.at("cursor_size")); + ImGui::SameLine(clipping_slider_left); + ImGui::PushItemWidth(window_width - clipping_slider_left); + ImGui::SliderFloat(" ", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f"); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(max_tooltip_width); + ImGui::TextUnformatted(_L("Alt + Mouse wheel").ToUTF8().data()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + ImGui::Separator(); + if (m_c->object_clipper()->get_position() == 0.f) + m_imgui->text(m_desc.at("clipping_of_view")); + else { + if (m_imgui->button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this](){ + m_c->object_clipper()->set_position(-1., false); + }); + } + } + + ImGui::SameLine(clipping_slider_left); + ImGui::PushItemWidth(window_width - clipping_slider_left); + float clp_dist = m_c->object_clipper()->get_position(); + if (ImGui::SliderFloat(" ", &clp_dist, 0.f, 1.f, "%.2f")) + m_c->object_clipper()->set_position(clp_dist, true); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(max_tooltip_width); + ImGui::TextUnformatted(_L("Ctrl + Mouse wheel").ToUTF8().data()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + m_imgui->end(); +} + + + +void GLGizmoSeam::update_model_object() const +{ + bool updated = false; + ModelObject* mo = m_c->selection_info()->model_object(); + int idx = -1; + for (ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + ++idx; + updated |= mv->m_seam_facets.set(*m_triangle_selectors[idx].get()); + } + + if (updated) + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); +} + + + +void GLGizmoSeam::update_from_model_object() +{ + wxBusyCursor wait; + + const ModelObject* mo = m_c->selection_info()->model_object(); + m_triangle_selectors.clear(); + + int volume_id = -1; + for (const ModelVolume* mv : mo->volumes) { + if (! mv->is_model_part()) + continue; + + ++volume_id; + + // This mesh does not account for the possible Z up SLA offset. + const TriangleMesh* mesh = &mv->mesh(); + + m_triangle_selectors.emplace_back(std::make_unique(*mesh)); + m_triangle_selectors.back()->deserialize(mv->m_seam_facets.get_data()); + } +} + + +PainterGizmoType GLGizmoSeam::get_painter_type() const +{ + return PainterGizmoType::SEAM; +} + + + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp new file mode 100644 index 0000000000..c3eb98c808 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp @@ -0,0 +1,43 @@ +#ifndef slic3r_GLGizmoSeam_hpp_ +#define slic3r_GLGizmoSeam_hpp_ + +#include "GLGizmoPainterBase.hpp" + +namespace Slic3r { + +namespace GUI { + +class GLGizmoSeam : public GLGizmoPainterBase +{ +public: + GLGizmoSeam(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoPainterBase(parent, icon_filename, sprite_id) {} + +protected: + void on_render_input_window(float x, float y, float bottom_limit) override; + std::string on_get_name() const override; + PainterGizmoType get_painter_type() const override; + +private: + bool on_init() override; + void on_render() const override; + void on_render_for_picking() const override {} + + void update_model_object() const override; + void update_from_model_object() override; + + void on_opening() override {} + void on_shutdown() override {} + + // This map holds all translated description texts, so they can be easily referenced during layout calculations + // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. + std::map m_desc; +}; + + + +} // namespace GUI +} // namespace Slic3r + + +#endif // slic3r_GLGizmoSeam_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index 908fe27b11..bc29da6d2b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -16,7 +16,7 @@ #include "slic3r/GUI/GUI_ObjectSettings.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" #include "libslic3r/SLAPrint.hpp" @@ -222,7 +222,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking) render_color[3] = 0.7f; glsafe(::glColor4fv(render_color)); for (const sla::DrainHole& drain_hole : m_c->selection_info()->model_object()->sla_drain_holes) { - if (is_mesh_point_clipped((drain_hole.pos+HoleStickOutLength*drain_hole.normal).cast())) + if (is_mesh_point_clipped(drain_hole.pos.cast())) continue; // Inverse matrix of the instance scaling is applied so that the mark does not scale with the object. @@ -241,10 +241,10 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking) glsafe(::glRotated(aa.angle() * (180. / M_PI), aa.axis()(0), aa.axis()(1), aa.axis()(2))); glsafe(::glPushMatrix()); glsafe(::glTranslated(0., 0., -drain_hole.height)); - ::gluCylinder(m_quadric, drain_hole.radius, drain_hole.radius, drain_hole.height, 24, 1); - glsafe(::glTranslated(0., 0., drain_hole.height)); + ::gluCylinder(m_quadric, drain_hole.radius, drain_hole.radius, drain_hole.height + sla::HoleStickOutLength, 24, 1); + glsafe(::glTranslated(0., 0., drain_hole.height + sla::HoleStickOutLength)); ::gluDisk(m_quadric, 0.0, drain_hole.radius, 24, 1); - glsafe(::glTranslated(0., 0., -drain_hole.height)); + glsafe(::glTranslated(0., 0., -drain_hole.height - sla::HoleStickOutLength)); glsafe(::glRotatef(180.f, 1.f, 0.f, 0.f)); ::gluDisk(m_quadric, 0.0, drain_hole.radius, 24, 1); glsafe(::glPopMatrix()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index 28f317c269..a25d9105fa 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -8,7 +8,7 @@ #include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" #include diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp index 31c473bac7..aedf782e89 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp @@ -15,8 +15,6 @@ namespace GUI { class GLCanvas3D; -static constexpr float HoleStickOutLength = 1.f; - enum class SLAGizmoEventType : unsigned char { LeftDown = 1, LeftUp, diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 511c68735c..1087c64d5a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -5,7 +5,6 @@ #include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#include "slic3r/GUI/PresetBundle.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/Utils/UndoRedo.hpp" @@ -17,8 +16,10 @@ #include "slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp" #include "slic3r/GUI/Gizmos/GLGizmoCut.hpp" #include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/PresetBundle.hpp" #include @@ -104,6 +105,7 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", 5)); m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", 6)); m_gizmos.emplace_back(new GLGizmoFdmSupports(m_parent, "sla_supports.svg", 7)); + m_gizmos.emplace_back(new GLGizmoSeam(m_parent, "seam.svg", 8)); m_common_gizmos_data.reset(new CommonGizmosDataPool(&m_parent)); @@ -144,8 +146,11 @@ void GLGizmosManager::refresh_on_off_state() if (m_serializing || m_current == Undefined || m_gizmos.empty()) return; - if (m_current != Undefined && ! m_gizmos[m_current]->is_activable()) + if (m_current != Undefined + && (! m_gizmos[m_current]->is_activable() || ! m_gizmos[m_current]->is_selectable())) { activate_gizmo(Undefined); + update_data(); + } } void GLGizmosManager::reset_all_states() @@ -204,9 +209,10 @@ void GLGizmosManager::update_data() enable_grabber(Scale, i, enable_scale_xyz); } - m_common_gizmos_data->update(get_current() - ? get_current()->get_requirements() - : CommonGizmosDataID(0)); + if (m_common_gizmos_data) + m_common_gizmos_data->update(get_current() + ? get_current()->get_requirements() + : CommonGizmosDataID(0)); if (selection.is_single_full_instance()) { @@ -217,7 +223,7 @@ void GLGizmosManager::update_data() ModelObject* model_object = selection.get_model()->objects[selection.get_object_idx()]; set_flattening_data(model_object); set_sla_support_data(model_object); - set_fdm_support_data(model_object); + set_painter_gizmo_data(); } else if (selection.is_single_volume() || selection.is_single_modifier()) { @@ -226,7 +232,7 @@ void GLGizmosManager::update_data() set_rotation(Vec3d::Zero()); set_flattening_data(nullptr); set_sla_support_data(nullptr); - set_fdm_support_data(nullptr); + set_painter_gizmo_data(); } else if (is_wipe_tower) { @@ -235,7 +241,7 @@ void GLGizmosManager::update_data() set_rotation(Vec3d(0., 0., (M_PI/180.) * dynamic_cast(config.option("wipe_tower_rotation_angle"))->value)); set_flattening_data(nullptr); set_sla_support_data(nullptr); - set_fdm_support_data(nullptr); + set_painter_gizmo_data(); } else { @@ -243,7 +249,7 @@ void GLGizmosManager::update_data() set_rotation(Vec3d::Zero()); set_flattening_data(selection.is_from_single_object() ? selection.get_model()->objects[selection.get_object_idx()] : nullptr); set_sla_support_data(selection.is_from_single_instance() ? selection.get_model()->objects[selection.get_object_idx()] : nullptr); - set_fdm_support_data(selection.is_from_single_instance() ? selection.get_model()->objects[selection.get_object_idx()] : nullptr); + set_painter_gizmo_data(); } } @@ -378,12 +384,13 @@ void GLGizmosManager::set_sla_support_data(ModelObject* model_object) gizmo_supports->set_sla_support_data(model_object, m_parent.get_selection()); } -void GLGizmosManager::set_fdm_support_data(ModelObject* model_object) +void GLGizmosManager::set_painter_gizmo_data() { if (!m_enabled || m_gizmos.empty()) return; - dynamic_cast(m_gizmos[FdmSupports].get())->set_fdm_support_data(model_object, m_parent.get_selection()); + dynamic_cast(m_gizmos[FdmSupports].get())->set_painter_gizmo_data(m_parent.get_selection()); + dynamic_cast(m_gizmos[Seam].get())->set_painter_gizmo_data(m_parent.get_selection()); } // Returns true if the gizmo used the event to do something, false otherwise. @@ -398,6 +405,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p return dynamic_cast(m_gizmos[Hollow].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else if (m_current == FdmSupports) return dynamic_cast(m_gizmos[FdmSupports].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + else if (m_current == Seam) + return dynamic_cast(m_gizmos[Seam].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); else return false; } @@ -461,7 +470,7 @@ bool GLGizmosManager::on_mouse_wheel(wxMouseEvent& evt) { bool processed = false; - if (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports) { + if (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam) { float rot = (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta(); if (gizmo_event((rot > 0.f ? SLAGizmoEventType::MouseWheelUp : SLAGizmoEventType::MouseWheelDown), Vec2d::Zero(), evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) processed = true; @@ -603,7 +612,7 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) if (evt.LeftDown()) { - if ((m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports) + if ((m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports ||m_current == Seam) && gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) // the gizmo got the event and took some action, there is no need to do anything more processed = true; @@ -630,23 +639,24 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) // event was taken care of by the SlaSupports gizmo processed = true; } - else if (evt.RightDown() && (selected_object_idx != -1) && m_current == FdmSupports + else if (evt.RightDown() && (selected_object_idx != -1) && (m_current == FdmSupports || m_current == Seam) && gizmo_event(SLAGizmoEventType::RightDown, mouse_pos)) { - // event was taken care of by the FdmSupports gizmo + // event was taken care of by the FdmSupports / Seam gizmo processed = true; } - else if (evt.Dragging() && (m_parent.get_move_volume_id() != -1) && (m_current == SlaSupports || m_current == Hollow)) + else if (evt.Dragging() && (m_parent.get_move_volume_id() != -1) + && (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam)) // don't allow dragging objects with the Sla gizmo on processed = true; - else if (evt.Dragging() && (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports ) + else if (evt.Dragging() && (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam ) && gizmo_event(SLAGizmoEventType::Dragging, mouse_pos, evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) { // the gizmo got the event and took some action, no need to do anything more here m_parent.set_as_dirty(); processed = true; } - else if (evt.LeftUp() && (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports) && !m_parent.is_mouse_dragging()) + else if (evt.LeftUp() && (m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam) && !m_parent.is_mouse_dragging()) { // in case SLA/FDM gizmo is selected, we just pass the LeftUp event and stop processing - neither // object moving or selecting is suppressed in that case @@ -658,7 +668,7 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) // to avoid to loose the selection when user clicks an the white faces of a different object while the Flatten gizmo is active processed = true; } - else if (evt.RightUp() && m_current == FdmSupports && !m_parent.is_mouse_dragging()) + else if (evt.RightUp() && (m_current == FdmSupports || m_current == Seam) && !m_parent.is_mouse_dragging()) { gizmo_event(SLAGizmoEventType::RightUp, mouse_pos, evt.ShiftDown(), evt.AltDown(), evt.ControlDown()); processed = true; @@ -748,7 +758,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) case 'r' : case 'R' : { - if ((m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports) && gizmo_event(SLAGizmoEventType::ResetClippingPlane)) + if ((m_current == SlaSupports || m_current == Hollow || m_current == FdmSupports || m_current == Seam) && gizmo_event(SLAGizmoEventType::ResetClippingPlane)) processed = true; break; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 4ad46a2a92..6b965525d5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -67,6 +67,7 @@ public: Hollow, SlaSupports, FdmSupports, + Seam, Undefined }; @@ -203,7 +204,7 @@ public: void set_sla_support_data(ModelObject* model_object); - void set_fdm_support_data(ModelObject* model_object); + void set_painter_gizmo_data(); bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position = Vec2d::Zero(), bool shift_down = false, bool alt_down = false, bool control_down = false); ClippingPlane get_clipping_plane() const; diff --git a/src/slic3r/GUI/I18N.hpp b/src/slic3r/GUI/I18N.hpp index 25e46930ba..7bad6880e9 100644 --- a/src/slic3r/GUI/I18N.hpp +++ b/src/slic3r/GUI/I18N.hpp @@ -12,7 +12,7 @@ #ifndef L // !!! If you needed to translate some wxString, -// !!! please use _(L(string)) +// !!! please use _L(string) // !!! _() - is a standard wxWidgets macro to translate // !!! L() is used only for marking localizable string // !!! It will be used in "xgettext" to create a Locating Message Catalog. diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 51a9a6d4eb..f9a23cb51c 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -37,13 +37,30 @@ namespace GUI { static const std::map font_icons = { - {ImGui::PrintIconMarker , "cog" }, - {ImGui::PrinterIconMarker , "printer" }, - {ImGui::PrinterSlaIconMarker, "sla_printer"}, - {ImGui::FilamentIconMarker , "spool" }, - {ImGui::MaterialIconMarker , "resin" } + {ImGui::PrintIconMarker , "cog" }, + {ImGui::PrinterIconMarker , "printer" }, + {ImGui::PrinterSlaIconMarker , "sla_printer" }, + {ImGui::FilamentIconMarker , "spool" }, + {ImGui::MaterialIconMarker , "resin" }, + {ImGui::CloseIconMarker , "notification_close" }, + {ImGui::CloseIconHoverMarker , "notification_close_hover" }, + //{ImGui::TimerDotMarker , "timer_dot" }, + //{ImGui::TimerDotEmptyMarker , "timer_dot_empty" }, + {ImGui::MinimalizeMarker , "notification_minimalize" }, + {ImGui::MinimalizeHoverMarker , "notification_minimalize_hover" }, + {ImGui::WarningMarker , "notification_warning" }, + {ImGui::ErrorMarker , "notification_error" } }; +const ImVec4 ImGuiWrapper::COL_GREY_DARK = { 0.333f, 0.333f, 0.333f, 1.0f }; +const ImVec4 ImGuiWrapper::COL_GREY_LIGHT = { 0.4f, 0.4f, 0.4f, 1.0f }; +const ImVec4 ImGuiWrapper::COL_ORANGE_DARK = { 0.757f, 0.404f, 0.216f, 1.0f }; +const ImVec4 ImGuiWrapper::COL_ORANGE_LIGHT = { 1.0f, 0.49f, 0.216f, 1.0f }; +const ImVec4 ImGuiWrapper::COL_WINDOW_BACKGROUND = { 0.133f, 0.133f, 0.133f, 0.8f }; +const ImVec4 ImGuiWrapper::COL_BUTTON_BACKGROUND = { 0.233f, 0.233f, 0.233f, 1.0f }; +const ImVec4 ImGuiWrapper::COL_BUTTON_HOVERED = { 0.433f, 0.433f, 0.433f, 1.8f }; +const ImVec4 ImGuiWrapper::COL_BUTTON_ACTIVE = ImGuiWrapper::COL_BUTTON_HOVERED; + ImGuiWrapper::ImGuiWrapper() : m_glyph_ranges(nullptr) , m_font_cjk(false) @@ -176,6 +193,9 @@ bool ImGuiWrapper::update_mouse_data(wxMouseEvent& evt) io.MouseDown[0] = evt.LeftIsDown(); io.MouseDown[1] = evt.RightIsDown(); io.MouseDown[2] = evt.MiddleIsDown(); + float wheel_delta = static_cast(evt.GetWheelDelta()); + if (wheel_delta != 0.0f) + io.MouseWheel = static_cast(evt.GetWheelRotation()) / wheel_delta; unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0); m_mouse_buttons = buttons; @@ -262,6 +282,11 @@ void ImGuiWrapper::set_next_window_bg_alpha(float alpha) ImGui::SetNextWindowBgAlpha(alpha); } +void ImGuiWrapper::set_next_window_size(float x, float y, ImGuiCond cond) +{ + ImGui::SetNextWindowSize(ImVec2(x, y), cond); +} + bool ImGuiWrapper::begin(const std::string &name, int flags) { return ImGui::Begin(name.c_str(), nullptr, (ImGuiWindowFlags)flags); @@ -293,12 +318,23 @@ bool ImGuiWrapper::button(const wxString &label) return ImGui::Button(label_utf8.c_str()); } +bool ImGuiWrapper::button(const wxString& label, float width, float height) +{ + auto label_utf8 = into_u8(label); + return ImGui::Button(label_utf8.c_str(), ImVec2(width, height)); +} + bool ImGuiWrapper::radio_button(const wxString &label, bool active) { auto label_utf8 = into_u8(label); return ImGui::RadioButton(label_utf8.c_str(), active); } +bool ImGuiWrapper::image_button() +{ + return false; +} + bool ImGuiWrapper::input_double(const std::string &label, const double &value, const std::string &format) { return ImGui::InputDouble(label.c_str(), const_cast(&value), 0.0f, 0.0f, format.c_str()); @@ -337,7 +373,7 @@ bool ImGuiWrapper::checkbox(const wxString &label, bool &value) void ImGuiWrapper::text(const char *label) { - ImGui::Text(label, NULL); + ImGui::Text("%s", label); } void ImGuiWrapper::text(const std::string &label) @@ -351,6 +387,22 @@ void ImGuiWrapper::text(const wxString &label) this->text(label_utf8.c_str()); } +void ImGuiWrapper::text_colored(const ImVec4& color, const char* label) +{ + ImGui::TextColored(color, "%s", label); +} + +void ImGuiWrapper::text_colored(const ImVec4& color, const std::string& label) +{ + this->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()); +} + bool ImGuiWrapper::slider_float(const char* label, float* v, float v_min, float v_max, const char* format/* = "%.3f"*/, float power/* = 1.0f*/) { return ImGui::SliderFloat(label, v, v_min, v_max, format, power); @@ -373,10 +425,10 @@ bool ImGuiWrapper::combo(const wxString& label, const std::vector& text(label); ImGui::SameLine(); - int selection_out = -1; + int selection_out = selection; bool res = false; - const char *selection_str = selection < (int)options.size() ? options[selection].c_str() : ""; + const char *selection_str = selection < int(options.size()) && selection >= 0 ? options[selection].c_str() : ""; if (ImGui::BeginCombo("", selection_str)) { for (int i = 0; i < (int)options.size(); i++) { if (ImGui::Selectable(options[i].c_str(), i == selection)) { @@ -751,6 +803,12 @@ void ImGuiWrapper::search_list(const ImVec2& size_, bool (*items_getter)(int, co check_box(_L("Search in English"), view_params.english); } +void ImGuiWrapper::title(const std::string& str) +{ + text(str); + ImGui::Separator(); +} + void ImGuiWrapper::disabled_begin(bool disabled) { wxCHECK_RET(!m_disabled, "ImGUI: Unbalanced disabled_begin() call"); @@ -869,7 +927,7 @@ void ImGuiWrapper::init_font(bool compress) if (font == nullptr) { font = io.Fonts->AddFontDefault(); if (font == nullptr) { - throw std::runtime_error("ImGui: Could not load deafult font"); + throw Slic3r::RuntimeError("ImGui: Could not load deafult font"); } } @@ -970,23 +1028,13 @@ void ImGuiWrapper::init_style() { ImGuiStyle &style = ImGui::GetStyle(); - auto set_color = [&](ImGuiCol_ col, unsigned hex_color) { - style.Colors[col] = ImVec4( - ((hex_color >> 24) & 0xff) / 255.0f, - ((hex_color >> 16) & 0xff) / 255.0f, - ((hex_color >> 8) & 0xff) / 255.0f, - (hex_color & 0xff) / 255.0f); + auto set_color = [&](ImGuiCol_ entity, ImVec4 color) { + style.Colors[entity] = color; }; - static const unsigned COL_WINDOW_BACKGROND = 0x222222cc; - static const unsigned COL_GREY_DARK = 0x555555ff; - static const unsigned COL_GREY_LIGHT = 0x666666ff; - static const unsigned COL_ORANGE_DARK = 0xc16737ff; - static const unsigned COL_ORANGE_LIGHT = 0xff7d38ff; - // Window style.WindowRounding = 4.0f; - set_color(ImGuiCol_WindowBg, COL_WINDOW_BACKGROND); + set_color(ImGuiCol_WindowBg, COL_WINDOW_BACKGROUND); set_color(ImGuiCol_TitleBgActive, COL_ORANGE_DARK); // Generics @@ -998,9 +1046,9 @@ void ImGuiWrapper::init_style() set_color(ImGuiCol_TextSelectedBg, COL_ORANGE_DARK); // Buttons - set_color(ImGuiCol_Button, COL_ORANGE_DARK); - set_color(ImGuiCol_ButtonHovered, COL_ORANGE_LIGHT); - set_color(ImGuiCol_ButtonActive, COL_ORANGE_LIGHT); + set_color(ImGuiCol_Button, COL_BUTTON_BACKGROUND); + set_color(ImGuiCol_ButtonHovered, COL_BUTTON_HOVERED); + set_color(ImGuiCol_ButtonActive, COL_BUTTON_ACTIVE); // Checkbox set_color(ImGuiCol_CheckMark, COL_ORANGE_LIGHT); @@ -1016,6 +1064,13 @@ void ImGuiWrapper::init_style() // Separator set_color(ImGuiCol_Separator, COL_ORANGE_LIGHT); + + // Tabs + set_color(ImGuiCol_Tab, COL_ORANGE_DARK); + set_color(ImGuiCol_TabHovered, COL_ORANGE_LIGHT); + set_color(ImGuiCol_TabActive, COL_ORANGE_LIGHT); + set_color(ImGuiCol_TabUnfocused, COL_GREY_DARK); + set_color(ImGuiCol_TabUnfocusedActive, COL_GREY_LIGHT); } void ImGuiWrapper::render_draw_data(ImDrawData *draw_data) diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index bf542e1381..5484e46c6f 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -57,6 +57,7 @@ public: void set_next_window_pos(float x, float y, int flag, float pivot_x = 0.0f, float pivot_y = 0.0f); void set_next_window_bg_alpha(float alpha); + void set_next_window_size(float x, float y, ImGuiCond cond); bool begin(const std::string &name, int flags = 0); bool begin(const wxString &name, int flags = 0); @@ -65,7 +66,9 @@ public: void end(); bool button(const wxString &label); + bool button(const wxString& label, float width, float height); bool radio_button(const wxString &label, bool active); + bool image_button(); bool input_double(const std::string &label, const double &value, const std::string &format = "%.3f"); bool input_double(const wxString &label, const double &value, const std::string &format = "%.3f"); bool input_vec3(const std::string &label, const Vec3d &value, float width, const std::string &format = "%.3f"); @@ -73,6 +76,9 @@ public: 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); bool slider_float(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); bool slider_float(const std::string& label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); bool slider_float(const wxString& label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); @@ -80,6 +86,7 @@ public: bool undo_redo_list(const ImVec2& size, const bool is_undo, bool (*items_getter)(const bool, int, const char**), int& hovered, int& selected, int& mouse_wheel); void search_list(const ImVec2& size, bool (*items_getter)(int, const char** label, const char** tooltip), char* search_str, Search::OptionViewParameters& view_params, int& selected, bool& edited, int& mouse_wheel, bool is_localized); + void title(const std::string& str); void disabled_begin(bool disabled); void disabled_end(); @@ -89,6 +96,15 @@ public: bool want_text_input() const; bool want_any_input() const; + static const ImVec4 COL_GREY_DARK; + static const ImVec4 COL_GREY_LIGHT; + static const ImVec4 COL_ORANGE_DARK; + static const ImVec4 COL_ORANGE_LIGHT; + static const ImVec4 COL_WINDOW_BACKGROUND; + static const ImVec4 COL_BUTTON_BACKGROUND; + static const ImVec4 COL_BUTTON_HOVERED; + static const ImVec4 COL_BUTTON_ACTIVE; + private: void init_font(bool compress); void init_input(); diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index c847c84b48..978ccf8fcf 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -4,6 +4,7 @@ #include "libslic3r/SLA/Rotfinder.hpp" #include "libslic3r/MinAreaBoundingBox.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/SLAPrint.hpp" #include "slic3r/GUI/Plater.hpp" @@ -12,26 +13,41 @@ namespace Slic3r { namespace GUI { void RotoptimizeJob::process() { int obj_idx = m_plater->get_selected_object_idx(); - if (obj_idx < 0) { return; } + if (obj_idx < 0 || int(m_plater->sla_print().objects().size()) <= obj_idx) + return; ModelObject *o = m_plater->model().objects[size_t(obj_idx)]; + const SLAPrintObject *po = m_plater->sla_print().objects()[size_t(obj_idx)]; - auto r = sla::find_best_rotation( - *o, - .005f, + if (!o || !po) return; + + TriangleMesh mesh = o->raw_mesh(); + mesh.require_shared_vertices(); + +// for (auto inst : o->instances) { +// Transform3d tr = Transform3d::Identity(); +// tr.rotate(Eigen::AngleAxisd(inst->get_rotation(Z), Vec3d::UnitZ())); +// tr.rotate(Eigen::AngleAxisd(inst->get_rotation(Y), Vec3d::UnitY())); +// tr.rotate(Eigen::AngleAxisd(inst->get_rotation(X), Vec3d::UnitX())); + +// double score = sla::get_model_supportedness(*po, tr); + +// std::cout << "Model supportedness before: " << score << std::endl; +// } + + Vec2d r = sla::find_best_rotation(*po, 0.75f, [this](unsigned s) { if (s < 100) - update_status(int(s), - _(L("Searching for optimal orientation"))); + update_status(int(s), _(L("Searching for optimal orientation"))); }, - [this]() { return was_canceled(); }); + [this] () { return was_canceled(); }); double mindist = 6.0; // FIXME if (!was_canceled()) { for(ModelInstance * oi : o->instances) { - oi->set_rotation({r[X], r[Y], r[Z]}); + oi->set_rotation({r[X], r[Y], 0.}); auto trmatrix = oi->get_transformation().get_matrix(); Polygon trchull = o->convex_hull_2d(trmatrix); diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index 4e9f08ff23..adecae6ac0 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -1,14 +1,14 @@ #include "SLAImportJob.hpp" +#include "libslic3r/Format/SL1.hpp" + #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/AppConfig.hpp" #include "slic3r/GUI/Plater.hpp" -#include "slic3r/GUI/PresetBundle.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" -#include "slic3r/Utils/SLAImport.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/PresetBundle.hpp" #include #include @@ -219,8 +219,10 @@ void SLAImportJob::finalize() wxGetApp().load_current_presets(); } - if (!p->mesh.empty()) - p->plater->sidebar().obj_list()->load_mesh_object(p->mesh, name); + if (!p->mesh.empty()) { + bool is_centered = false; + p->plater->sidebar().obj_list()->load_mesh_object(p->mesh, name, is_centered); + } reset(); } diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 556b610e91..4affd13269 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -1,3 +1,4 @@ +#include "libslic3r/libslic3r.h" #include "KBShortcutsDialog.hpp" #include "I18N.hpp" #include "libslic3r/Utils.hpp" @@ -6,6 +7,9 @@ #include #include "GUI_App.hpp" #include "wxExtensions.hpp" +#if ENABLE_GCODE_VIEWER +#include "MainFrame.hpp" +#endif // ENABLE_GCODE_VIEWER #define NOTEBOOK_TOP 1 #define NOTEBOOK_LEFT 2 @@ -29,12 +33,12 @@ namespace Slic3r { namespace GUI { KBShortcutsDialog::KBShortcutsDialog() - : DPIDialog(NULL, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(L("Keyboard Shortcuts")), -#if ENABLE_SCROLLABLE - wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +#if ENABLE_GCODE_VIEWER + : DPIDialog(NULL, wxID_ANY, wxString(wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME) + " - " + _L("Keyboard Shortcuts"), #else - wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE) -#endif // ENABLE_SCROLLABLE + : DPIDialog(NULL, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _L("Keyboard Shortcuts"), +#endif // ENABLE_GCODE_VIEWER + wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); @@ -65,13 +69,9 @@ main_sizer->Add(book, 1, wxEXPAND | wxALL, 10); fill_shortcuts(); for (size_t i = 0; i < m_full_shortcuts.size(); ++i) { -#if ENABLE_SCROLLABLE wxPanel* page = create_page(book, m_full_shortcuts[i], font, bold_font); m_pages.push_back(page); book->AddPage(page, m_full_shortcuts[i].first, i == 0); -#else - book->AddPage(create_page(book, m_full_shortcuts[i], font, bold_font), m_full_shortcuts[i].first, i == 0); -#endif // ENABLE_SCROLLABLE } wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK); @@ -98,114 +98,120 @@ void KBShortcutsDialog::fill_shortcuts() const std::string& ctrl = GUI::shortkey_ctrl_prefix(); const std::string& alt = GUI::shortkey_alt_prefix(); - Shortcuts commands_shortcuts = { - // File - { ctrl + "N", L("New project, clear plater") }, - { ctrl + "O", L("Open project STL/OBJ/AMF/3MF with config, clear plater") }, - { ctrl + "S", L("Save project (3mf)") }, - { ctrl + alt + "S", L("Save project as (3mf)") }, - { ctrl + "R", L("(Re)slice") }, - // File>Import - { ctrl + "I", L("Import STL/OBJ/AMF/3MF without config, keep plater") }, - { ctrl + "L", L("Import Config from ini/amf/3mf/gcode") }, - { ctrl + alt + "L", L("Load Config from ini/amf/3mf/gcode and merge") }, - // File>Export - { ctrl + "G", L("Export G-code") }, - { ctrl + "Shift+" + "G", L("Send G-code") }, - { ctrl + "E", L("Export config") }, - { ctrl + "U", L("Export to SD card / Flash drive") }, - { ctrl + "T", L("Eject SD card / Flash drive") }, - // Edit - { ctrl + "A", L("Select all objects") }, - { "Esc", L("Deselect all") }, - { "Del", L("Delete selected") }, - { ctrl + "Del", L("Delete all") }, - { ctrl + "Z", L("Undo") }, - { ctrl + "Y", L("Redo") }, - { ctrl + "C", L("Copy to clipboard") }, - { ctrl + "V", L("Paste from clipboard") }, - { "F5", L("Reload plater from disk") }, - { ctrl + "F", L("Search") }, - // Window - { ctrl + "1", L("Select Plater Tab") }, - { ctrl + "2", L("Select Print Settings Tab") }, - { ctrl + "3", L("Select Filament Settings Tab") }, - { ctrl + "4", L("Select Printer Settings Tab") }, - { ctrl + "5", L("Switch to 3D") }, - { ctrl + "6", L("Switch to Preview") }, - { ctrl + "J", L("Print host upload queue") }, - // View - { "0-6", L("Camera view") }, - { "E", L("Show/Hide object/instance labels") }, +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor()) { +#endif // ENABLE_GCODE_VIEWER + Shortcuts commands_shortcuts = { + // File + { ctrl + "N", L("New project, clear plater") }, + { ctrl + "O", L("Open project STL/OBJ/AMF/3MF with config, clear plater") }, + { ctrl + "S", L("Save project (3mf)") }, + { ctrl + alt + "S", L("Save project as (3mf)") }, + { ctrl + "R", L("(Re)slice") }, + // File>Import + { ctrl + "I", L("Import STL/OBJ/AMF/3MF without config, keep plater") }, + { ctrl + "L", L("Import Config from ini/amf/3mf/gcode") }, + { ctrl + alt + "L", L("Load Config from ini/amf/3mf/gcode and merge") }, + // File>Export + { ctrl + "G", L("Export G-code") }, + { ctrl + "Shift+" + "G", L("Send G-code") }, + { ctrl + "E", L("Export config") }, + { ctrl + "U", L("Export to SD card / Flash drive") }, + { ctrl + "T", L("Eject SD card / Flash drive") }, + // Edit + { ctrl + "A", L("Select all objects") }, + { "Esc", L("Deselect all") }, + { "Del", L("Delete selected") }, + { ctrl + "Del", L("Delete all") }, + { ctrl + "Z", L("Undo") }, + { ctrl + "Y", L("Redo") }, + { ctrl + "C", L("Copy to clipboard") }, + { ctrl + "V", L("Paste from clipboard") }, + { "F5", L("Reload plater from disk") }, + { ctrl + "F", L("Search") }, + // Window + { ctrl + "1", L("Select Plater Tab") }, + { ctrl + "2", L("Select Print Settings Tab") }, + { ctrl + "3", L("Select Filament Settings Tab") }, + { ctrl + "4", L("Select Printer Settings Tab") }, + { ctrl + "5", L("Switch to 3D") }, + { ctrl + "6", L("Switch to Preview") }, + { ctrl + "J", L("Print host upload queue") }, + // View + { "0-6", L("Camera view") }, + { "E", L("Show/Hide object/instance labels") }, #if ENABLE_SLOPE_RENDERING - { "D", L("Turn On/Off facets' slope rendering") }, + { "D", L("Turn On/Off facets' slope rendering") }, #endif // ENABLE_SLOPE_RENDERING - // Configuration - { ctrl + "P", L("Preferences") }, - // Help - { "?", L("Show keyboard shortcuts list") } - }; + // Configuration + { ctrl + "P", L("Preferences") }, + // Help + { "?", L("Show keyboard shortcuts list") } + }; - m_full_shortcuts.push_back(std::make_pair(_(L("Commands")), commands_shortcuts)); + m_full_shortcuts.push_back(std::make_pair(_L("Commands"), commands_shortcuts)); - Shortcuts plater_shortcuts = { - { "A", L("Arrange") }, - { "Shift+A", L("Arrange selection") }, - { "+", L("Add Instance of the selected object") }, - { "-", L("Remove Instance of the selected object") }, - { ctrl, L("Press to select multiple objects\nor move multiple objects with mouse") }, - { "Shift+", L("Press to activate selection rectangle") }, - { alt, L("Press to activate deselection rectangle") }, - { L("Arrow Up"), L("Move selection 10 mm in positive Y direction") }, - { L("Arrow Down"), L("Move selection 10 mm in negative Y direction") }, - { L("Arrow Left"), L("Move selection 10 mm in negative X direction") }, - { L("Arrow Right"), L("Move selection 10 mm in positive X direction") }, - { std::string("Shift+") + L("Any arrow"), L("Movement step set to 1 mm") }, - { ctrl + L("Any arrow"), L("Movement in camera space") }, - { L("Page Up"), L("Rotate selection 45 degrees CCW") }, - { L("Page Down"), L("Rotate selection 45 degrees CW") }, - { "M", L("Gizmo move") }, - { "S", L("Gizmo scale") }, - { "R", L("Gizmo rotate") }, - { "C", L("Gizmo cut") }, - { "F", L("Gizmo Place face on bed") }, - { "H", L("Gizmo SLA hollow") }, - { "L", L("Gizmo SLA support points") }, - { "Esc", L("Unselect gizmo or clear selection") }, - { "K", L("Change camera type (perspective, orthographic)") }, - { "B", L("Zoom to Bed") }, - { "Z", L("Zoom to selected object\nor all objects in scene, if none selected") }, - { "I", L("Zoom in") }, - { "O", L("Zoom out") }, + Shortcuts plater_shortcuts = { + { "A", L("Arrange") }, + { "Shift+A", L("Arrange selection") }, + { "+", L("Add Instance of the selected object") }, + { "-", L("Remove Instance of the selected object") }, + { ctrl, L("Press to select multiple objects\nor move multiple objects with mouse") }, + { "Shift+", L("Press to activate selection rectangle") }, + { alt, L("Press to activate deselection rectangle") }, + { L("Arrow Up"), L("Move selection 10 mm in positive Y direction") }, + { L("Arrow Down"), L("Move selection 10 mm in negative Y direction") }, + { L("Arrow Left"), L("Move selection 10 mm in negative X direction") }, + { L("Arrow Right"), L("Move selection 10 mm in positive X direction") }, + { std::string("Shift+") + L("Any arrow"), L("Movement step set to 1 mm") }, + { ctrl + L("Any arrow"), L("Movement in camera space") }, + { L("Page Up"), L("Rotate selection 45 degrees CCW") }, + { L("Page Down"), L("Rotate selection 45 degrees CW") }, + { "M", L("Gizmo move") }, + { "S", L("Gizmo scale") }, + { "R", L("Gizmo rotate") }, + { "C", L("Gizmo cut") }, + { "F", L("Gizmo Place face on bed") }, + { "H", L("Gizmo SLA hollow") }, + { "L", L("Gizmo SLA support points") }, + { "Esc", L("Unselect gizmo or clear selection") }, + { "K", L("Change camera type (perspective, orthographic)") }, + { "B", L("Zoom to Bed") }, + { "Z", L("Zoom to selected object\nor all objects in scene, if none selected") }, + { "I", L("Zoom in") }, + { "O", L("Zoom out") }, #ifdef __linux__ - { ctrl + "M", L("Show/Hide 3Dconnexion devices settings dialog") }, + { ctrl + "M", L("Show/Hide 3Dconnexion devices settings dialog") }, #endif // __linux__ #if ENABLE_RENDER_PICKING_PASS - // Don't localize debugging texts. - { "T", "Toggle picking pass texture rendering on/off" }, + // Don't localize debugging texts. + { "P", "Toggle picking pass texture rendering on/off" }, #endif // ENABLE_RENDER_PICKING_PASS - }; + }; - m_full_shortcuts.push_back(std::make_pair(_(L("Plater")), plater_shortcuts)); + m_full_shortcuts.push_back(std::make_pair(_L("Plater"), plater_shortcuts)); - Shortcuts gizmos_shortcuts = { - { "Shift+", L("Press to snap by 5% in Gizmo scale\nor to snap by 1mm in Gizmo move") }, - { "F", L("Scale selection to fit print volume\nin Gizmo scale") }, - { ctrl, L("Press to activate one direction scaling in Gizmo scale") }, - { alt, L("Press to scale (in Gizmo scale) or rotate (in Gizmo rotate)\nselected objects around their own center") }, - }; + Shortcuts gizmos_shortcuts = { + { "Shift+", L("Press to snap by 5% in Gizmo scale\nor to snap by 1mm in Gizmo move") }, + { "F", L("Scale selection to fit print volume\nin Gizmo scale") }, + { ctrl, L("Press to activate one direction scaling in Gizmo scale") }, + { alt, L("Press to scale (in Gizmo scale) or rotate (in Gizmo rotate)\nselected objects around their own center") }, + }; - m_full_shortcuts.push_back(std::make_pair(_(L("Gizmos")), gizmos_shortcuts)); + m_full_shortcuts.push_back(std::make_pair(_L("Gizmos"), gizmos_shortcuts)); +#if ENABLE_GCODE_VIEWER + } +#endif // ENABLE_GCODE_VIEWER Shortcuts preview_shortcuts = { { L("Arrow Up"), L("Upper Layer") }, { L("Arrow Down"), L("Lower Layer") }, { "U", L("Upper Layer") }, { "D", L("Lower Layer") }, - { "L", L("Show/Hide Legend") } + { "L", L("Show/Hide Legend/Estimated printing time") }, }; - m_full_shortcuts.push_back(std::make_pair(_(L("Preview")), preview_shortcuts)); + m_full_shortcuts.push_back(std::make_pair(_L("Preview"), preview_shortcuts)); Shortcuts layers_slider_shortcuts = { { L("Arrow Up"), L("Move current slider thumb Up") }, @@ -213,10 +219,23 @@ void KBShortcutsDialog::fill_shortcuts() { L("Arrow Left"), L("Set upper thumb to current slider thumb") }, { L("Arrow Right"), L("Set lower thumb to current slider thumb") }, { "+", L("Add color change marker for current layer") }, - { "-", L("Delete color change marker for current layer") } + { "-", L("Delete color change marker for current layer") }, + { "Shift+", L("Press to speed up 5 times while moving thumb\nwith arrow keys or mouse wheel") }, + { ctrl, L("Press to speed up 5 times while moving thumb\nwith arrow keys or mouse wheel") }, }; - m_full_shortcuts.push_back(std::make_pair(_(L("Layers Slider")), layers_slider_shortcuts)); + m_full_shortcuts.push_back(std::make_pair(_L("Layers Slider"), layers_slider_shortcuts)); + +#if ENABLE_GCODE_VIEWER + Shortcuts sequential_slider_shortcuts = { + { L("Arrow Left"), L("Move current slider thumb Left") }, + { L("Arrow Right"), L("Move current slider thumb Right") }, + { "Shift+", L("Press to speed up 5 times while moving thumb\nwith arrow keys or mouse wheel") }, + { ctrl, L("Press to speed up 5 times while moving thumb\nwith arrow keys or mouse wheel") }, + }; + + m_full_shortcuts.push_back(std::make_pair(_L("Sequential Slider"), sequential_slider_shortcuts)); +#endif // ENABLE_GCODE_VIEWER } wxPanel* KBShortcutsDialog::create_header(wxWindow* parent, const wxFont& bold_font) @@ -239,7 +258,7 @@ wxPanel* KBShortcutsDialog::create_header(wxWindow* parent, const wxFont& bold_f sizer->Add(m_header_bitmap, 0, wxEXPAND | wxLEFT | wxRIGHT, 10); // text - wxStaticText* text = new wxStaticText(panel, wxID_ANY, _(L("Keyboard shortcuts"))); + wxStaticText* text = new wxStaticText(panel, wxID_ANY, _L("Keyboard shortcuts")); text->SetFont(header_font); sizer->Add(text, 0, wxALIGN_CENTER_VERTICAL); @@ -254,13 +273,9 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const std::pairSetScrollbars(20, 20, 50, 50); page->SetInitialSize(wxSize(850, 450)); -#else - wxPanel* page = new wxPanel(parent); -#endif // ENABLE_SCROLLABLE #if (BOOK_TYPE == LISTBOOK_TOP) || (BOOK_TYPE == LISTBOOK_LEFT) wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, page, " " + shortcuts.first + " "); diff --git a/src/slic3r/GUI/KBShortcutsDialog.hpp b/src/slic3r/GUI/KBShortcutsDialog.hpp index 70820ae774..a8ec4e4267 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.hpp +++ b/src/slic3r/GUI/KBShortcutsDialog.hpp @@ -8,8 +8,6 @@ #include "GUI_Utils.hpp" #include "wxExtensions.hpp" -#define ENABLE_SCROLLABLE 1 - namespace Slic3r { namespace GUI { @@ -22,9 +20,7 @@ class KBShortcutsDialog : public DPIDialog ShortcutsVec m_full_shortcuts; ScalableBitmap m_logo_bmp; wxStaticBitmap* m_header_bitmap; -#if ENABLE_SCROLLABLE std::vector m_pages; -#endif // ENABLE_SCROLLABLE public: KBShortcutsDialog(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 10d9d800f9..fbb7a190f0 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -8,6 +8,7 @@ #include #include //#include +#include #include #include @@ -15,12 +16,11 @@ #include "libslic3r/Print.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/SLAPrint.hpp" +#include "libslic3r/PresetBundle.hpp" #include "Tab.hpp" -#include "PresetBundle.hpp" #include "ProgressStatusBar.hpp" #include "3DScene.hpp" -#include "AppConfig.hpp" #include "PrintHostDialogs.hpp" #include "wxExtensions.hpp" #include "GUI_ObjectList.hpp" @@ -30,6 +30,7 @@ #include "I18N.hpp" #include "GLCanvas3D.hpp" #include "Plater.hpp" +#include "../Utils/Process.hpp" #include #include "GUI_App.hpp" @@ -42,44 +43,17 @@ namespace Slic3r { namespace GUI { -#if ENABLE_LAYOUT_NO_RESTART enum class ERescaleTarget { Mainframe, SettingsDialog }; -static void rescale_dialog_after_dpi_change(MainFrame& mainframe, SettingsDialog& dialog, ERescaleTarget target) -{ - int mainframe_dpi = get_dpi_for_window(&mainframe); - int dialog_dpi = get_dpi_for_window(&dialog); - if (mainframe_dpi != dialog_dpi) { - if (target == ERescaleTarget::SettingsDialog) { - dialog.enable_force_rescale(); -#if wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - dialog.GetEventHandler()->AddPendingEvent(wxDPIChangedEvent(wxSize(mainframe_dpi, mainframe_dpi), wxSize(dialog_dpi, dialog_dpi))); -#else - dialog.GetEventHandler()->AddPendingEvent(DpiChangedEvent(EVT_DPI_CHANGED_SLICER, dialog_dpi, dialog.GetRect())); -#endif // wxVERSION_EQUAL_OR_GREATER_THAN - } else { -#if wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - mainframe.GetEventHandler()->AddPendingEvent(wxDPIChangedEvent(wxSize(dialog_dpi, dialog_dpi), wxSize(mainframe_dpi, mainframe_dpi))); -#else - mainframe.enable_force_rescale(); - mainframe.GetEventHandler()->AddPendingEvent(DpiChangedEvent(EVT_DPI_CHANGED_SLICER, mainframe_dpi, mainframe.GetRect())); -#endif // wxVERSION_EQUAL_OR_GREATER_THAN - } - } -} -#endif // ENABLE_LAYOUT_NO_RESTART - MainFrame::MainFrame() : DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE, "mainframe"), m_printhost_queue_dlg(new PrintHostQueueDialog(this)) , m_recent_projects(9) -#if ENABLE_LAYOUT_NO_RESTART , m_settings_dialog(this) -#endif // ENABLE_LAYOUT_NO_RESTART { // Fonts were created by the DPIFrame constructor for the monitor, on which the window opened. wxGetApp().update_fonts(this); @@ -89,33 +63,92 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S #endif // Font is already set in DPIFrame constructor */ + +#if ENABLE_GCODE_VIEWER_TASKBAR_ICON + if (wxTaskBarIcon::IsAvailable()) { +#if defined(__WXOSX__) && wxOSX_USE_COCOA + m_taskbar_icon = new wxTaskBarIcon(wxTBI_DOCK); +#else + m_taskbar_icon = new wxTaskBarIcon(); +#endif + m_taskbar_icon->SetIcon(wxIcon(Slic3r::var("PrusaSlicer_128px.png"), wxBITMAP_TYPE_PNG), "PrusaSlicer"); + + m_taskbar_icon->Bind(wxEVT_TASKBAR_CLICK, [this](wxTaskBarIconEvent& evt) { + wxString msg = _L("You pressed the icon in taskbar for ") + "\n"; + if (m_mode == EMode::Editor) + msg += wxString(SLIC3R_APP_NAME); + else + msg += wxString(SLIC3R_APP_NAME) + "-GCode viewer"; + + wxMessageDialog dialog(nullptr, msg, _("Taskbar icon clicked"), wxOK); + dialog.ShowModal(); + }); + } +#endif // ENABLE_GCODE_VIEWER_TASKBAR_ICON + // Load the icon either from the exe, or from the ico file. #if _WIN32 { + TCHAR szExeFileName[MAX_PATH]; GetModuleFileName(nullptr, szExeFileName, MAX_PATH); SetIcon(wxIcon(szExeFileName, wxBITMAP_TYPE_ICO)); } #else - SetIcon(wxIcon(Slic3r::var("PrusaSlicer_128px.png"), wxBITMAP_TYPE_PNG)); +#if ENABLE_GCODE_VIEWER + switch (wxGetApp().get_app_mode()) + { + default: + case GUI_App::EAppMode::Editor: + { +#endif // ENABLE_GCODE_VIEWER + SetIcon(wxIcon(Slic3r::var("PrusaSlicer_128px.png"), wxBITMAP_TYPE_PNG)); +#if ENABLE_GCODE_VIEWER + break; + } + case GUI_App::EAppMode::GCodeViewer: + { + SetIcon(wxIcon(Slic3r::var("PrusaSlicer-gcodeviewer_128px.png"), wxBITMAP_TYPE_PNG)); + break; + } + } +#endif // ENABLE_GCODE_VIEWER #endif // _WIN32 // initialize status bar - m_statusbar = std::make_shared(this); + m_statusbar = std::make_shared(this); m_statusbar->set_font(GUI::wxGetApp().normal_font()); - m_statusbar->embed(this); - m_statusbar->set_status_text(_(L("Version")) + " " + - SLIC3R_VERSION + - _(L(" - Remember to check for updates at http://github.com/prusa3d/PrusaSlicer/releases"))); - - /* Load default preset bitmaps before a tabpanel initialization, - * but after filling of an em_unit value - */ - wxGetApp().preset_bundle->load_default_preset_bitmaps(); +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor()) +#endif // ENABLE_GCODE_VIEWER + m_statusbar->embed(this); + m_statusbar->set_status_text(_L("Version") + " " + + SLIC3R_VERSION + + _L(" - Remember to check for updates at https://github.com/prusa3d/PrusaSlicer/releases")); // initialize tabpanel and menubar init_tabpanel(); +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_gcode_viewer()) + init_menubar_as_gcodeviewer(); + else + init_menubar_as_editor(); + +#if _WIN32 + // This is needed on Windows to fake the CTRL+# of the window menu when using the numpad + wxAcceleratorEntry entries[6]; + entries[0].Set(wxACCEL_CTRL, WXK_NUMPAD1, wxID_HIGHEST + 1); + entries[1].Set(wxACCEL_CTRL, WXK_NUMPAD2, wxID_HIGHEST + 2); + entries[2].Set(wxACCEL_CTRL, WXK_NUMPAD3, wxID_HIGHEST + 3); + entries[3].Set(wxACCEL_CTRL, WXK_NUMPAD4, wxID_HIGHEST + 4); + entries[4].Set(wxACCEL_CTRL, WXK_NUMPAD5, wxID_HIGHEST + 5); + entries[5].Set(wxACCEL_CTRL, WXK_NUMPAD6, wxID_HIGHEST + 6); + wxAcceleratorTable accel(6, entries); + SetAcceleratorTable(accel); +#endif // _WIN32 +#else init_menubar(); +#endif // ENABLE_GCODE_VIEWER // set default tooltip timer in msec // SetAutoPop supposedly accepts long integers but some bug doesn't allow for larger values @@ -124,43 +157,18 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S m_loaded = true; -#if !ENABLE_LAYOUT_NO_RESTART -#ifdef __APPLE__ - // Using SetMinSize() on Mac messes up the window position in some cases - // cf. https://groups.google.com/forum/#!topic/wx-users/yUKPBBfXWO0 - // So, if we haven't possibility to set MinSize() for the MainFrame, - // set the MinSize() as a half of regular for the m_plater and m_tabpanel, when settings layout is in slNew mode - // Otherwise, MainFrame will be maximized by height - if (slNew) { - wxSize size = wxGetApp().get_min_size(); - size.SetHeight(int(0.5*size.GetHeight())); - m_plater->SetMinSize(size); - m_tabpanel->SetMinSize(size); - } -#endif -#endif // !ENABLE_LAYOUT_NO_RESTART - // initialize layout m_main_sizer = new wxBoxSizer(wxVERTICAL); wxSizer* sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(m_main_sizer, 1, wxEXPAND); -#if ENABLE_LAYOUT_NO_RESTART SetSizer(sizer); // initialize layout from config - update_layout(); +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor()) +#endif // ENABLE_GCODE_VIEWER + update_layout(); sizer->SetSizeHints(this); Fit(); -#else - if (m_plater && m_layout != slOld) - sizer->Add(m_plater, 1, wxEXPAND); - - if (m_tabpanel && m_layout != slDlg) - sizer->Add(m_tabpanel, 1, wxEXPAND); - - sizer->SetSizeHints(this); - SetSizer(sizer); - Fit(); -#endif // !ENABLE_LAYOUT_NO_RESTART const wxSize min_size = wxGetApp().get_min_size(); //wxSize(76*wxGetApp().em_unit(), 49*wxGetApp().em_unit()); #ifdef __APPLE__ @@ -252,20 +260,25 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S }); wxGetApp().persist_window_geometry(this, true); -#if ENABLE_LAYOUT_NO_RESTART wxGetApp().persist_window_geometry(&m_settings_dialog, true); -#else - if (m_settings_dialog != nullptr) - wxGetApp().persist_window_geometry(m_settings_dialog, true); -#endif // ENABLE_LAYOUT_NO_RESTART update_ui_from_settings(); // FIXME (?) - if (m_plater != nullptr) + if (m_plater != nullptr) { +#if ENABLE_GCODE_VIEWER + m_plater->get_collapse_toolbar().set_enabled(wxGetApp().app_config->get("show_collapse_button") == "1"); +#endif // ENABLE_GCODE_VIEWER m_plater->show_action_buttons(true); + } } -#if ENABLE_LAYOUT_NO_RESTART +#if ENABLE_GCODE_VIEWER_TASKBAR_ICON +MainFrame::~MainFrame() +{ + delete m_taskbar_icon; +} +#endif // ENABLE_GCODE_VIEWER_TASKBAR_ICON + void MainFrame::update_layout() { auto restore_to_creation = [this]() { @@ -292,9 +305,6 @@ void MainFrame::update_layout() m_plater_page = nullptr; } - if (m_layout == ESettingsLayout::Dlg) - rescale_dialog_after_dpi_change(*this, m_settings_dialog, ERescaleTarget::Mainframe); - clean_sizer(m_main_sizer); clean_sizer(m_settings_dialog.GetSizer()); @@ -307,9 +317,16 @@ void MainFrame::update_layout() Layout(); }; +#if ENABLE_GCODE_VIEWER + ESettingsLayout layout = wxGetApp().is_gcode_viewer() ? ESettingsLayout::GCodeViewer : + (wxGetApp().app_config->get("old_settings_layout_mode") == "1" ? ESettingsLayout::Old : + wxGetApp().app_config->get("new_settings_layout_mode") == "1" ? ESettingsLayout::New : + wxGetApp().app_config->get("dlg_settings_layout_mode") == "1" ? ESettingsLayout::Dlg : ESettingsLayout::Old); +#else ESettingsLayout layout = wxGetApp().app_config->get("old_settings_layout_mode") == "1" ? ESettingsLayout::Old : wxGetApp().app_config->get("new_settings_layout_mode") == "1" ? ESettingsLayout::New : wxGetApp().app_config->get("dlg_settings_layout_mode") == "1" ? ESettingsLayout::Dlg : ESettingsLayout::Old; +#endif // ENABLE_GCODE_VIEWER if (m_layout == layout) return; @@ -322,6 +339,16 @@ void MainFrame::update_layout() if (m_layout != ESettingsLayout::Unknown) restore_to_creation(); +#ifdef __WXMSW__ + enum class State { + noUpdate, + fromDlg, + toDlg + }; + State update_scaling_state = m_layout == ESettingsLayout::Dlg ? State::fromDlg : + layout == ESettingsLayout::Dlg ? State::toDlg : State::noUpdate; +#endif //__WXMSW__ + m_layout = layout; // From the very beginning the Print settings should be selected @@ -330,6 +357,10 @@ void MainFrame::update_layout() // Set new settings switch (m_layout) { + case ESettingsLayout::Unknown: + { + break; + } case ESettingsLayout::Old: { m_plater->Reparent(m_tabpanel); @@ -354,14 +385,52 @@ void MainFrame::update_layout() m_main_sizer->Add(m_plater, 1, wxEXPAND); m_tabpanel->Reparent(&m_settings_dialog); m_settings_dialog.GetSizer()->Add(m_tabpanel, 1, wxEXPAND); - - rescale_dialog_after_dpi_change(*this, m_settings_dialog, ERescaleTarget::SettingsDialog); - m_tabpanel->Show(); m_plater->Show(); break; } +#if ENABLE_GCODE_VIEWER + case ESettingsLayout::GCodeViewer: + { + m_main_sizer->Add(m_plater, 1, wxEXPAND); + m_plater->set_bed_shape({ { 0.0, 0.0 }, { 200.0, 0.0 }, { 200.0, 200.0 }, { 0.0, 200.0 } }, "", "", true); + m_plater->get_collapse_toolbar().set_enabled(false); + m_plater->collapse_sidebar(true); + m_plater->Show(); + break; } +#endif // ENABLE_GCODE_VIEWER + } + +#ifdef __WXMSW__ + if (update_scaling_state != State::noUpdate) + { + int mainframe_dpi = get_dpi_for_window(this); + int dialog_dpi = get_dpi_for_window(&m_settings_dialog); + if (mainframe_dpi != dialog_dpi) { + wxSize oldDPI = update_scaling_state == State::fromDlg ? wxSize(dialog_dpi, dialog_dpi) : wxSize(mainframe_dpi, mainframe_dpi); + wxSize newDPI = update_scaling_state == State::toDlg ? wxSize(dialog_dpi, dialog_dpi) : wxSize(mainframe_dpi, mainframe_dpi); + + if (update_scaling_state == State::fromDlg) + this->enable_force_rescale(); + else + (&m_settings_dialog)->enable_force_rescale(); + + wxWindow* win { nullptr }; + if (update_scaling_state == State::fromDlg) + win = this; + else + win = &m_settings_dialog; + +#if wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) + m_tabpanel->MSWUpdateOnDPIChange(oldDPI, newDPI); + win->GetEventHandler()->AddPendingEvent(wxDPIChangedEvent(oldDPI, newDPI)); +#else + win->GetEventHandler()->AddPendingEvent(DpiChangedEvent(EVT_DPI_CHANGED_SLICER, newDPI, win->GetRect())); +#endif // wxVERSION_EQUAL_OR_GREATER_THAN + } + } +#endif //__WXMSW__ //#ifdef __APPLE__ // // Using SetMinSize() on Mac messes up the window position in some cases @@ -380,7 +449,6 @@ void MainFrame::update_layout() Layout(); Thaw(); } -#endif // ENABLE_LAYOUT_NO_RESTART // Called when closing the application and when switching the application language. void MainFrame::shutdown() @@ -396,6 +464,20 @@ void MainFrame::shutdown() } #endif // _WIN32 +#if ENABLE_GCODE_VIEWER + if (m_plater != nullptr) { + m_plater->stop_jobs(); + + // 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, + // causing a crash + m_plater->unbind_canvas_event_handlers(); + + // Cleanup of canvases' volumes needs to be done here or a crash may happen on some Linux Debian flavours + // see: https://github.com/prusa3d/PrusaSlicer/issues/3964 + m_plater->reset_canvas_volumes(); + } +#else if (m_plater) m_plater->stop_jobs(); @@ -407,6 +489,7 @@ void MainFrame::shutdown() // Cleanup of canvases' volumes needs to be done here or a crash may happen on some Linux Debian flavours // see: https://github.com/prusa3d/PrusaSlicer/issues/3964 if (m_plater) m_plater->reset_canvas_volumes(); +#endif // ENABLE_GCODE_VIEWER // Weird things happen as the Paint messages are floating around the windows being destructed. // Avoid the Paint messages by hiding the main window. @@ -414,26 +497,17 @@ void MainFrame::shutdown() // In addition, there were some crashes due to the Paint events sent to already destructed windows. this->Show(false); -#if ENABLE_LAYOUT_NO_RESTART if (m_settings_dialog.IsShown()) // call Close() to trigger call to lambda defined into GUI_App::persist_window_geometry() m_settings_dialog.Close(); -#else - if (m_settings_dialog != nullptr) - { - if (m_settings_dialog->IsShown()) - // call Close() to trigger call to lambda defined into GUI_App::persist_window_geometry() - m_settings_dialog->Close(); - m_settings_dialog->Destroy(); + if (m_plater != nullptr) { + // Stop the background thread (Windows and Linux). + // Disconnect from a 3DConnextion driver (OSX). + m_plater->get_mouse3d_controller().shutdown(); + // Store the device parameter database back to appconfig. + m_plater->get_mouse3d_controller().save_config(*wxGetApp().app_config); } -#endif // ENABLE_LAYOUT_NO_RESTART - - // Stop the background thread (Windows and Linux). - // Disconnect from a 3DConnextion driver (OSX). - m_plater->get_mouse3d_controller().shutdown(); - // Store the device parameter database back to appconfig. - m_plater->get_mouse3d_controller().save_config(*wxGetApp().app_config); // Stop the background thread of the removable drive manager, so that no new updates will be sent to the Plater. wxGetApp().removable_drive_manager()->shutdown(); @@ -455,8 +529,7 @@ void MainFrame::shutdown() void MainFrame::update_title() { wxString title = wxEmptyString; - if (m_plater != nullptr) - { + if (m_plater != nullptr) { // m_plater->get_project_filename() produces file name including path, but excluding extension. // Don't try to remove the extension, it would remove part of the file name after the last dot! wxString project = from_path(into_path(m_plater->get_project_filename()).filename()); @@ -464,7 +537,11 @@ void MainFrame::update_title() title += (project + " - "); } +#if ENABLE_GCODE_VIEWER + std::string build_id = wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID; +#else std::string build_id = SLIC3R_BUILD_ID; +#endif // ENABLE_GCODE_VIEWER size_t idx_plus = build_id.find('+'); if (idx_plus != build_id.npos) { // Parse what is behind the '+'. If there is a number, then it is a build number after the label, and full build ID is shown. @@ -479,14 +556,19 @@ void MainFrame::update_title() #endif } } - title += (wxString(build_id) + " " + _(L("based on Slic3r"))); +#if ENABLE_GCODE_VIEWER + title += wxString(build_id); + if (wxGetApp().is_editor()) + title += (" " + _L("based on Slic3r")); +#else + title += (wxString(build_id) + " " + _L("based on Slic3r")); +#endif // ENABLE_GCODE_VIEWER SetTitle(title); } void MainFrame::init_tabpanel() { -#if ENABLE_LAYOUT_NO_RESTART // wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on Windows 10 // with multiple high resolution displays connected. m_tabpanel = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); @@ -495,27 +577,6 @@ void MainFrame::init_tabpanel() #endif m_tabpanel->Hide(); m_settings_dialog.set_tabpanel(m_tabpanel); -#else - m_layout = wxGetApp().app_config->get("old_settings_layout_mode") == "1" ? slOld : - wxGetApp().app_config->get("new_settings_layout_mode") == "1" ? slNew : - wxGetApp().app_config->get("dlg_settings_layout_mode") == "1" ? slDlg : slOld; - - // From the very beginning the Print settings should be selected - m_last_selected_tab = m_layout == slDlg ? 0 : 1; - - if (m_layout == slDlg) { - m_settings_dialog = new SettingsDialog(this); - m_tabpanel = m_settings_dialog->get_tabpanel(); - } - else { - // wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on Windows 10 - // with multiple high resolution displays connected. - m_tabpanel = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); -#ifndef __WXOSX__ // Don't call SetFont under OSX to avoid name cutting in ObjectList - m_tabpanel->SetFont(Slic3r::GUI::wxGetApp().normal_font()); -#endif - } -#endif // ENABLE_LAYOUT_NO_RESTART m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, [this](wxEvent&) { wxWindow* panel = m_tabpanel->GetCurrentPage(); @@ -536,20 +597,9 @@ void MainFrame::init_tabpanel() select_tab(0); // select Plater }); -#if ENABLE_LAYOUT_NO_RESTART m_plater = new Plater(this, this); m_plater->Hide(); -#else - if (m_layout == slOld) { - m_plater = new Plater(m_tabpanel, this); - m_tabpanel->AddPage(m_plater, _L("Plater")); - } - else { - m_plater = new Plater(this, this); - if (m_layout == slNew) - m_tabpanel->AddPage(new wxPanel(m_tabpanel), _L("Plater")); // empty panel just for Plater tab - } -#endif // ENABLE_LAYOUT_NO_RESTART + wxGetApp().plater_ = m_plater; wxGetApp().obj_list()->create_popup_menus(); @@ -561,7 +611,10 @@ void MainFrame::init_tabpanel() // or when the preset's "modified" status changes. Bind(EVT_TAB_PRESETS_CHANGED, &MainFrame::on_presets_changed, this); // #ys_FIXME_to_delete - create_preset_tabs(); +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor()) +#endif // ENABLE_GCODE_VIEWER + create_preset_tabs(); if (m_plater) { // load initial config @@ -691,7 +744,6 @@ bool MainFrame::can_slice() const bool MainFrame::can_change_view() const { -#if ENABLE_LAYOUT_NO_RESTART switch (m_layout) { default: { return false; } @@ -701,16 +753,10 @@ bool MainFrame::can_change_view() const int page_id = m_tabpanel->GetSelection(); return page_id != wxNOT_FOUND && dynamic_cast(m_tabpanel->GetPage((size_t)page_id)) != nullptr; } +#if ENABLE_GCODE_VIEWER + case ESettingsLayout::GCodeViewer: { return true; } +#endif // ENABLE_GCODE_VIEWER } -#else - if (m_layout == slNew) - return m_plater->IsShown(); - if (m_layout == slDlg) - return true; - // slOld layout mode - int page_id = m_tabpanel->GetSelection(); - return page_id != wxNOT_FOUND && dynamic_cast(m_tabpanel->GetPage((size_t)page_id)) != nullptr; -#endif // ENABLE_LAYOUT_NO_RESTART } bool MainFrame::can_select() const @@ -747,20 +793,11 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect) #endif // ENABLE_WX_3_1_3_DPI_CHANGED_EVENT this->SetFont(this->normal_font()); - /* Load default preset bitmaps before a tabpanel initialization, - * but after filling of an em_unit value - */ - wxGetApp().preset_bundle->load_default_preset_bitmaps(); - // update Plater wxGetApp().plater()->msw_rescale(); // update Tabs -#if ENABLE_LAYOUT_NO_RESTART if (m_layout != ESettingsLayout::Dlg) // Do not update tabs if the Settings are in the separated dialog -#else - if (m_layout != slDlg) // Update tabs later, from the SettingsDialog, when the Settings are in the separated dialog -#endif // ENABLE_LAYOUT_NO_RESTART for (auto tab : wxGetApp().tabs_list) tab->msw_rescale(); @@ -788,11 +825,6 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect) this->SetSize(sz); this->Maximize(is_maximized); - -#if ENABLE_LAYOUT_NO_RESTART - if (m_layout == ESettingsLayout::Dlg) - rescale_dialog_after_dpi_change(*this, m_settings_dialog, ERescaleTarget::SettingsDialog); -#endif // ENABLE_LAYOUT_NO_RESTART } void MainFrame::on_sys_color_changed() @@ -802,8 +834,6 @@ void MainFrame::on_sys_color_changed() // update label colors in respect to the system mode wxGetApp().init_label_colours(); - wxGetApp().preset_bundle->load_default_preset_bitmaps(); - // update Plater wxGetApp().plater()->sys_color_changed(); @@ -817,7 +847,89 @@ void MainFrame::on_sys_color_changed() msw_rescale_menu(menu_bar->GetMenu(id)); } +#if ENABLE_GCODE_VIEWER +#ifdef _MSC_VER + // \xA0 is a non-breaking space. It is entered here to spoil the automatic accelerators, + // as the simple numeric accelerators spoil all numeric data entry. +static const wxString sep = "\t\xA0"; +static const wxString sep_space = "\xA0"; +#else +static const wxString sep = " - "; +static const wxString sep_space = ""; +#endif + +static wxMenu* generate_help_menu() +{ + wxMenu* helpMenu = new wxMenu(); + append_menu_item(helpMenu, wxID_ANY, _L("Prusa 3D &Drivers"), _L("Open the Prusa3D drivers download page in your browser"), + [](wxCommandEvent&) { wxGetApp().open_web_page_localized("https://www.prusa3d.com/downloads"); }); + append_menu_item(helpMenu, wxID_ANY, _(L("Software &Releases")), _(L("Open the software releases page in your browser")), + [](wxCommandEvent&) { wxLaunchDefaultBrowser("https://github.com/prusa3d/PrusaSlicer/releases"); }); +//# my $versioncheck = $self->_append_menu_item($helpMenu, "Check for &Updates...", "Check for new Slic3r versions", sub{ +//# wxTheApp->check_version(1); +//# }); +//# $versioncheck->Enable(wxTheApp->have_version_check); + append_menu_item(helpMenu, wxID_ANY, wxString::Format(_L("%s &Website"), SLIC3R_APP_NAME), + wxString::Format(_L("Open the %s website in your browser"), SLIC3R_APP_NAME), + [](wxCommandEvent&) { wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); }); +// append_menu_item(helpMenu, wxID_ANY, wxString::Format(_(L("%s &Manual")), SLIC3R_APP_NAME), +// wxString::Format(_(L("Open the %s manual in your browser")), SLIC3R_APP_NAME), +// [this](wxCommandEvent&) { wxLaunchDefaultBrowser("http://manual.slic3r.org/"); }); + helpMenu->AppendSeparator(); + append_menu_item(helpMenu, wxID_ANY, _L("System &Info"), _L("Show system information"), + [](wxCommandEvent&) { wxGetApp().system_info(); }); + append_menu_item(helpMenu, wxID_ANY, _L("Show &Configuration Folder"), _L("Show user configuration folder (datadir)"), + [](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); }); + append_menu_item(helpMenu, wxID_ANY, _(L"Report an I&ssue"), wxString::Format(_L("Report an issue on %s"), SLIC3R_APP_NAME), + [](wxCommandEvent&) { wxLaunchDefaultBrowser("https://github.com/prusa3d/slic3r/issues/new"); }); +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor()) +#endif // ENABLE_GCODE_VIEWER + append_menu_item(helpMenu, wxID_ANY, wxString::Format(_L("&About %s"), SLIC3R_APP_NAME), _L("Show about dialog"), + [](wxCommandEvent&) { Slic3r::GUI::about(); }); +#if ENABLE_GCODE_VIEWER + else + append_menu_item(helpMenu, wxID_ANY, wxString::Format(_L("&About %s"), GCODEVIEWER_APP_NAME), _L("Show about dialog"), + [](wxCommandEvent&) { Slic3r::GUI::about(); }); +#endif // ENABLE_GCODE_VIEWER + helpMenu->AppendSeparator(); + append_menu_item(helpMenu, wxID_ANY, _L("Keyboard Shortcuts") + sep + "&?", _L("Show the list of the keyboard shortcuts"), + [](wxCommandEvent&) { wxGetApp().keyboard_shortcuts(); }); +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG + helpMenu->AppendSeparator(); + append_menu_item(helpMenu, wxID_ANY, "DEBUG gcode thumbnails", "DEBUG ONLY - read the selected gcode file and generates png for the contained thumbnails", + [](wxCommandEvent&) { wxGetApp().gcode_thumbnails_debug(); }); +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + + return helpMenu; +} + +static void add_common_view_menu_items(wxMenu* view_menu, MainFrame* mainFrame, std::function can_change_view) +{ + // The camera control accelerators are captured by GLCanvas3D::on_char(). + append_menu_item(view_menu, wxID_ANY, _L("Iso") + sep + "&0", _L("Iso View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("iso"); }, + "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); + view_menu->AppendSeparator(); + //TRN To be shown in the main menu View->Top + append_menu_item(view_menu, wxID_ANY, _L("Top") + sep + "&1", _L("Top View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("top"); }, + "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); + //TRN To be shown in the main menu View->Bottom + append_menu_item(view_menu, wxID_ANY, _L("Bottom") + sep + "&2", _L("Bottom View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("bottom"); }, + "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); + append_menu_item(view_menu, wxID_ANY, _L("Front") + sep + "&3", _L("Front View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("front"); }, + "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); + append_menu_item(view_menu, wxID_ANY, _L("Rear") + sep + "&4", _L("Rear View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("rear"); }, + "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); + append_menu_item(view_menu, wxID_ANY, _L("Left") + sep + "&5", _L("Left View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("left"); }, + "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); + append_menu_item(view_menu, wxID_ANY, _L("Right") + sep + "&6", _L("Right View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("right"); }, + "", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame); +} + +void MainFrame::init_menubar_as_editor() +#else void MainFrame::init_menubar() +#endif // ENABLE_GCODE_VIEWER { #ifdef __APPLE__ wxMenuBar::SetAutoWindowMenu(false); @@ -826,15 +938,15 @@ void MainFrame::init_menubar() // File menu wxMenu* fileMenu = new wxMenu; { - append_menu_item(fileMenu, wxID_ANY, _(L("&New Project")) + "\tCtrl+N", _(L("Start a new project")), + append_menu_item(fileMenu, wxID_ANY, _L("&New Project") + "\tCtrl+N", _L("Start a new project"), [this](wxCommandEvent&) { if (m_plater) m_plater->new_project(); }, "", nullptr, [this](){return m_plater != nullptr && can_start_new_project(); }, this); - append_menu_item(fileMenu, wxID_ANY, _(L("&Open Project")) + dots + "\tCtrl+O", _(L("Open a project file")), + append_menu_item(fileMenu, wxID_ANY, _L("&Open Project") + dots + "\tCtrl+O", _L("Open a project file"), [this](wxCommandEvent&) { if (m_plater) m_plater->load_project(); }, "open", nullptr, [this](){return m_plater != nullptr; }, this); wxMenu* recent_projects_menu = new wxMenu(); - wxMenuItem* recent_projects_submenu = append_submenu(fileMenu, recent_projects_menu, wxID_ANY, _(L("Recent projects")), ""); + wxMenuItem* recent_projects_submenu = append_submenu(fileMenu, recent_projects_menu, wxID_ANY, _L("Recent projects"), ""); m_recent_projects.UseMenu(recent_projects_menu); Bind(wxEVT_MENU, [this](wxCommandEvent& evt) { size_t file_id = evt.GetId() - wxID_FILE1; @@ -843,7 +955,7 @@ void MainFrame::init_menubar() m_plater->load_project(filename); else { - wxMessageDialog msg(this, _(L("The selected project is no longer available.\nDo you want to remove it from the recent projects list?")), _(L("Error")), wxYES_NO | wxYES_DEFAULT); + wxMessageDialog msg(this, _L("The selected project is no longer available.\nDo you want to remove it from the recent projects list?"), _L("Error"), wxYES_NO | wxYES_DEFAULT); if (msg.ShowModal() == wxID_YES) { m_recent_projects.RemoveFileFromHistory(file_id); @@ -868,13 +980,13 @@ void MainFrame::init_menubar() Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(m_recent_projects.GetCount() > 0); }, recent_projects_submenu->GetId()); - append_menu_item(fileMenu, wxID_ANY, _(L("&Save Project")) + "\tCtrl+S", _(L("Save current project file")), + append_menu_item(fileMenu, wxID_ANY, _L("&Save Project") + "\tCtrl+S", _L("Save current project file"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_3mf(into_path(m_plater->get_project_filename(".3mf"))); }, "save", nullptr, [this](){return m_plater != nullptr && can_save(); }, this); #ifdef __APPLE__ - append_menu_item(fileMenu, wxID_ANY, _(L("Save Project &as")) + dots + "\tCtrl+Shift+S", _(L("Save current project file as")), + append_menu_item(fileMenu, wxID_ANY, _L("Save Project &as") + dots + "\tCtrl+Shift+S", _L("Save current project file as"), #else - append_menu_item(fileMenu, wxID_ANY, _(L("Save Project &as")) + dots + "\tCtrl+Alt+S", _(L("Save current project file as")), + append_menu_item(fileMenu, wxID_ANY, _L("Save Project &as") + dots + "\tCtrl+Alt+S", _L("Save current project file as"), #endif // __APPLE__ [this](wxCommandEvent&) { if (m_plater) m_plater->export_3mf(); }, "save", nullptr, [this](){return m_plater != nullptr && can_save(); }, this); @@ -882,7 +994,7 @@ void MainFrame::init_menubar() fileMenu->AppendSeparator(); wxMenu* import_menu = new wxMenu(); - append_menu_item(import_menu, wxID_ANY, _(L("Import STL/OBJ/AM&F/3MF")) + dots + "\tCtrl+I", _(L("Load a model")), + append_menu_item(import_menu, wxID_ANY, _L("Import STL/OBJ/AM&F/3MF") + dots + "\tCtrl+I", _L("Load a model"), [this](wxCommandEvent&) { if (m_plater) m_plater->add_model(); }, "import_plater", nullptr, [this](){return m_plater != nullptr; }, this); @@ -890,59 +1002,59 @@ void MainFrame::init_menubar() [this](wxCommandEvent&) { if (m_plater) m_plater->add_model(true); }, "import_plater", nullptr, [this](){return m_plater != nullptr; }, this); - append_menu_item(import_menu, wxID_ANY, _(L("Import SL1 archive")) + dots, _(L("Load an SL1 output archive")), + append_menu_item(import_menu, wxID_ANY, _L("Import SL1 archive") + dots, _L("Load an SL1 output archive"), [this](wxCommandEvent&) { if (m_plater) m_plater->import_sl1_archive(); }, "import_plater", nullptr, [this](){return m_plater != nullptr; }, this); import_menu->AppendSeparator(); - append_menu_item(import_menu, wxID_ANY, _(L("Import &Config")) + dots + "\tCtrl+L", _(L("Load exported configuration file")), + append_menu_item(import_menu, wxID_ANY, _L("Import &Config") + dots + "\tCtrl+L", _L("Load exported configuration file"), [this](wxCommandEvent&) { load_config_file(); }, "import_config", nullptr, [this]() {return true; }, this); - append_menu_item(import_menu, wxID_ANY, _(L("Import Config from &project")) + dots +"\tCtrl+Alt+L", _(L("Load configuration from project file")), + append_menu_item(import_menu, wxID_ANY, _L("Import Config from &project") + dots +"\tCtrl+Alt+L", _L("Load configuration from project file"), [this](wxCommandEvent&) { if (m_plater) m_plater->extract_config_from_project(); }, "import_config", nullptr, [this]() {return true; }, this); import_menu->AppendSeparator(); - append_menu_item(import_menu, wxID_ANY, _(L("Import Config &Bundle")) + dots, _(L("Load presets from a bundle")), + append_menu_item(import_menu, wxID_ANY, _L("Import Config &Bundle") + dots, _L("Load presets from a bundle"), [this](wxCommandEvent&) { load_configbundle(); }, "import_config_bundle", nullptr, [this]() {return true; }, this); - append_submenu(fileMenu, import_menu, wxID_ANY, _(L("&Import")), ""); + append_submenu(fileMenu, import_menu, wxID_ANY, _L("&Import"), ""); wxMenu* export_menu = new wxMenu(); - wxMenuItem* item_export_gcode = append_menu_item(export_menu, wxID_ANY, _(L("Export &G-code")) + dots +"\tCtrl+G", _(L("Export current plate as G-code")), - [this](wxCommandEvent&) { if (m_plater) m_plater->export_gcode(); }, "export_gcode", nullptr, + wxMenuItem* item_export_gcode = append_menu_item(export_menu, wxID_ANY, _L("Export &G-code") + dots +"\tCtrl+G", _L("Export current plate as G-code"), + [this](wxCommandEvent&) { if (m_plater) m_plater->export_gcode(false); }, "export_gcode", nullptr, [this](){return can_export_gcode(); }, this); m_changeable_menu_items.push_back(item_export_gcode); - wxMenuItem* item_send_gcode = append_menu_item(export_menu, wxID_ANY, _(L("S&end G-code")) + dots +"\tCtrl+Shift+G", _(L("Send to print current plate as G-code")), + wxMenuItem* item_send_gcode = append_menu_item(export_menu, wxID_ANY, _L("S&end G-code") + dots +"\tCtrl+Shift+G", _L("Send to print current plate as G-code"), [this](wxCommandEvent&) { if (m_plater) m_plater->send_gcode(); }, "export_gcode", nullptr, [this](){return can_send_gcode(); }, this); m_changeable_menu_items.push_back(item_send_gcode); - append_menu_item(export_menu, wxID_ANY, _(L("Export G-code to SD card / Flash drive")) + dots + "\tCtrl+U", _(L("Export current plate as G-code to SD card / Flash drive")), + append_menu_item(export_menu, wxID_ANY, _L("Export G-code to SD card / Flash drive") + dots + "\tCtrl+U", _L("Export current plate as G-code to SD card / Flash drive"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_gcode(true); }, "export_to_sd", nullptr, [this]() {return can_export_gcode_sd(); }, this); export_menu->AppendSeparator(); - append_menu_item(export_menu, wxID_ANY, _(L("Export plate as &STL")) + dots, _(L("Export current plate as STL")), + append_menu_item(export_menu, wxID_ANY, _L("Export plate as &STL") + dots, _L("Export current plate as STL"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_stl(); }, "export_plater", nullptr, [this](){return can_export_model(); }, this); - append_menu_item(export_menu, wxID_ANY, _(L("Export plate as STL &including supports")) + dots, _(L("Export current plate as STL including supports")), + append_menu_item(export_menu, wxID_ANY, _L("Export plate as STL &including supports") + dots, _L("Export current plate as STL including supports"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_stl(true); }, "export_plater", nullptr, [this](){return can_export_supports(); }, this); - append_menu_item(export_menu, wxID_ANY, _(L("Export plate as &AMF")) + dots, _(L("Export current plate as AMF")), + append_menu_item(export_menu, wxID_ANY, _L("Export plate as &AMF") + dots, _L("Export current plate as AMF"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_amf(); }, "export_plater", nullptr, [this](){return can_export_model(); }, this); export_menu->AppendSeparator(); - append_menu_item(export_menu, wxID_ANY, _(L("Export &toolpaths as OBJ")) + dots, _(L("Export toolpaths as OBJ")), + append_menu_item(export_menu, wxID_ANY, _L("Export &toolpaths as OBJ") + dots, _L("Export toolpaths as OBJ"), [this](wxCommandEvent&) { if (m_plater) m_plater->export_toolpaths_to_obj(); }, "export_plater", nullptr, [this]() {return can_export_toolpaths(); }, this); export_menu->AppendSeparator(); - append_menu_item(export_menu, wxID_ANY, _(L("Export &Config")) +dots +"\tCtrl+E", _(L("Export current configuration to file")), + append_menu_item(export_menu, wxID_ANY, _L("Export &Config") + dots +"\tCtrl+E", _L("Export current configuration to file"), [this](wxCommandEvent&) { export_config(); }, "export_config", nullptr, [this]() {return true; }, this); - append_menu_item(export_menu, wxID_ANY, _(L("Export Config &Bundle")) + dots, _(L("Export all presets to file")), + append_menu_item(export_menu, wxID_ANY, _L("Export Config &Bundle") + dots, _L("Export all presets to file"), [this](wxCommandEvent&) { export_configbundle(); }, "export_config_bundle", nullptr, [this]() {return true; }, this); - append_submenu(fileMenu, export_menu, wxID_ANY, _(L("&Export")), ""); + append_submenu(fileMenu, export_menu, wxID_ANY, _L("&Export"), ""); - append_menu_item(fileMenu, wxID_ANY, _(L("Ejec&t SD card / Flash drive")) + dots + "\tCtrl+T", _(L("Eject SD card / Flash drive after the G-code was exported to it.")), + append_menu_item(fileMenu, wxID_ANY, _L("Ejec&t SD card / Flash drive") + dots + "\tCtrl+T", _L("Eject SD card / Flash drive after the G-code was exported to it."), [this](wxCommandEvent&) { if (m_plater) m_plater->eject_drive(); }, "eject_sd", nullptr, [this]() {return can_eject(); }, this); @@ -950,19 +1062,19 @@ void MainFrame::init_menubar() #if 0 m_menu_item_repeat = nullptr; - append_menu_item(fileMenu, wxID_ANY, _(L("Quick Slice")) +dots+ "\tCtrl+U", _(L("Slice a file into a G-code")), + append_menu_item(fileMenu, wxID_ANY, _L("Quick Slice") +dots+ "\tCtrl+U", _L("Slice a file into a G-code"), [this](wxCommandEvent&) { wxTheApp->CallAfter([this]() { quick_slice(); m_menu_item_repeat->Enable(is_last_input_file()); }); }, "cog_go.png"); - append_menu_item(fileMenu, wxID_ANY, _(L("Quick Slice and Save As")) +dots +"\tCtrl+Alt+U", _(L("Slice a file into a G-code, save as")), + append_menu_item(fileMenu, wxID_ANY, _L("Quick Slice and Save As") +dots +"\tCtrl+Alt+U", _L("Slice a file into a G-code, save as"), [this](wxCommandEvent&) { wxTheApp->CallAfter([this]() { quick_slice(qsSaveAs); m_menu_item_repeat->Enable(is_last_input_file()); }); }, "cog_go.png"); - m_menu_item_repeat = append_menu_item(fileMenu, wxID_ANY, _(L("Repeat Last Quick Slice")) +"\tCtrl+Shift+U", _(L("Repeat last quick slice")), + m_menu_item_repeat = append_menu_item(fileMenu, wxID_ANY, _L("Repeat Last Quick Slice") +"\tCtrl+Shift+U", _L("Repeat last quick slice"), [this](wxCommandEvent&) { wxTheApp->CallAfter([this]() { quick_slice(qsReslice); @@ -970,18 +1082,22 @@ void MainFrame::init_menubar() m_menu_item_repeat->Enable(false); fileMenu->AppendSeparator(); #endif - m_menu_item_reslice_now = append_menu_item(fileMenu, wxID_ANY, _(L("(Re)Slice No&w")) + "\tCtrl+R", _(L("Start new slicing process")), + m_menu_item_reslice_now = append_menu_item(fileMenu, wxID_ANY, _L("(Re)Slice No&w") + "\tCtrl+R", _L("Start new slicing process"), [this](wxCommandEvent&) { reslice_now(); }, "re_slice", nullptr, - [this](){return m_plater != nullptr && can_reslice(); }, this); + [this]() { return m_plater != nullptr && can_reslice(); }, this); fileMenu->AppendSeparator(); - append_menu_item(fileMenu, wxID_ANY, _(L("&Repair STL file")) + dots, _(L("Automatically repair an STL file")), + append_menu_item(fileMenu, wxID_ANY, _L("&Repair STL file") + dots, _L("Automatically repair an STL file"), [this](wxCommandEvent&) { repair_stl(); }, "wrench", nullptr, - [this]() {return true; }, this); + [this]() { return true; }, this); fileMenu->AppendSeparator(); - append_menu_item(fileMenu, wxID_EXIT, _(L("&Quit")), wxString::Format(_(L("Quit %s")), SLIC3R_APP_NAME), + append_menu_item(fileMenu, wxID_ANY, _L("&G-code preview") + dots, _L("Open G-code viewer"), + [this](wxCommandEvent&) { start_new_gcodeviewer_open_file(this); }, "", nullptr); + fileMenu->AppendSeparator(); + append_menu_item(fileMenu, wxID_EXIT, _L("&Quit"), wxString::Format(_L("Quit %s"), SLIC3R_APP_NAME), [this](wxCommandEvent&) { Close(false); }); } +#if !ENABLE_GCODE_VIEWER #ifdef _MSC_VER // \xA0 is a non-breaking space. It is entered here to spoil the automatic accelerators, // as the simple numeric accelerators spoil all numeric data entry. @@ -991,6 +1107,7 @@ void MainFrame::init_menubar() wxString sep = " - "; wxString sep_space = ""; #endif +#endif // !ENABLE_GCODE_VIEWER // Edit menu wxMenu* editMenu = nullptr; @@ -1003,44 +1120,44 @@ void MainFrame::init_menubar() #else wxString hotkey_delete = "Del"; #endif - append_menu_item(editMenu, wxID_ANY, _(L("&Select all")) + sep + GUI::shortkey_ctrl_prefix() + sep_space + "A", - _(L("Selects all objects")), [this](wxCommandEvent&) { m_plater->select_all(); }, + append_menu_item(editMenu, wxID_ANY, _L("&Select all") + sep + GUI::shortkey_ctrl_prefix() + sep_space + "A", + _L("Selects all objects"), [this](wxCommandEvent&) { m_plater->select_all(); }, "", nullptr, [this](){return can_select(); }, this); - append_menu_item(editMenu, wxID_ANY, _(L("D&eselect all")) + sep + "Esc", - _(L("Deselects all objects")), [this](wxCommandEvent&) { m_plater->deselect_all(); }, + append_menu_item(editMenu, wxID_ANY, _L("D&eselect all") + sep + "Esc", + _L("Deselects all objects"), [this](wxCommandEvent&) { m_plater->deselect_all(); }, "", nullptr, [this](){return can_deselect(); }, this); editMenu->AppendSeparator(); - append_menu_item(editMenu, wxID_ANY, _(L("&Delete selected")) + sep + hotkey_delete, - _(L("Deletes the current selection")),[this](wxCommandEvent&) { m_plater->remove_selected(); }, + append_menu_item(editMenu, wxID_ANY, _L("&Delete selected") + sep + hotkey_delete, + _L("Deletes the current selection"),[this](wxCommandEvent&) { m_plater->remove_selected(); }, "remove_menu", nullptr, [this](){return can_delete(); }, this); - append_menu_item(editMenu, wxID_ANY, _(L("Delete &all")) + sep + GUI::shortkey_ctrl_prefix() + sep_space + hotkey_delete, - _(L("Deletes all objects")), [this](wxCommandEvent&) { m_plater->reset_with_confirm(); }, + append_menu_item(editMenu, wxID_ANY, _L("Delete &all") + sep + GUI::shortkey_ctrl_prefix() + sep_space + hotkey_delete, + _L("Deletes all objects"), [this](wxCommandEvent&) { m_plater->reset_with_confirm(); }, "delete_all_menu", nullptr, [this](){return can_delete_all(); }, this); editMenu->AppendSeparator(); - append_menu_item(editMenu, wxID_ANY, _(L("&Undo")) + sep + GUI::shortkey_ctrl_prefix() + sep_space + "Z", - _(L("Undo")), [this](wxCommandEvent&) { m_plater->undo(); }, + append_menu_item(editMenu, wxID_ANY, _L("&Undo") + sep + GUI::shortkey_ctrl_prefix() + sep_space + "Z", + _L("Undo"), [this](wxCommandEvent&) { m_plater->undo(); }, "undo_menu", nullptr, [this](){return m_plater->can_undo(); }, this); - append_menu_item(editMenu, wxID_ANY, _(L("&Redo")) + sep + GUI::shortkey_ctrl_prefix() + sep_space + "Y", - _(L("Redo")), [this](wxCommandEvent&) { m_plater->redo(); }, + append_menu_item(editMenu, wxID_ANY, _L("&Redo") + sep + GUI::shortkey_ctrl_prefix() + sep_space + "Y", + _L("Redo"), [this](wxCommandEvent&) { m_plater->redo(); }, "redo_menu", nullptr, [this](){return m_plater->can_redo(); }, this); editMenu->AppendSeparator(); - append_menu_item(editMenu, wxID_ANY, _(L("&Copy")) + sep + GUI::shortkey_ctrl_prefix() + sep_space + "C", - _(L("Copy selection to clipboard")), [this](wxCommandEvent&) { m_plater->copy_selection_to_clipboard(); }, + append_menu_item(editMenu, wxID_ANY, _L("&Copy") + sep + GUI::shortkey_ctrl_prefix() + sep_space + "C", + _L("Copy selection to clipboard"), [this](wxCommandEvent&) { m_plater->copy_selection_to_clipboard(); }, "copy_menu", nullptr, [this](){return m_plater->can_copy_to_clipboard(); }, this); - append_menu_item(editMenu, wxID_ANY, _(L("&Paste")) + sep + GUI::shortkey_ctrl_prefix() + sep_space + "V", - _(L("Paste clipboard")), [this](wxCommandEvent&) { m_plater->paste_from_clipboard(); }, + append_menu_item(editMenu, wxID_ANY, _L("&Paste") + sep + GUI::shortkey_ctrl_prefix() + sep_space + "V", + _L("Paste clipboard"), [this](wxCommandEvent&) { m_plater->paste_from_clipboard(); }, "paste_menu", nullptr, [this](){return m_plater->can_paste_from_clipboard(); }, this); editMenu->AppendSeparator(); - append_menu_item(editMenu, wxID_ANY, _(L("Re&load from disk")) + sep + "F5", - _(L("Reload the plater from disk")), [this](wxCommandEvent&) { m_plater->reload_all_from_disk(); }, + append_menu_item(editMenu, wxID_ANY, _L("Re&load from disk") + sep + "F5", + _L("Reload the plater from disk"), [this](wxCommandEvent&) { m_plater->reload_all_from_disk(); }, "", nullptr, [this]() {return !m_plater->model().objects.empty(); }, this); editMenu->AppendSeparator(); - append_menu_item(editMenu, wxID_ANY, _(L("Searc&h")) + "\tCtrl+F", - _(L("Find option")), [this](wxCommandEvent&) { m_plater->search(/*m_tabpanel->GetCurrentPage() == */m_plater->IsShown()); }, + append_menu_item(editMenu, wxID_ANY, _L("Searc&h") + "\tCtrl+F", + _L("Find option"), [this](wxCommandEvent&) { m_plater->search(/*m_tabpanel->GetCurrentPage() == */m_plater->IsShown()); }, "search", nullptr, [this]() {return true; }, this); } @@ -1048,32 +1165,33 @@ void MainFrame::init_menubar() auto windowMenu = new wxMenu(); { if (m_plater) { - append_menu_item(windowMenu, wxID_HIGHEST + 1, _(L("&Plater Tab")) + "\tCtrl+1", _(L("Show the plater")), + append_menu_item(windowMenu, wxID_HIGHEST + 1, _L("&Plater Tab") + "\tCtrl+1", _L("Show the plater"), [this](wxCommandEvent&) { select_tab(0); }, "plater", nullptr, [this]() {return true; }, this); windowMenu->AppendSeparator(); } - append_menu_item(windowMenu, wxID_HIGHEST + 2, _(L("P&rint Settings Tab")) + "\tCtrl+2", _(L("Show the print settings")), + append_menu_item(windowMenu, wxID_HIGHEST + 2, _L("P&rint Settings Tab") + "\tCtrl+2", _L("Show the print settings"), [this/*, tab_offset*/](wxCommandEvent&) { select_tab(1); }, "cog", nullptr, [this]() {return true; }, this); - wxMenuItem* item_material_tab = append_menu_item(windowMenu, wxID_HIGHEST + 3, _(L("&Filament Settings Tab")) + "\tCtrl+3", _(L("Show the filament settings")), + wxMenuItem* item_material_tab = append_menu_item(windowMenu, wxID_HIGHEST + 3, _L("&Filament Settings Tab") + "\tCtrl+3", _L("Show the filament settings"), [this/*, tab_offset*/](wxCommandEvent&) { select_tab(2); }, "spool", nullptr, [this]() {return true; }, this); m_changeable_menu_items.push_back(item_material_tab); - wxMenuItem* item_printer_tab = append_menu_item(windowMenu, wxID_HIGHEST + 4, _(L("Print&er Settings Tab")) + "\tCtrl+4", _(L("Show the printer settings")), + wxMenuItem* item_printer_tab = append_menu_item(windowMenu, wxID_HIGHEST + 4, _L("Print&er Settings Tab") + "\tCtrl+4", _L("Show the printer settings"), [this/*, tab_offset*/](wxCommandEvent&) { select_tab(3); }, "printer", nullptr, [this]() {return true; }, this); m_changeable_menu_items.push_back(item_printer_tab); if (m_plater) { windowMenu->AppendSeparator(); - append_menu_item(windowMenu, wxID_HIGHEST + 5, _(L("3&D")) + "\tCtrl+5", _(L("Show the 3D editing view")), + append_menu_item(windowMenu, wxID_HIGHEST + 5, _L("3&D") + "\tCtrl+5", _L("Show the 3D editing view"), [this](wxCommandEvent&) { m_plater->select_view_3D("3D"); }, "editor_menu", nullptr, [this](){return can_change_view(); }, this); - append_menu_item(windowMenu, wxID_HIGHEST + 6, _(L("Pre&view")) + "\tCtrl+6", _(L("Show the 3D slices preview")), + append_menu_item(windowMenu, wxID_HIGHEST + 6, _L("Pre&view") + "\tCtrl+6", _L("Show the 3D slices preview"), [this](wxCommandEvent&) { m_plater->select_view_3D("Preview"); }, "preview_menu", nullptr, [this](){return can_change_view(); }, this); } +#if !ENABLE_GCODE_VIEWER #if _WIN32 // This is needed on Windows to fake the CTRL+# of the window menu when using the numpad wxAcceleratorEntry entries[6]; @@ -1086,83 +1204,94 @@ void MainFrame::init_menubar() wxAcceleratorTable accel(6, entries); SetAcceleratorTable(accel); #endif // _WIN32 +#endif // !ENABLE_GCODE_VIEWER windowMenu->AppendSeparator(); - append_menu_item(windowMenu, wxID_ANY, _(L("Print &Host Upload Queue")) + "\tCtrl+J", _(L("Display the Print Host Upload Queue window")), - [this](wxCommandEvent&) { m_printhost_queue_dlg->Show(); }, "upload_queue", nullptr, - [this]() {return true; }, this); + append_menu_item(windowMenu, wxID_ANY, _L("Print &Host Upload Queue") + "\tCtrl+J", _L("Display the Print Host Upload Queue window"), + [this](wxCommandEvent&) { m_printhost_queue_dlg->Show(); }, "upload_queue", nullptr, [this]() {return true; }, this); + + windowMenu->AppendSeparator(); + append_menu_item(windowMenu, wxID_ANY, _(L("Open new instance")) + "\tCtrl+I", _(L("Open a new PrusaSlicer instance")), + [this](wxCommandEvent&) { start_new_slicer(); }, "", nullptr); } // View menu wxMenu* viewMenu = nullptr; if (m_plater) { viewMenu = new wxMenu(); +#if ENABLE_GCODE_VIEWER + add_common_view_menu_items(viewMenu, this, std::bind(&MainFrame::can_change_view, this)); +#else // The camera control accelerators are captured by GLCanvas3D::on_char(). - append_menu_item(viewMenu, wxID_ANY, _(L("Iso")) + sep + "&0", _(L("Iso View")),[this](wxCommandEvent&) { select_view("iso"); }, + append_menu_item(viewMenu, wxID_ANY, _L("Iso") + sep + "&0", _L("Iso View"), [this](wxCommandEvent&) { select_view("iso"); }, "", nullptr, [this](){return can_change_view(); }, this); viewMenu->AppendSeparator(); //TRN To be shown in the main menu View->Top - append_menu_item(viewMenu, wxID_ANY, _(L("Top")) + sep + "&1", _(L("Top View")), [this](wxCommandEvent&) { select_view("top"); }, + append_menu_item(viewMenu, wxID_ANY, _L("Top") + sep + "&1", _L("Top View"), [this](wxCommandEvent&) { select_view("top"); }, "", nullptr, [this](){return can_change_view(); }, this); //TRN To be shown in the main menu View->Bottom - append_menu_item(viewMenu, wxID_ANY, _(L("Bottom")) + sep + "&2", _(L("Bottom View")), [this](wxCommandEvent&) { select_view("bottom"); }, + append_menu_item(viewMenu, wxID_ANY, _L("Bottom") + sep + "&2", _L("Bottom View"), [this](wxCommandEvent&) { select_view("bottom"); }, "", nullptr, [this](){return can_change_view(); }, this); - append_menu_item(viewMenu, wxID_ANY, _(L("Front")) + sep + "&3", _(L("Front View")), [this](wxCommandEvent&) { select_view("front"); }, + append_menu_item(viewMenu, wxID_ANY, _L("Front") + sep + "&3", _L("Front View"), [this](wxCommandEvent&) { select_view("front"); }, "", nullptr, [this](){return can_change_view(); }, this); - append_menu_item(viewMenu, wxID_ANY, _(L("Rear")) + sep + "&4", _(L("Rear View")), [this](wxCommandEvent&) { select_view("rear"); }, + append_menu_item(viewMenu, wxID_ANY, _L("Rear") + sep + "&4", _L("Rear View"), [this](wxCommandEvent&) { select_view("rear"); }, "", nullptr, [this](){return can_change_view(); }, this); - append_menu_item(viewMenu, wxID_ANY, _(L("Left")) + sep + "&5", _(L("Left View")), [this](wxCommandEvent&) { select_view("left"); }, + append_menu_item(viewMenu, wxID_ANY, _L("Left") + sep + "&5", _L("Left View"), [this](wxCommandEvent&) { select_view("left"); }, "", nullptr, [this](){return can_change_view(); }, this); - append_menu_item(viewMenu, wxID_ANY, _(L("Right")) + sep + "&6", _(L("Right View")), [this](wxCommandEvent&) { select_view("right"); }, + append_menu_item(viewMenu, wxID_ANY, _L("Right") + sep + "&6", _L("Right View"), [this](wxCommandEvent&) { select_view("right"); }, "", nullptr, [this](){return can_change_view(); }, this); +#endif // ENABLE_GCODE_VIEWER viewMenu->AppendSeparator(); #if ENABLE_SLOPE_RENDERING wxMenu* options_menu = new wxMenu(); - append_menu_check_item(options_menu, wxID_ANY, _(L("Show &labels")) + sep + "E", _(L("Show object/instance labels in 3D scene")), + append_menu_check_item(options_menu, wxID_ANY, _L("Show &labels") + sep + "E", _L("Show object/instance labels in 3D scene"), [this](wxCommandEvent&) { m_plater->show_view3D_labels(!m_plater->are_view3D_labels_shown()); }, this, [this]() { return m_plater->is_view3D_shown(); }, [this]() { return m_plater->are_view3D_labels_shown(); }, this); - append_menu_check_item(options_menu, wxID_ANY, _(L("Show &slope")) + sep + "D", _(L("Objects coloring using faces' slope")), + append_menu_check_item(options_menu, wxID_ANY, _L("Show &slope") + sep + "D", _L("Objects coloring using faces' slope"), [this](wxCommandEvent&) { m_plater->show_view3D_slope(!m_plater->is_view3D_slope_shown()); }, this, [this]() { return m_plater->is_view3D_shown() && !m_plater->is_view3D_layers_editing_enabled(); }, [this]() { return m_plater->is_view3D_slope_shown(); }, this); - append_submenu(viewMenu, options_menu, wxID_ANY, _(L("&Options")), ""); + append_submenu(viewMenu, options_menu, wxID_ANY, _L("&Options"), ""); #else - append_menu_check_item(viewMenu, wxID_ANY, _(L("Show &labels")) + sep + "E", _(L("Show object/instance labels in 3D scene")), + append_menu_check_item(viewMenu, wxID_ANY, _L("Show &labels") + sep + "E", _L("Show object/instance labels in 3D scene"), [this](wxCommandEvent&) { m_plater->show_view3D_labels(!m_plater->are_view3D_labels_shown()); }, this, [this]() { return m_plater->is_view3D_shown(); }, [this]() { return m_plater->are_view3D_labels_shown(); }, this); #endif // ENABLE_SLOPE_RENDERING - append_menu_check_item(viewMenu, wxID_ANY, _(L("&Collapse sidebar")), _(L("Collapse sidebar")), + append_menu_check_item(viewMenu, wxID_ANY, _L("&Collapse sidebar"), _L("Collapse sidebar"), [this](wxCommandEvent&) { m_plater->collapse_sidebar(!m_plater->is_sidebar_collapsed()); }, this, [this]() { return true; }, [this]() { return m_plater->is_sidebar_collapsed(); }, this); } // Help menu +#if ENABLE_GCODE_VIEWER + auto helpMenu = generate_help_menu(); +#else auto helpMenu = new wxMenu(); { - append_menu_item(helpMenu, wxID_ANY, _(L("Prusa 3D &Drivers")), _(L("Open the Prusa3D drivers download page in your browser")), + append_menu_item(helpMenu, wxID_ANY, _L("Prusa 3D &Drivers"), _L("Open the Prusa3D drivers download page in your browser"), [this](wxCommandEvent&) { wxGetApp().open_web_page_localized("https://www.prusa3d.com/downloads"); }); - append_menu_item(helpMenu, wxID_ANY, _(L("Software &Releases")), _(L("Open the software releases page in your browser")), - [this](wxCommandEvent&) { wxLaunchDefaultBrowser("http://github.com/prusa3d/PrusaSlicer/releases"); }); + append_menu_item(helpMenu, wxID_ANY, _L("Software &Releases"), _L("Open the software releases page in your browser"), + [this](wxCommandEvent&) { wxLaunchDefaultBrowser("https://github.com/prusa3d/PrusaSlicer/releases"); }); //# my $versioncheck = $self->_append_menu_item($helpMenu, "Check for &Updates...", "Check for new Slic3r versions", sub{ //# wxTheApp->check_version(1); //# }); //# $versioncheck->Enable(wxTheApp->have_version_check); - append_menu_item(helpMenu, wxID_ANY, wxString::Format(_(L("%s &Website")), SLIC3R_APP_NAME), - wxString::Format(_(L("Open the %s website in your browser")), SLIC3R_APP_NAME), + append_menu_item(helpMenu, wxID_ANY, wxString::Format(_L("%s &Website"), SLIC3R_APP_NAME), + wxString::Format(_L("Open the %s website in your browser"), SLIC3R_APP_NAME), [this](wxCommandEvent&) { wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); }); // append_menu_item(helpMenu, wxID_ANY, wxString::Format(_(L("%s &Manual")), SLIC3R_APP_NAME), // wxString::Format(_(L("Open the %s manual in your browser")), SLIC3R_APP_NAME), // [this](wxCommandEvent&) { wxLaunchDefaultBrowser("http://manual.slic3r.org/"); }); helpMenu->AppendSeparator(); - append_menu_item(helpMenu, wxID_ANY, _(L("System &Info")), _(L("Show system information")), + append_menu_item(helpMenu, wxID_ANY, _L("System &Info"), _L("Show system information"), [this](wxCommandEvent&) { wxGetApp().system_info(); }); - append_menu_item(helpMenu, wxID_ANY, _(L("Show &Configuration Folder")), _(L("Show user configuration folder (datadir)")), + append_menu_item(helpMenu, wxID_ANY, _L("Show &Configuration Folder"), _L("Show user configuration folder (datadir)"), [this](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); }); - append_menu_item(helpMenu, wxID_ANY, _(L("Report an I&ssue")), wxString::Format(_(L("Report an issue on %s")), SLIC3R_APP_NAME), - [this](wxCommandEvent&) { wxLaunchDefaultBrowser("http://github.com/prusa3d/slic3r/issues/new"); }); + append_menu_item(helpMenu, wxID_ANY, _L("Report an I&ssue"), wxString::Format(_L("Report an issue on %s"), SLIC3R_APP_NAME), + [this](wxCommandEvent&) { wxLaunchDefaultBrowser("https://github.com/prusa3d/slic3r/issues/new"); }); append_menu_item(helpMenu, wxID_ANY, wxString::Format(_(L("&About %s")), SLIC3R_APP_NAME), _(L("Show about dialog")), [this](wxCommandEvent&) { Slic3r::GUI::about(); }); helpMenu->AppendSeparator(); - append_menu_item(helpMenu, wxID_ANY, _(L("Keyboard Shortcuts")) + sep + "&?", _(L("Show the list of the keyboard shortcuts")), + append_menu_item(helpMenu, wxID_ANY, _L("Keyboard Shortcuts") + sep + "&?", _L("Show the list of the keyboard shortcuts"), [this](wxCommandEvent&) { wxGetApp().keyboard_shortcuts(); }); #if ENABLE_THUMBNAIL_GENERATOR_DEBUG helpMenu->AppendSeparator(); @@ -1170,43 +1299,111 @@ void MainFrame::init_menubar() [this](wxCommandEvent&) { wxGetApp().gcode_thumbnails_debug(); }); #endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG } +#endif // ENABLE_GCODE_VIEWER // menubar // assign menubar to frame after appending items, otherwise special items // will not be handled correctly +#if ENABLE_GCODE_VIEWER + m_menubar = new wxMenuBar(); + m_menubar->Append(fileMenu, _L("&File")); + if (editMenu) m_menubar->Append(editMenu, _L("&Edit")); + m_menubar->Append(windowMenu, _L("&Window")); + if (viewMenu) m_menubar->Append(viewMenu, _L("&View")); + // Add additional menus from C++ + wxGetApp().add_config_menu(m_menubar); + m_menubar->Append(helpMenu, _L("&Help")); + SetMenuBar(m_menubar); +#else auto menubar = new wxMenuBar(); - menubar->Append(fileMenu, _(L("&File"))); - if (editMenu) menubar->Append(editMenu, _(L("&Edit"))); - menubar->Append(windowMenu, _(L("&Window"))); - if (viewMenu) menubar->Append(viewMenu, _(L("&View"))); + menubar->Append(fileMenu, _L("&File")); + if (editMenu) menubar->Append(editMenu, _L("&Edit")); + menubar->Append(windowMenu, _L("&Window")); + if (viewMenu) menubar->Append(viewMenu, _L("&View")); // Add additional menus from C++ wxGetApp().add_config_menu(menubar); - menubar->Append(helpMenu, _(L("&Help"))); + menubar->Append(helpMenu, _L("&Help")); SetMenuBar(menubar); +#endif // ENABLE_GCODE_VIEWER #ifdef __APPLE__ // This fixes a bug on Mac OS where the quit command doesn't emit window close events // wx bug: https://trac.wxwidgets.org/ticket/18328 +#if ENABLE_GCODE_VIEWER + wxMenu* apple_menu = m_menubar->OSXGetAppleMenu(); +#else wxMenu *apple_menu = menubar->OSXGetAppleMenu(); +#endif // ENABLE_GCODE_VIEWER if (apple_menu != nullptr) { apple_menu->Bind(wxEVT_MENU, [this](wxCommandEvent &) { Close(); }, wxID_EXIT); } -#endif +#endif // __APPLE__ if (plater()->printer_technology() == ptSLA) update_menubar(); } +#if ENABLE_GCODE_VIEWER +void MainFrame::init_menubar_as_gcodeviewer() +{ + wxMenu* fileMenu = new wxMenu; + { + append_menu_item(fileMenu, wxID_ANY, _L("&Open G-code") + dots + "\tCtrl+O", _L("Open a G-code file"), + [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->load_gcode(); }, "open", nullptr, + [this]() {return m_plater != nullptr; }, this); + fileMenu->AppendSeparator(); + append_menu_item(fileMenu, wxID_ANY, _L("Export &toolpaths as OBJ") + dots, _L("Export toolpaths as OBJ"), + [this](wxCommandEvent&) { if (m_plater != nullptr) m_plater->export_toolpaths_to_obj(); }, "export_plater", nullptr, + [this]() {return can_export_toolpaths(); }, this); + fileMenu->AppendSeparator(); + append_menu_item(fileMenu, wxID_EXIT, _L("&Quit"), wxString::Format(_L("Quit %s"), SLIC3R_APP_NAME), + [this](wxCommandEvent&) { Close(false); }); + } + + // View menu + wxMenu* viewMenu = nullptr; + if (m_plater != nullptr) { + viewMenu = new wxMenu(); + add_common_view_menu_items(viewMenu, this, std::bind(&MainFrame::can_change_view, this)); + } + + // helpmenu + auto helpMenu = generate_help_menu(); + + m_menubar = new wxMenuBar(); + m_menubar->Append(fileMenu, _L("&File")); + if (viewMenu != nullptr) m_menubar->Append(viewMenu, _L("&View")); + m_menubar->Append(helpMenu, _L("&Help")); + SetMenuBar(m_menubar); + +#ifdef __APPLE__ + // This fixes a bug on Mac OS where the quit command doesn't emit window close events + // wx bug: https://trac.wxwidgets.org/ticket/18328 + wxMenu* apple_menu = m_menubar->OSXGetAppleMenu(); + if (apple_menu != nullptr) { + apple_menu->Bind(wxEVT_MENU, [this](wxCommandEvent&) { + Close(); + }, wxID_EXIT); + } +#endif // __APPLE__ +} +#endif // ENABLE_GCODE_VIEWER + void MainFrame::update_menubar() { +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_gcode_viewer()) + return; +#endif // ENABLE_GCODE_VIEWER + const bool is_fff = plater()->printer_technology() == ptFFF; - m_changeable_menu_items[miExport] ->SetItemLabel((is_fff ? _(L("Export &G-code")) : _(L("E&xport")) ) + dots + "\tCtrl+G"); - m_changeable_menu_items[miSend] ->SetItemLabel((is_fff ? _(L("S&end G-code")) : _(L("S&end to print"))) + dots + "\tCtrl+Shift+G"); + m_changeable_menu_items[miExport] ->SetItemLabel((is_fff ? _L("Export &G-code") : _L("E&xport")) + dots + "\tCtrl+G"); + m_changeable_menu_items[miSend] ->SetItemLabel((is_fff ? _L("S&end G-code") : _L("S&end to print")) + dots + "\tCtrl+Shift+G"); - m_changeable_menu_items[miMaterialTab] ->SetItemLabel((is_fff ? _(L("&Filament Settings Tab")) : _(L("Mate&rial Settings Tab"))) + "\tCtrl+3"); + m_changeable_menu_items[miMaterialTab] ->SetItemLabel((is_fff ? _L("&Filament Settings Tab") : _L("Mate&rial Settings Tab")) + "\tCtrl+3"); m_changeable_menu_items[miMaterialTab] ->SetBitmap(create_scaled_bitmap(is_fff ? "spool" : "resin")); m_changeable_menu_items[miPrinterTab] ->SetBitmap(create_scaled_bitmap(is_fff ? "printer" : "sla_printer")); @@ -1527,25 +1724,16 @@ void MainFrame::load_config(const DynamicPrintConfig& config) void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) { -#if ENABLE_LAYOUT_NO_RESTART + bool tabpanel_was_hidden = false; if (m_layout == ESettingsLayout::Dlg) { -#else - if (m_layout == slDlg) { -#endif // ENABLE_LAYOUT_NO_RESTART if (tab==0) { -#if ENABLE_LAYOUT_NO_RESTART if (m_settings_dialog.IsShown()) this->SetFocus(); -#else - if (m_settings_dialog->IsShown()) - this->SetFocus(); -#endif // ENABLE_LAYOUT_NO_RESTART // plater should be focused for correct navigation inside search window if (m_plater->canvas3D()->is_search_pressed()) m_plater->SetFocus(); return; } -#if ENABLE_LAYOUT_NO_RESTART // Show/Activate Settings Dialog #ifdef __WXOSX__ // Don't call SetFont under OSX to avoid name cutting in ObjectList if (m_settings_dialog.IsShown()) @@ -1557,31 +1745,16 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) if (m_settings_dialog.IsShown()) m_settings_dialog.SetFocus(); else { + tabpanel_was_hidden = true; m_tabpanel->Show(); m_settings_dialog.Show(); } #endif -#else - // Show/Activate Settings Dialog - if (m_settings_dialog->IsShown()) -#ifdef __WXOSX__ // Don't call SetFont under OSX to avoid name cutting in ObjectList - m_settings_dialog->Hide(); -#else - m_settings_dialog->SetFocus(); - else -#endif - m_settings_dialog->Show(); -#endif // ENABLE_LAYOUT_NO_RESTART } -#if ENABLE_LAYOUT_NO_RESTART else if (m_layout == ESettingsLayout::New) { m_main_sizer->Show(m_plater, tab == 0); + tabpanel_was_hidden = !m_main_sizer->IsShown(m_tabpanel); m_main_sizer->Show(m_tabpanel, tab != 0); -#else - else if (m_layout == slNew) { - m_plater->Show(tab == 0); - m_tabpanel->Show(tab != 0); -#endif // ENABLE_LAYOUT_NO_RESTART // plater should be focused for correct navigation inside search window if (tab == 0 && m_plater->canvas3D()->is_search_pressed()) @@ -1589,12 +1762,16 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/) Layout(); } + // When we run application in ESettingsLayout::New or ESettingsLayout::Dlg mode, tabpanel is hidden from the very beginning + // and as a result Tab::update_changed_tree_ui() function couldn't update m_is_nonsys_values values, + // which are used for update TreeCtrl and "revert_buttons". + // So, force the call of this function for Tabs, if tab panel was hidden + if (tabpanel_was_hidden) + for (auto tab : wxGetApp().tabs_list) + tab->update_changed_tree_ui(); + // when tab == -1, it means we should show the last selected tab -#if ENABLE_LAYOUT_NO_RESTART m_tabpanel->SetSelection(tab == (size_t)(-1) ? m_last_selected_tab : (m_layout == ESettingsLayout::Dlg && tab != 0) ? tab - 1 : tab); -#else - m_tabpanel->SetSelection(tab == (size_t)(-1) ? m_last_selected_tab : (m_layout == slDlg && tab != 0) ? tab-1 : tab); -#endif // ENABLE_LAYOUT_NO_RESTART } // Set a camera direction, zoom to all objects. @@ -1709,12 +1886,25 @@ SettingsDialog::SettingsDialog(MainFrame* mainframe) wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxMINIMIZE_BOX | wxMAXIMIZE_BOX, "settings_dialog"), m_main_frame(mainframe) { +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_gcode_viewer()) + return; +#endif // ENABLE_GCODE_VIEWER + +#if ENABLE_WX_3_1_3_DPI_CHANGED_EVENT && defined(__WXMSW__) + // ys_FIXME! temporary workaround for correct font scaling + // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, + // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT + this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); +#else this->SetFont(wxGetApp().normal_font()); +#endif // ENABLE_WX_3_1_3_DPI_CHANGED_EVENT this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); // Load the icon either from the exe, or from the ico file. #if _WIN32 { + TCHAR szExeFileName[MAX_PATH]; GetModuleFileName(nullptr, szExeFileName, MAX_PATH); SetIcon(wxIcon(szExeFileName, wxBITMAP_TYPE_ICO)); @@ -1723,34 +1913,6 @@ SettingsDialog::SettingsDialog(MainFrame* mainframe) SetIcon(wxIcon(var("PrusaSlicer_128px.png"), wxBITMAP_TYPE_PNG)); #endif // _WIN32 -#if !ENABLE_LAYOUT_NO_RESTART - // wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on Windows 10 - // with multiple high resolution displays connected. - m_tabpanel = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size(), wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME); -#ifndef __WXOSX__ // Don't call SetFont under OSX to avoid name cutting in ObjectList - m_tabpanel->SetFont(Slic3r::GUI::wxGetApp().normal_font()); -#endif - - m_tabpanel->Bind(wxEVT_KEY_UP, [this](wxKeyEvent& evt) { - if ((evt.GetModifiers() & wxMOD_CONTROL) != 0) { - switch (evt.GetKeyCode()) { - case '1': { m_main_frame->select_tab(0); break; } - case '2': { m_main_frame->select_tab(1); break; } - case '3': { m_main_frame->select_tab(2); break; } - case '4': { m_main_frame->select_tab(3); break; } -#ifdef __APPLE__ - case 'f': -#else /* __APPLE__ */ - case WXK_CONTROL_F: -#endif /* __APPLE__ */ - case 'F': { m_main_frame->plater()->search(false); break; } - default:break; - } - } - }); -#endif // !ENABLE_LAYOUT_NO_RESTART - -#if ENABLE_LAYOUT_NO_RESTART this->Bind(wxEVT_SHOW, [this](wxShowEvent& evt) { auto key_up_handker = [this](wxKeyEvent& evt) { @@ -1780,13 +1942,9 @@ SettingsDialog::SettingsDialog(MainFrame* mainframe) m_tabpanel->Unbind(wxEVT_KEY_UP, key_up_handker); } }); -#endif // ENABLE_LAYOUT_NO_RESTART // initialize layout auto sizer = new wxBoxSizer(wxVERTICAL); -#if !ENABLE_LAYOUT_NO_RESTART - sizer->Add(m_tabpanel, 1, wxEXPAND); -#endif // !ENABLE_LAYOUT_NO_RESTART sizer->SetSizeHints(this); SetSizer(sizer); Fit(); @@ -1805,6 +1963,11 @@ SettingsDialog::SettingsDialog(MainFrame* mainframe) void SettingsDialog::on_dpi_changed(const wxRect& suggested_rect) { +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_gcode_viewer()) + return; +#endif // ENABLE_GCODE_VIEWER + const int& em = em_unit(); const wxSize& size = wxSize(85 * em, 50 * em); diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 4514b8f50f..868a684925 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -7,6 +7,9 @@ #include #include #include +#if ENABLE_GCODE_VIEWER_TASKBAR_ICON +#include +#endif // ENABLE_GCODE_VIEWER_TASKBAR_ICON #include #include @@ -54,12 +57,8 @@ class SettingsDialog : public DPIDialog MainFrame* m_main_frame { nullptr }; public: SettingsDialog(MainFrame* mainframe); - ~SettingsDialog() {} -#if ENABLE_LAYOUT_NO_RESTART + ~SettingsDialog() = default; void set_tabpanel(wxNotebook* tabpanel) { m_tabpanel = tabpanel; } -#else - wxNotebook* get_tabpanel() { return m_tabpanel; } -#endif // ENABLE_LAYOUT_NO_RESTART protected: void on_dpi_changed(const wxRect& suggested_rect) override; @@ -72,6 +71,10 @@ class MainFrame : public DPIFrame wxString m_qs_last_input_file = wxEmptyString; wxString m_qs_last_output_file = wxEmptyString; wxString m_last_config = wxEmptyString; +#if ENABLE_GCODE_VIEWER + wxMenuBar* m_menubar{ nullptr }; +#endif // ENABLE_GCODE_VIEWER + #if 0 wxMenuItem* m_menu_item_repeat { nullptr }; // doesn't used now #endif @@ -119,23 +122,18 @@ class MainFrame : public DPIFrame wxFileHistory m_recent_projects; -#if ENABLE_LAYOUT_NO_RESTART enum class ESettingsLayout { Unknown, Old, New, Dlg, +#if ENABLE_GCODE_VIEWER + GCodeViewer +#endif // ENABLE_GCODE_VIEWER }; ESettingsLayout m_layout{ ESettingsLayout::Unknown }; -#else - enum SettingsLayout { - slOld = 0, - slNew, - slDlg, - } m_layout; -#endif // ENABLE_LAYOUT_NO_RESTART protected: virtual void on_dpi_changed(const wxRect &suggested_rect); @@ -143,11 +141,13 @@ protected: public: MainFrame(); +#if ENABLE_GCODE_VIEWER_TASKBAR_ICON + ~MainFrame(); +#else ~MainFrame() = default; +#endif // ENABLE_GCODE_VIEWER_TASKBAR_ICON -#if ENABLE_LAYOUT_NO_RESTART void update_layout(); -#endif // ENABLE_LAYOUT_NO_RESTART // Called when closing the application and when switching the application language. void shutdown(); @@ -159,7 +159,12 @@ public: void init_tabpanel(); void create_preset_tabs(); void add_created_tab(Tab* panel); +#if ENABLE_GCODE_VIEWER + void init_menubar_as_editor(); + void init_menubar_as_gcodeviewer(); +#else void init_menubar(); +#endif // ENABLE_GCODE_VIEWER void update_menubar(); void update_ui_from_settings(); @@ -190,15 +195,15 @@ public: Plater* m_plater { nullptr }; wxNotebook* m_tabpanel { nullptr }; -#if ENABLE_LAYOUT_NO_RESTART SettingsDialog m_settings_dialog; wxWindow* m_plater_page{ nullptr }; -#else - SettingsDialog* m_settings_dialog { nullptr }; -#endif // ENABLE_LAYOUT_NO_RESTART wxProgressDialog* m_progress_dialog { nullptr }; std::shared_ptr m_statusbar; +#if ENABLE_GCODE_VIEWER_TASKBAR_ICON + wxTaskBarIcon* m_taskbar_icon{ nullptr }; +#endif // ENABLE_GCODE_VIEWER_TASKBAR_ICON + #ifdef _WIN32 void* m_hDeviceNotify { nullptr }; uint32_t m_ulSHChangeNotifyRegister { 0 }; diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 581f50a882..ee0abe76f9 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -134,7 +134,7 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& Vec3d direction; line_from_mouse_pos(mouse_pos, trafo, camera, point, direction); - std::vector hits = m_emesh.query_ray_hits(point, direction); + std::vector hits = m_emesh.query_ray_hits(point, direction); if (hits.empty()) return false; // no intersection found @@ -184,7 +184,7 @@ std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transfo bool is_obscured = false; // Cast a ray in the direction of the camera and look for intersection with the mesh: - std::vector hits; + std::vector hits; // Offset the start of the ray by EPSILON to account for numerical inaccuracies. hits = m_emesh.query_ray_hits((inverse_trafo * pt + direction_to_camera_mesh * EPSILON).cast(), direction_to_camera.cast()); diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index 2758577a25..60dcb30c81 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -3,7 +3,7 @@ #include "libslic3r/Point.hpp" #include "libslic3r/Geometry.hpp" -#include "libslic3r/SLA/EigenMesh3D.hpp" +#include "libslic3r/SLA/IndexedMesh.hpp" #include "admesh/stl.h" #include "slic3r/GUI/3DScene.hpp" @@ -147,7 +147,7 @@ public: Vec3f get_triangle_normal(size_t facet_idx) const; private: - sla::EigenMesh3D m_emesh; + sla::IndexedMesh m_emesh; std::vector m_normals; }; diff --git a/src/slic3r/GUI/Mouse3DController.cpp b/src/slic3r/GUI/Mouse3DController.cpp index baa9356b69..4c5ee20762 100644 --- a/src/slic3r/GUI/Mouse3DController.cpp +++ b/src/slic3r/GUI/Mouse3DController.cpp @@ -1,12 +1,12 @@ #include "libslic3r/libslic3r.h" +#include "libslic3r/PresetBundle.hpp" #include "Mouse3DController.hpp" #include "Camera.hpp" #include "GUI_App.hpp" -#include "PresetBundle.hpp" -#include "AppConfig.hpp" #include "GLCanvas3D.hpp" #include "Plater.hpp" +#include "NotificationManager.hpp" #include @@ -239,8 +239,7 @@ void Mouse3DController::render_settings_dialog(GLCanvas3D& canvas) const // when the user clicks on [X] or [Close] button we need to trigger // an extra frame to let the dialog disappear - if (m_settings_dialog_closed_by_user) - { + if (m_settings_dialog_closed_by_user) { m_show_settings_dialog = false; m_settings_dialog_closed_by_user = false; canvas.request_extra_frame(); @@ -261,13 +260,10 @@ void Mouse3DController::render_settings_dialog(GLCanvas3D& canvas) const static ImVec2 last_win_size(0.0f, 0.0f); bool shown = true; - if (imgui.begin(_(L("3Dconnexion settings")), &shown, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse)) - { - if (shown) - { + if (imgui.begin(_L("3Dconnexion settings"), &shown, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse)) { + if (shown) { ImVec2 win_size = ImGui::GetWindowSize(); - if ((last_win_size.x != win_size.x) || (last_win_size.y != win_size.y)) - { + if (last_win_size.x != win_size.x || last_win_size.y != win_size.y) { // when the user clicks on [X] button, the next time the dialog is shown // has a dummy size, so we trigger an extra frame to let it have the correct size last_win_size = win_size; @@ -275,59 +271,51 @@ void Mouse3DController::render_settings_dialog(GLCanvas3D& canvas) const } const ImVec4& color = ImGui::GetStyleColorVec4(ImGuiCol_Separator); - ImGui::PushStyleColor(ImGuiCol_Text, color); - imgui.text(_(L("Device:"))); - ImGui::PopStyleColor(); + imgui.text_colored(color, _L("Device:")); ImGui::SameLine(); imgui.text(m_device_str); ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, color); - imgui.text(_(L("Speed:"))); - ImGui::PopStyleColor(); + imgui.text_colored(color, _L("Speed:")); float translation_scale = (float)params_copy.translation.scale / Params::DefaultTranslationScale; - if (imgui.slider_float(_(L("Translation")) + "##1", &translation_scale, 0.1f, 10.0f, "%.1f")) { + if (imgui.slider_float(_L("Translation") + "##1", &translation_scale, 0.1f, 10.0f, "%.1f")) { params_copy.translation.scale = Params::DefaultTranslationScale * (double)translation_scale; params_changed = true; } float rotation_scale = params_copy.rotation.scale / Params::DefaultRotationScale; - if (imgui.slider_float(_(L("Rotation")) + "##1", &rotation_scale, 0.1f, 10.0f, "%.1f")) { + if (imgui.slider_float(_L("Rotation") + "##1", &rotation_scale, 0.1f, 10.0f, "%.1f")) { params_copy.rotation.scale = Params::DefaultRotationScale * rotation_scale; params_changed = true; } float zoom_scale = params_copy.zoom.scale / Params::DefaultZoomScale; - if (imgui.slider_float(_(L("Zoom")), &zoom_scale, 0.1f, 10.0f, "%.1f")) { + if (imgui.slider_float(_L("Zoom"), &zoom_scale, 0.1f, 10.0f, "%.1f")) { params_copy.zoom.scale = Params::DefaultZoomScale * zoom_scale; params_changed = true; } ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, color); - imgui.text(_(L("Deadzone:"))); - ImGui::PopStyleColor(); + imgui.text_colored(color, _L("Deadzone:")); float translation_deadzone = (float)params_copy.translation.deadzone; - if (imgui.slider_float(_(L("Translation")) + "/" + _(L("Zoom")), &translation_deadzone, 0.0f, (float)Params::MaxTranslationDeadzone, "%.2f")) { + if (imgui.slider_float(_L("Translation") + "/" + _L("Zoom"), &translation_deadzone, 0.0f, (float)Params::MaxTranslationDeadzone, "%.2f")) { params_copy.translation.deadzone = (double)translation_deadzone; params_changed = true; } float rotation_deadzone = params_copy.rotation.deadzone; - if (imgui.slider_float(_(L("Rotation")) + "##2", &rotation_deadzone, 0.0f, Params::MaxRotationDeadzone, "%.2f")) { + if (imgui.slider_float(_L("Rotation") + "##2", &rotation_deadzone, 0.0f, Params::MaxRotationDeadzone, "%.2f")) { params_copy.rotation.deadzone = rotation_deadzone; params_changed = true; } ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, color); - imgui.text(_(L("Options:"))); - ImGui::PopStyleColor(); + imgui.text_colored(color, _L("Options:")); bool swap_yz = params_copy.swap_yz; - if (imgui.checkbox("Swap Y/Z axes", swap_yz)) { + if (imgui.checkbox(_L("Swap Y/Z axes"), swap_yz)) { params_copy.swap_yz = swap_yz; params_changed = true; } @@ -335,25 +323,20 @@ void Mouse3DController::render_settings_dialog(GLCanvas3D& canvas) const #if ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT ImGui::Separator(); ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, color); - imgui.text("DEBUG:"); - imgui.text("Vectors:"); - ImGui::PopStyleColor(); + imgui.text_colored(color, "DEBUG:"); + imgui.text_colored(color, "Vectors:"); Vec3f translation = m_state.get_first_vector_of_type(State::QueueItem::TranslationType).cast(); Vec3f rotation = m_state.get_first_vector_of_type(State::QueueItem::RotationType).cast(); ImGui::InputFloat3("Translation##3", translation.data(), "%.3f", ImGuiInputTextFlags_ReadOnly); ImGui::InputFloat3("Rotation##3", rotation.data(), "%.3f", ImGuiInputTextFlags_ReadOnly); - ImGui::PushStyleColor(ImGuiCol_Text, color); - imgui.text("Queue size:"); - ImGui::PopStyleColor(); + imgui.text_colored(color, "Queue size:"); int input_queue_size_current[2] = { int(m_state.input_queue_size_current()), int(m_state.input_queue_max_size_achieved) }; ImGui::InputInt2("Current##4", input_queue_size_current, ImGuiInputTextFlags_ReadOnly); int input_queue_size_param = int(params_copy.input_queue_max_size); - if (ImGui::InputInt("Max size", &input_queue_size_param, 1, 1, ImGuiInputTextFlags_ReadOnly)) - { + if (ImGui::InputInt("Max size", &input_queue_size_param, 1, 1, ImGuiInputTextFlags_ReadOnly)) { if (input_queue_size_param > 0) { params_copy.input_queue_max_size = input_queue_size_param; params_changed = true; @@ -361,23 +344,19 @@ void Mouse3DController::render_settings_dialog(GLCanvas3D& canvas) const } ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, color); - imgui.text("Camera:"); - ImGui::PopStyleColor(); + imgui.text_colored(color, "Camera:"); Vec3f target = wxGetApp().plater()->get_camera().get_target().cast(); ImGui::InputFloat3("Target", target.data(), "%.3f", ImGuiInputTextFlags_ReadOnly); #endif // ENABLE_3DCONNEXION_DEVICES_DEBUG_OUTPUT ImGui::Separator(); - if (imgui.button(_(L("Close")))) - { + if (imgui.button(_L("Close"))) { // the user clicked on the [Close] button m_settings_dialog_closed_by_user = true; canvas.set_as_dirty(); } } - else - { + else { // the user clicked on the [X] button m_settings_dialog_closed_by_user = true; canvas.set_as_dirty(); @@ -424,6 +403,8 @@ void Mouse3DController::disconnected() m_params_by_device[m_device_str] = m_params_ui; m_device_str.clear(); m_connected = false; + wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::Mouse3dDisconnected, *(wxGetApp().plater()->get_current_canvas3D())); + wxGetApp().plater()->CallAfter([]() { Plater *plater = wxGetApp().plater(); if (plater != nullptr) { diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp new file mode 100644 index 0000000000..47962f4b2a --- /dev/null +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -0,0 +1,940 @@ +#include "NotificationManager.hpp" + +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "GLCanvas3D.hpp" +#include "ImGuiWrapper.hpp" + +#include "wxExtensions.hpp" + +#include +#include +#include +#include + + + + +#define NOTIFICATION_MAX_MOVE 3.0f + +#define GAP_WIDTH 10.0f +#define SPACE_RIGHT_PANEL 10.0f + +namespace Slic3r { +namespace GUI { + +wxDEFINE_EVENT(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, EjectDriveNotificationClickedEvent); +wxDEFINE_EVENT(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, ExportGcodeNotificationClickedEvent); +wxDEFINE_EVENT(EVT_PRESET_UPDATE_AVIABLE_CLICKED, PresetUpdateAviableClickedEvent); + +namespace Notifications_Internal{ + void push_style_color(ImGuiCol idx, const ImVec4& col, bool fading_out, float current_fade_opacity) + { + if (fading_out) + ImGui::PushStyleColor(idx, ImVec4(col.x, col.y, col.z, col.w * current_fade_opacity)); + else + ImGui::PushStyleColor(idx, col); + } +} +//ScalableBitmap bmp_icon; +//------PopNotification-------- +NotificationManager::PopNotification::PopNotification(const NotificationData &n, const int id, wxEvtHandler* evt_handler) : + m_data (n) + , m_id (id) + , m_remaining_time (n.duration) + , m_last_remaining_time (n.duration) + , m_counting_down (n.duration != 0) + , m_text1 (n.text1) + , m_hypertext (n.hypertext) + , m_text2 (n.text2) + , m_evt_handler (evt_handler) +{ + //init(); +} +NotificationManager::PopNotification::~PopNotification() +{ +} +NotificationManager::PopNotification::RenderResult NotificationManager::PopNotification::render(GLCanvas3D& canvas, const float& initial_y) +{ + if (!m_initialized) + { + init(); + } + if (m_finished) + return RenderResult::Finished; + if (m_close_pending) { + // request of extra frame will be done in caller function by ret val ClosePending + m_finished = true; + return RenderResult::ClosePending; + } + if (m_hidden) { + m_top_y = initial_y - GAP_WIDTH; + return RenderResult::Static; + } + RenderResult ret_val = m_counting_down ? RenderResult::Countdown : RenderResult::Static; + Size cnv_size = canvas.get_canvas_size(); + ImGuiWrapper& imgui = *wxGetApp().imgui(); + bool shown = true; + std::string name; + ImVec2 mouse_pos = ImGui::GetMousePos(); + + if (m_line_height != ImGui::CalcTextSize("A").y) + init(); + + set_next_window_size(imgui); + + //top y of window + m_top_y = initial_y + m_window_height; + //top right position + ImVec2 win_pos(1.0f * (float)cnv_size.get_width() - SPACE_RIGHT_PANEL, 1.0f * (float)cnv_size.get_height() - m_top_y); + imgui.set_next_window_pos(win_pos.x, win_pos.y, ImGuiCond_Always, 1.0f, 0.0f); + imgui.set_next_window_size(m_window_width, m_window_height, ImGuiCond_Always); + + //find if hovered + if (mouse_pos.x < win_pos.x && mouse_pos.x > win_pos.x - m_window_width && mouse_pos.y > win_pos.y&& mouse_pos.y < win_pos.y + m_window_height) + { + ImGui::SetNextWindowFocus(); + ret_val = RenderResult::Hovered; + //reset fading + m_fading_out = false; + m_current_fade_opacity = 1.f; + m_remaining_time = m_data.duration; + m_countdown_frame = 0; + } + + if (m_counting_down && m_remaining_time < 0) + m_close_pending = true; + + if (m_close_pending) { + // request of extra frame will be done in caller function by ret val ClosePending + m_finished = true; + return RenderResult::ClosePending; + } + + // color change based on fading out + bool fading_pop = false; + if (m_fading_out) { + if (!m_paused) + m_current_fade_opacity -= 1.f / ((m_fading_time + 1.f) * 60.f); + Notifications_Internal::push_style_color(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_fading_out, m_current_fade_opacity); + Notifications_Internal::push_style_color(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_Text), m_fading_out, m_current_fade_opacity); + fading_pop = true; + } + // background color + if (m_is_gray) { + ImVec4 backcolor(0.7f, 0.7f, 0.7f, 0.5f); + Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + } else if (m_data.level == NotificationLevel::ErrorNotification) { + ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); + backcolor.x += 0.3f; + Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + } else if (m_data.level == NotificationLevel::WarningNotification) { + ImVec4 backcolor = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg); + backcolor.x += 0.3f; + backcolor.y += 0.15f; + Notifications_Internal::push_style_color(ImGuiCol_WindowBg, backcolor, m_fading_out, m_current_fade_opacity); + } + + //name of window - probably indentifies window and is shown so last_end add whitespaces according to id + for (size_t i = 0; i < m_id; i++) + name += " "; + if (imgui.begin(name, &shown, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar )) { + if (shown) { + + ImVec2 win_size = ImGui::GetWindowSize(); + + + //FIXME: dont forget to us this for texts + //GUI::format(_utf8(L())); + + /* + //countdown numbers + ImGui::SetCursorPosX(15); + ImGui::SetCursorPosY(15); + imgui.text(std::to_string(m_remaining_time).c_str()); + */ + if(m_counting_down) + render_countdown(imgui, win_size.x, win_size.y, win_pos.x, win_pos.y); + render_left_sign(imgui); + render_text(imgui, win_size.x, win_size.y, win_pos.x, win_pos.y); + render_close_button(imgui, win_size.x, win_size.y, win_pos.x, win_pos.y); + if (m_multiline && m_lines_count > 3) + render_minimize_button(imgui, win_pos.x, win_pos.y); + } else { + // the user clicked on the [X] button ( ImGuiWindowFlags_NoTitleBar means theres no [X] button) + m_close_pending = true; + canvas.set_as_dirty(); + } + } + imgui.end(); + + if (fading_pop) { + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + } + if (m_is_gray) + ImGui::PopStyleColor(); + else if (m_data.level == NotificationLevel::ErrorNotification) + ImGui::PopStyleColor(); + else if (m_data.level == NotificationLevel::WarningNotification) + ImGui::PopStyleColor(); + return ret_val; +} +void NotificationManager::PopNotification::init() +{ + std::string text = m_text1 + " " + m_hypertext; + int last_end = 0; + m_lines_count = 0; + + //determine line width + m_line_height = ImGui::CalcTextSize("A").y; + + m_left_indentation = m_line_height; + if (m_data.level == NotificationLevel::ErrorNotification || m_data.level == NotificationLevel::WarningNotification) { + std::string text; + text = (m_data.level == NotificationLevel::ErrorNotification ? ImGui::ErrorMarker : ImGui::WarningMarker); + float picture_width = ImGui::CalcTextSize(text.c_str()).x; + m_left_indentation = picture_width + m_line_height / 2; + } + m_window_width_offset = m_left_indentation + m_line_height * 2; + m_window_width = m_line_height * 25; + + // count lines + m_endlines.clear(); + while (last_end < text.length() - 1) + { + int next_hard_end = text.find_first_of('\n', last_end); + if (next_hard_end > 0 && ImGui::CalcTextSize(text.substr(last_end, next_hard_end - last_end).c_str()).x < m_window_width - m_window_width_offset) { + //next line is ended by '/n' + m_endlines.push_back(next_hard_end); + last_end = next_hard_end + 1; + } + else { + // find next suitable endline + if (ImGui::CalcTextSize(text.substr(last_end).c_str()).x >= m_window_width - 3.5f * m_line_height) {// m_window_width_offset) { + // more than one line till end + int next_space = text.find_first_of(' ', last_end); + if (next_space > 0) { + int next_space_candidate = text.find_first_of(' ', next_space + 1); + while (next_space_candidate > 0 && ImGui::CalcTextSize(text.substr(last_end, next_space_candidate - last_end).c_str()).x < m_window_width - m_window_width_offset) { + next_space = next_space_candidate; + next_space_candidate = text.find_first_of(' ', next_space + 1); + } + m_endlines.push_back(next_space); + last_end = next_space + 1; + } + } + else { + m_endlines.push_back(text.length()); + last_end = text.length(); + } + + } + m_lines_count++; + } + m_initialized = true; +} +void NotificationManager::PopNotification::set_next_window_size(ImGuiWrapper& imgui) +{ + if (m_multiline) { + m_window_height = m_lines_count * m_line_height; + }else + { + m_window_height = 2 * m_line_height; + } + m_window_height += 1 * m_line_height; // top and bottom +} + +void NotificationManager::PopNotification::render_text(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + float x_offset = m_left_indentation; + std::string fulltext = m_text1 + m_hypertext; //+ m_text2; + ImVec2 text_size = ImGui::CalcTextSize(fulltext.c_str()); + // text posistions are calculated by lines count + // large texts has "more" button or are displayed whole + // smaller texts are divided as one liners and two liners + if (m_lines_count > 2) { + if (m_multiline) { + + int last_end = 0; + float starting_y = m_line_height/2;//10; + float shift_y = m_line_height;// -m_line_height / 20; + for (size_t i = 0; i < m_lines_count; i++) { + std::string line = m_text1.substr(last_end , m_endlines[i] - last_end); + last_end = m_endlines[i] + 1; + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(starting_y + i * shift_y); + imgui.text(line.c_str()); + } + //hyperlink text + if (!m_hypertext.empty()) + { + render_hypertext(imgui, x_offset + ImGui::CalcTextSize(m_text1.substr(m_endlines[m_lines_count - 2] + 1, m_endlines[m_lines_count - 1] - m_endlines[m_lines_count - 2] - 1).c_str()).x, starting_y + (m_lines_count - 1) * shift_y, m_hypertext); + } + + + } else { + // line1 + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(win_size.y / 2 - win_size.y / 6 - m_line_height / 2); + imgui.text(m_text1.substr(0, m_endlines[0]).c_str()); + // line2 + std::string line = m_text1.substr(m_endlines[0] + 1, m_endlines[1] - m_endlines[0] - 1); + if (ImGui::CalcTextSize(line.c_str()).x > m_window_width - m_window_width_offset - ImGui::CalcTextSize((".." + _u8L("More")).c_str()).x) + { + line = line.substr(0, line.length() - 6); + line += ".."; + }else + line += " "; + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(win_size.y / 2 + win_size.y / 6 - m_line_height / 2); + imgui.text(line.c_str()); + // "More" hypertext + render_hypertext(imgui, x_offset + ImGui::CalcTextSize(line.c_str()).x, win_size.y / 2 + win_size.y / 6 - m_line_height / 2, _u8L("More"), true); + } + } else { + //text 1 + float cursor_y = win_size.y / 2 - text_size.y / 2; + float cursor_x = x_offset; + if(m_lines_count > 1) { + // line1 + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(win_size.y / 2 - win_size.y / 6 - m_line_height / 2); + imgui.text(m_text1.substr(0, m_endlines[0]).c_str()); + // line2 + std::string line = m_text1.substr(m_endlines[0] + 1); + cursor_y = win_size.y / 2 + win_size.y / 6 - m_line_height / 2; + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(cursor_y); + imgui.text(line.c_str()); + cursor_x = x_offset + ImGui::CalcTextSize(line.c_str()).x; + } else { + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(cursor_y); + imgui.text(m_text1.c_str()); + cursor_x = x_offset + ImGui::CalcTextSize(m_text1.c_str()).x; + } + //hyperlink text + if (!m_hypertext.empty()) + { + render_hypertext(imgui, cursor_x + 4, cursor_y, m_hypertext); + } + + //notification text 2 + //text 2 is suposed to be after the hyperlink - currently it is not used + /* + if (!m_text2.empty()) + { + ImVec2 part_size = ImGui::CalcTextSize(m_hypertext.c_str()); + ImGui::SetCursorPosX(win_size.x / 2 + text_size.x / 2 - part_size.x + 8 - x_offset); + ImGui::SetCursorPosY(cursor_y); + imgui.text(m_text2.c_str()); + } + */ + } +} + +void NotificationManager::PopNotification::render_hypertext(ImGuiWrapper& imgui, const float text_x, const float text_y, const std::string text, bool more) +{ + //invisible button + ImVec2 part_size = ImGui::CalcTextSize(text.c_str()); + ImGui::SetCursorPosX(text_x -4); + ImGui::SetCursorPosY(text_y -5); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + if (imgui.button(" ", part_size.x + 6, part_size.y + 10)) + { + if (more) + { + m_multiline = true; + set_next_window_size(imgui); + } + else { + on_text_click(); + m_close_pending = true; + } + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + + //hover color + ImVec4 orange_color = ImGui::GetStyleColorVec4(ImGuiCol_Button); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly)) + orange_color.y += 0.2f; + + //text + Notifications_Internal::push_style_color(ImGuiCol_Text, orange_color, m_fading_out, m_current_fade_opacity); + ImGui::SetCursorPosX(text_x); + ImGui::SetCursorPosY(text_y); + imgui.text(text.c_str()); + ImGui::PopStyleColor(); + + //underline + ImVec2 lineEnd = ImGui::GetItemRectMax(); + lineEnd.y -= 2; + ImVec2 lineStart = lineEnd; + lineStart.x = ImGui::GetItemRectMin().x; + ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, IM_COL32((int)(orange_color.x * 255), (int)(orange_color.y * 255), (int)(orange_color.z * 255), (int)(orange_color.w * 255.f * (m_fading_out ? m_current_fade_opacity : 1.f)))); + +} + +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) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImVec4 orange_color = ImGui::GetStyleColorVec4(ImGuiCol_Button); + orange_color.w = 0.8f; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + Notifications_Internal::push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_fading_out, m_current_fade_opacity); + Notifications_Internal::push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_fading_out, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + //button - if part if treggered + std::string button_text; + button_text = ImGui::CloseIconMarker; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_multiline? 2 * m_line_height : 0)), + true)) + { + button_text = ImGui::CloseIconHoverMarker; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.25f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y/2); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + m_close_pending = true; + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - win_size.x / 10.f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", win_size.x / 10.f, win_size.y - (m_multiline ? 2 * m_line_height : 0))) + { + m_close_pending = true; + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); +} +void NotificationManager::PopNotification::render_countdown(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + /* + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + + //countdown dots + std::string dot_text; + dot_text = m_remaining_time <= (float)m_data.duration / 4 * 3 ? ImGui::TimerDotEmptyMarker : ImGui::TimerDotMarker; + ImGui::SetCursorPosX(win_size.x - m_line_height); + //ImGui::SetCursorPosY(win_size.y / 2 - 24); + ImGui::SetCursorPosY(0); + imgui.text(dot_text.c_str()); + + dot_text = m_remaining_time < m_data.duration / 2 ? ImGui::TimerDotEmptyMarker : ImGui::TimerDotMarker; + ImGui::SetCursorPosX(win_size.x - m_line_height); + //ImGui::SetCursorPosY(win_size.y / 2 - 9); + ImGui::SetCursorPosY(win_size.y / 2 - m_line_height / 2); + imgui.text(dot_text.c_str()); + + dot_text = m_remaining_time <= m_data.duration / 4 ? ImGui::TimerDotEmptyMarker : ImGui::TimerDotMarker; + ImGui::SetCursorPosX(win_size.x - m_line_height); + //ImGui::SetCursorPosY(win_size.y / 2 + 6); + ImGui::SetCursorPosY(win_size.y - m_line_height); + imgui.text(dot_text.c_str()); + */ + if (!m_fading_out && m_remaining_time <= m_data.duration / 4) { + m_fading_out = true; + m_fading_time = m_remaining_time; + } + + if (m_last_remaining_time != m_remaining_time) { + m_last_remaining_time = m_remaining_time; + m_countdown_frame = 0; + } + /* + //countdown line + ImVec4 orange_color = ImGui::GetStyleColorVec4(ImGuiCol_Button); + float invisible_length = ((float)(m_data.duration - m_remaining_time) / (float)m_data.duration * win_size_x); + invisible_length -= win_size_x / ((float)m_data.duration * 60.f) * (60 - m_countdown_frame); + ImVec2 lineEnd = ImVec2(win_pos_x - invisible_length, win_pos_y + win_size_y - 5); + ImVec2 lineStart = ImVec2(win_pos_x - win_size_x, win_pos_y + win_size_y - 5); + ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, IM_COL32((int)(orange_color.x * 255), (int)(orange_color.y * 255), (int)(orange_color.z * 255), (int)(orange_color.picture_width * 255.f * (m_fading_out ? m_current_fade_opacity : 1.f))), 2.f); + if (!m_paused) + m_countdown_frame++; + */ +} +void NotificationManager::PopNotification::render_left_sign(ImGuiWrapper& imgui) +{ + if (m_data.level == NotificationLevel::ErrorNotification || m_data.level == NotificationLevel::WarningNotification) { + std::string text; + text = (m_data.level == NotificationLevel::ErrorNotification ? ImGui::ErrorMarker : ImGui::WarningMarker); + ImGui::SetCursorPosX(m_line_height / 3); + ImGui::SetCursorPosY(m_window_height / 2 - m_line_height / 2); + imgui.text(text.c_str()); + } +} +void NotificationManager::PopNotification::render_minimize_button(ImGuiWrapper& imgui, const float win_pos_x, const float win_pos_y) +{ + ImVec4 orange_color = ImGui::GetStyleColorVec4(ImGuiCol_Button); + orange_color.w = 0.8f; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + Notifications_Internal::push_style_color(ImGuiCol_ButtonActive, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg), m_fading_out, m_current_fade_opacity); + Notifications_Internal::push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_fading_out, m_current_fade_opacity); + Notifications_Internal::push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_fading_out, m_current_fade_opacity); + + + //button - if part if treggered + std::string button_text; + button_text = ImGui::MinimalizeMarker; + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos_x - m_window_width / 10.f, win_pos_y + m_window_height - 2 * m_line_height + 1), + ImVec2(win_pos_x, win_pos_y + m_window_height), + true)) + { + button_text = ImGui::MinimalizeHoverMarker; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(m_window_width - m_line_height * 2.25f); + ImGui::SetCursorPosY(m_window_height - button_size.y - 5); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + m_multiline = false; + } + + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); +} +void NotificationManager::PopNotification::on_text_click() +{ + switch (m_data.type) { + case NotificationType::ExportToRemovableFinished : + assert(m_evt_handler != nullptr); + if (m_evt_handler != nullptr) + wxPostEvent(m_evt_handler, EjectDriveNotificationClickedEvent(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED)); + break; + case NotificationType::SlicingComplete : + //wxGetApp().plater()->export_gcode(false); + assert(m_evt_handler != nullptr); + if (m_evt_handler != nullptr) + wxPostEvent(m_evt_handler, ExportGcodeNotificationClickedEvent(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED)); + break; + case NotificationType::PresetUpdateAviable : + //wxGetApp().plater()->export_gcode(false); + assert(m_evt_handler != nullptr); + if (m_evt_handler != nullptr) + wxPostEvent(m_evt_handler, PresetUpdateAviableClickedEvent(EVT_PRESET_UPDATE_AVIABLE_CLICKED)); + break; + case NotificationType::NewAppAviable: + wxLaunchDefaultBrowser("https://github.com/prusa3d/PrusaSlicer/releases"); + break; + default: + break; + } +} +void NotificationManager::PopNotification::update(const NotificationData& n) +{ + m_text1 = n.text1; + m_hypertext = n.hypertext; + m_text2 = n.text2; + init(); +} +bool NotificationManager::PopNotification::compare_text(const std::string& text) +{ + std::string t1(m_text1); + std::string t2(text); + t1.erase(std::remove_if(t1.begin(), t1.end(), ::isspace), t1.end()); + t2.erase(std::remove_if(t2.begin(), t2.end(), ::isspace), t2.end()); + if (t1.compare(t2) == 0) + return true; + return false; +} + +NotificationManager::SlicingCompleteLargeNotification::SlicingCompleteLargeNotification(const NotificationData& n, const int id, wxEvtHandler* evt_handler, bool large) : + NotificationManager::PopNotification(n, id, evt_handler) +{ + set_large(large); +} +void NotificationManager::SlicingCompleteLargeNotification::render_text(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + if (!m_is_large) + PopNotification::render_text(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + else { + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + + ImVec2 text1_size = ImGui::CalcTextSize(m_text1.c_str()); + float x_offset = m_left_indentation; + std::string fulltext = m_text1 + m_hypertext + m_text2; + ImVec2 text_size = ImGui::CalcTextSize(fulltext.c_str()); + float cursor_y = win_size.y / 2 - text_size.y / 2; + if (m_has_print_info) { + x_offset = 20; + cursor_y = win_size.y / 2 + win_size.y / 6 - text_size.y / 2; + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(cursor_y); + imgui.text(m_print_info.c_str()); + cursor_y = win_size.y / 2 - win_size.y / 6 - text_size.y / 2; + } + ImGui::SetCursorPosX(x_offset); + ImGui::SetCursorPosY(cursor_y); + imgui.text(m_text1.c_str()); + + render_hypertext(imgui, x_offset + text1_size.x + 4, cursor_y, m_hypertext); + + } +} +void NotificationManager::SlicingCompleteLargeNotification::set_print_info(std::string info) +{ + m_print_info = info; + m_has_print_info = true; + if(m_is_large) + m_lines_count = 2; +} +void NotificationManager::SlicingCompleteLargeNotification::set_large(bool l) +{ + m_is_large = l; + m_counting_down = !l; + m_hypertext = l ? _u8L("Export G-Code.") : std::string(); + m_hidden = !l; +} +//------NotificationManager-------- +NotificationManager::NotificationManager(wxEvtHandler* evt_handler) : + m_evt_handler(evt_handler) +{ +} +NotificationManager::~NotificationManager() +{ + for (PopNotification* notification : m_pop_notifications) + { + delete notification; + } +} +void NotificationManager::push_notification(const NotificationType type, GLCanvas3D& canvas, int timestamp) +{ + auto it = std::find_if(basic_notifications.begin(), basic_notifications.end(), + boost::bind(&NotificationData::type, _1) == type); + if (it != basic_notifications.end()) + push_notification_data( *it, canvas, timestamp); +} +void NotificationManager::push_notification(const std::string& text, GLCanvas3D& canvas, int timestamp) +{ + push_notification_data({ NotificationType::CustomNotification, NotificationLevel::RegularNotification, 10, text }, canvas, timestamp ); +} +void NotificationManager::push_notification(const std::string& text, NotificationManager::NotificationLevel level, GLCanvas3D& canvas, int timestamp) +{ + switch (level) + { + case Slic3r::GUI::NotificationManager::NotificationLevel::RegularNotification: + push_notification_data({ NotificationType::CustomNotification, level, 10, text }, canvas, timestamp); + break; + case Slic3r::GUI::NotificationManager::NotificationLevel::ErrorNotification: + push_notification_data({ NotificationType::CustomNotification, level, 0, text }, canvas, timestamp); + + break; + case Slic3r::GUI::NotificationManager::NotificationLevel::ImportantNotification: + push_notification_data({ NotificationType::CustomNotification, level, 0, text }, canvas, timestamp); + break; + default: + break; + } +} +void NotificationManager::push_slicing_error_notification(const std::string& text, GLCanvas3D& canvas) +{ + set_all_slicing_errors_gray(false); + push_notification_data({ NotificationType::SlicingError, NotificationLevel::ErrorNotification, 0, _u8L("ERROR:") + "\n" + text }, canvas, 0); + close_notification_of_type(NotificationType::SlicingComplete); +} +void NotificationManager::push_slicing_warning_notification(const std::string& text, bool gray, GLCanvas3D& canvas, size_t oid, int warning_step) +{ + NotificationData data { NotificationType::SlicingWarning, NotificationLevel::WarningNotification, 0, _u8L("WARNING:") + "\n" + text }; + + NotificationManager::SlicingWarningNotification* notification = new NotificationManager::SlicingWarningNotification(data, m_next_id++, m_evt_handler); + notification->set_object_id(oid); + notification->set_warning_step(warning_step); + if + (push_notification_data(notification, canvas, 0)) { + notification->set_gray(gray); + } + else { + delete notification; + } + +} +void NotificationManager::push_plater_error_notification(const std::string& text, GLCanvas3D& canvas) +{ + push_notification_data({ NotificationType::PlaterError, NotificationLevel::ErrorNotification, 0, _u8L("ERROR:") + "\n" + text }, canvas, 0); +} +void NotificationManager::push_plater_warning_notification(const std::string& text, GLCanvas3D& canvas) +{ + push_notification_data({ NotificationType::PlaterWarning, NotificationLevel::WarningNotification, 0, _u8L("WARNING:") + "\n" + text }, canvas, 0); +} +void NotificationManager::close_plater_error_notification() +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::PlaterError) { + notification->close(); + } + } +} +void NotificationManager::close_plater_warning_notification(const std::string& text) +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::PlaterWarning && notification->compare_text(_u8L("WARNING:") + "\n" + text)) { + notification->close(); + } + } +} +void NotificationManager::set_all_slicing_errors_gray(bool g) +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingError) { + notification->set_gray(g); + } + } +} +void NotificationManager::set_all_slicing_warnings_gray(bool g) +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingWarning) { + notification->set_gray(g); + } + } +} +void NotificationManager::set_slicing_warning_gray(const std::string& text, bool g) +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingWarning && notification->compare_text(text)) { + notification->set_gray(g); + } + } +} +void NotificationManager::close_slicing_errors_and_warnings() +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingError || notification->get_type() == NotificationType::SlicingWarning) { + notification->close(); + } + } +} +void NotificationManager::push_slicing_complete_notification(GLCanvas3D& canvas, int timestamp, bool large) +{ + std::string hypertext; + int time = 10; + if (has_error_notification()) + return; + if (large) { + hypertext = _u8L("Export G-Code."); + time = 0; + } + NotificationData data{ NotificationType::SlicingComplete, NotificationLevel::RegularNotification, time, _u8L("Slicing finished."), hypertext }; + + NotificationManager::SlicingCompleteLargeNotification* notification = new NotificationManager::SlicingCompleteLargeNotification(data, m_next_id++, m_evt_handler, large); + if (!push_notification_data(notification, canvas, timestamp)) { + delete notification; + } +} +void NotificationManager::set_slicing_complete_print_time(std::string info) +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingComplete) { + dynamic_cast(notification)->set_print_info(info); + break; + } + } +} +void NotificationManager::set_slicing_complete_large(bool large) +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingComplete) { + dynamic_cast(notification)->set_large(large); + break; + } + } +} +void NotificationManager::close_notification_of_type(const NotificationType type) +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == type) { + notification->close(); + } + } +} +void NotificationManager::compare_warning_oids(const std::vector& living_oids) +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::SlicingWarning) { + auto w = dynamic_cast(notification); + bool found = false; + for (size_t oid : living_oids) { + if (w->get_object_id() == oid) { + found = true; + break; + } + } + if (!found) + notification->close(); + } + } +} +bool NotificationManager::push_notification_data(const NotificationData ¬ification_data, GLCanvas3D& canvas, int timestamp) +{ + PopNotification* n = new PopNotification(notification_data, m_next_id++, m_evt_handler); + bool r = push_notification_data(n, canvas, timestamp); + if (!r) + delete n; + return r; +} +bool NotificationManager::push_notification_data(NotificationManager::PopNotification* notification, GLCanvas3D& canvas, int timestamp) +{ + // if timestamped notif, push only new one + if (timestamp != 0) { + if (m_used_timestamps.find(timestamp) == m_used_timestamps.end()) { + m_used_timestamps.insert(timestamp); + } else { + return false; + } + } + if (!this->find_older(notification)) { + m_pop_notifications.emplace_back(notification); + canvas.request_extra_frame(); + return true; + } else { + m_pop_notifications.back()->update(notification->get_data()); + canvas.request_extra_frame(); + return false; + } +} +void NotificationManager::render_notifications(GLCanvas3D& canvas) +{ + float last_x = 0.0f; + float current_height = 0.0f; + bool request_next_frame = false; + bool render_main = false; + bool hovered = false; + sort_notifications(); + // iterate thru notifications and render them / erease them + for (auto it = m_pop_notifications.begin(); it != m_pop_notifications.end();) { + if ((*it)->get_finished()) { + delete (*it); + it = m_pop_notifications.erase(it); + } else { + (*it)->set_paused(m_hovered); + PopNotification::RenderResult res = (*it)->render(canvas, last_x); + if (res != PopNotification::RenderResult::Finished) { + last_x = (*it)->get_top() + GAP_WIDTH; + current_height = std::max(current_height, (*it)->get_current_top()); + render_main = true; + } + if (res == PopNotification::RenderResult::Countdown || res == PopNotification::RenderResult::ClosePending || res == PopNotification::RenderResult::Finished) + request_next_frame = true; + if (res == PopNotification::RenderResult::Hovered) + hovered = true; + ++it; + } + } + m_hovered = hovered; + + //actualizate timers and request frame if needed + wxWindow* p = dynamic_cast (wxGetApp().plater()); + while (p->GetParent()) + p = p->GetParent(); + wxTopLevelWindow* top_level_wnd = dynamic_cast(p); + if (!top_level_wnd->IsActive()) + return; + + if (!m_hovered && m_last_time < wxGetLocalTime()) + { + if (wxGetLocalTime() - m_last_time == 1) + { + for(auto notification : m_pop_notifications) + { + notification->substract_remaining_time(); + } + } + m_last_time = wxGetLocalTime(); + } + + if (request_next_frame) + canvas.request_extra_frame(); +} + + +void NotificationManager::sort_notifications() +{ + std::sort(m_pop_notifications.begin(), m_pop_notifications.end(), [](PopNotification* n1, PopNotification* n2) { + int n1l = (int)n1->get_data().level; + int n2l = (int)n2->get_data().level; + if (n1l == n2l && n1->get_is_gray() && !n2->get_is_gray()) + return true; + return (n1l < n2l); + }); +} + +bool NotificationManager::find_older(NotificationManager::PopNotification* notification) +{ + NotificationType type = notification->get_type(); + std::string text = notification->get_data().text1; + for (auto it = m_pop_notifications.begin(); it != m_pop_notifications.end(); ++it) { + if((*it)->get_type() == type && !(*it)->get_finished()) { + if (type == NotificationType::CustomNotification || type == NotificationType::PlaterWarning) { + if (!(*it)->compare_text(text)) + continue; + }else if (type == NotificationType::SlicingWarning) { + auto w1 = dynamic_cast(notification); + auto w2 = dynamic_cast(*it); + if (w1 != nullptr && w2 != nullptr) { + if (!(*it)->compare_text(text) || w1->get_object_id() != w2->get_object_id()) { + continue; + } + } else { + continue; + } + } + + if (it != m_pop_notifications.end() - 1) + std::rotate(it, it + 1, m_pop_notifications.end()); + return true; + } + } + return false; +} + +void NotificationManager::set_in_preview(bool preview) +{ + m_in_preview = preview; + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::PlaterWarning) + notification->hide(preview); + } +} +bool NotificationManager::has_error_notification() +{ + for (PopNotification* notification : m_pop_notifications) { + if (notification->get_data().level == NotificationLevel::ErrorNotification) + return true; + } + return false; +} + +void NotificationManager::dpi_changed() +{ + +} + +}//namespace GUI +}//namespace Slic3r diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp new file mode 100644 index 0000000000..a11d08394c --- /dev/null +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -0,0 +1,273 @@ +#ifndef slic3r_GUI_NotificationManager_hpp_ +#define slic3r_GUI_NotificationManager_hpp_ + +#include "Event.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include + +namespace Slic3r { +namespace GUI { + +using EjectDriveNotificationClickedEvent = SimpleEvent; +wxDECLARE_EVENT(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, EjectDriveNotificationClickedEvent); +using ExportGcodeNotificationClickedEvent = SimpleEvent; +wxDECLARE_EVENT(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, ExportGcodeNotificationClickedEvent); +using PresetUpdateAviableClickedEvent = SimpleEvent; +wxDECLARE_EVENT(EVT_PRESET_UPDATE_AVIABLE_CLICKED, PresetUpdateAviableClickedEvent); + +class GLCanvas3D; +class ImGuiWrapper; + +enum class NotificationType +{ + CustomNotification, + SlicingComplete, + SlicingNotPossible, + ExportToRemovableFinished, + Mouse3dDisconnected, + Mouse3dConnected, + NewPresetsAviable, + NewAppAviable, + PresetUpdateAviable, + LoadingFailed, + ValidateError, // currently not used - instead Slicing error is used for both slicing and validate errors + SlicingError, + SlicingWarning, + PlaterError, + PlaterWarning, + ApplyError + +}; +class NotificationManager +{ +public: + enum class NotificationLevel : int + { + ErrorNotification = 4, + WarningNotification = 3, + ImportantNotification = 2, + RegularNotification = 1, + }; + // duration 0 means not disapearing + struct NotificationData { + NotificationType type; + NotificationLevel level; + const int duration; + const std::string text1; + const std::string hypertext = std::string(); + const std::string text2 = std::string(); + }; + + //Pop notification - shows only once to user. + class PopNotification + { + public: + enum class RenderResult + { + Finished, + ClosePending, + Static, + Countdown, + Hovered + }; + PopNotification(const NotificationData &n, const int id, wxEvtHandler* evt_handler); + virtual ~PopNotification(); + RenderResult render(GLCanvas3D& canvas, const float& initial_y); + // close will dissapear notification on next render + void close() { m_close_pending = true; } + // data from newer notification of same type + void update(const NotificationData& n); + bool get_finished() const { return m_finished; } + // returns top after movement + float get_top() const { return m_top_y; } + //returns top in actual frame + float get_current_top() const { return m_top_y; } + const NotificationType get_type() const { return m_data.type; } + const NotificationData get_data() const { return m_data; } + const bool get_is_gray() const { return m_is_gray; } + // Call equals one second down + void substract_remaining_time() { m_remaining_time--; } + void set_gray(bool g) { m_is_gray = g; } + void set_paused(bool p) { m_paused = p; } + bool compare_text(const std::string& text); + void hide(bool h) { m_hidden = h; } + protected: + // Call after every size change + void init(); + // Calculetes correct size but not se it in imgui! + virtual void set_next_window_size(ImGuiWrapper& imgui); + virtual void render_text(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x , const float win_pos_y); + void 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); + void render_countdown(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x , const float win_pos_y); + void render_hypertext(ImGuiWrapper& imgui, + const float text_x, const float text_y, + const std::string text, + bool more = false); + void render_left_sign(ImGuiWrapper& imgui); + void render_minimize_button(ImGuiWrapper& imgui, + const float win_pos_x, const float win_pos_y); + void on_text_click(); + + const NotificationData m_data; + + int m_id; + bool m_initialized { false }; + // Main text + std::string m_text1; + // Clickable text + std::string m_hypertext; + // Aditional text after hypertext - currently not used + std::string m_text2; + // Countdown variables + long m_remaining_time; + bool m_counting_down; + long m_last_remaining_time; + bool m_paused { false }; + int m_countdown_frame { 0 }; + bool m_fading_out { false }; + // total time left when fading beggins + float m_fading_time { 0.0f }; + float m_current_fade_opacity { 1.f }; + // If hidden the notif is alive but not visible to user + bool m_hidden { false }; + // m_finished = true - does not render, marked to delete + bool m_finished { false }; + // Will go to m_finished next render + bool m_close_pending { false }; + // variables to count positions correctly + float m_window_width_offset; + float m_left_indentation; + // Total size of notification window - varies based on monitor + float m_window_height { 56.0f }; + float m_window_width { 450.0f }; + //Distance from bottom of notifications to top of this notification + float m_top_y { 0.0f }; + + // Height of text + // Used as basic scaling unit! + float m_line_height; + std::vector m_endlines; + // Gray are f.e. eorrors when its uknown if they are still valid + bool m_is_gray { false }; + //if multiline = true, notification is showing all lines(>2) + bool m_multiline { false }; + int m_lines_count{ 1 }; + wxEvtHandler* m_evt_handler; + }; + + class SlicingCompleteLargeNotification : public PopNotification + { + public: + SlicingCompleteLargeNotification(const NotificationData& n, const int id, wxEvtHandler* evt_handler, bool largeds); + void set_large(bool l); + bool get_large() { return m_is_large; } + + void set_print_info(std::string info); + protected: + virtual void render_text(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) + override; + + bool m_is_large; + bool m_has_print_info { false }; + std::string m_print_info { std::string() }; + }; + + class SlicingWarningNotification : public PopNotification + { + public: + SlicingWarningNotification(const NotificationData& n, const int id, wxEvtHandler* evt_handler) : PopNotification(n, id, evt_handler) {} + void set_object_id(size_t id) { object_id = id; } + const size_t get_object_id() { return object_id; } + void set_warning_step(int ws) { warning_step = ws; } + const int get_warning_step() { return warning_step; } + protected: + size_t object_id; + int warning_step; + }; + + NotificationManager(wxEvtHandler* evt_handler); + ~NotificationManager(); + + + // only type means one of basic_notification (see below) + void push_notification(const NotificationType type, GLCanvas3D& canvas, int timestamp = 0); + // only text means Undefined type + void push_notification(const std::string& text, GLCanvas3D& canvas, int timestamp = 0); + void push_notification(const std::string& text, NotificationLevel level, GLCanvas3D& canvas, int timestamp = 0); + // creates Slicing Error notification with custom text + void push_slicing_error_notification(const std::string& text, GLCanvas3D& canvas); + // creates Slicing Warning notification with custom text + void push_slicing_warning_notification(const std::string& text, bool gray, GLCanvas3D& canvas, size_t oid, int warning_step); + // marks slicing errors as gray + void set_all_slicing_errors_gray(bool g); + // marks slicing warings as gray + void set_all_slicing_warnings_gray(bool g); + void set_slicing_warning_gray(const std::string& text, bool g); + // imidietly stops showing slicing errors + void close_slicing_errors_and_warnings(); + void compare_warning_oids(const std::vector& living_oids); + void push_plater_error_notification(const std::string& text, GLCanvas3D& canvas); + void push_plater_warning_notification(const std::string& text, GLCanvas3D& canvas); + void close_plater_error_notification(); + void close_plater_warning_notification(const std::string& text); + // creates special notification slicing complete + // if large = true prints printing time and export button + void push_slicing_complete_notification(GLCanvas3D& canvas, int timestamp, bool large); + void set_slicing_complete_print_time(std::string info); + void set_slicing_complete_large(bool large); + // renders notifications in queue and deletes expired ones + void render_notifications(GLCanvas3D& canvas); + // finds and closes all notifications of given type + void close_notification_of_type(const NotificationType type); + void dpi_changed(); + void set_in_preview(bool preview); +private: + //pushes notification into the queue of notifications that are rendered + //can be used to create custom notification + bool push_notification_data(const NotificationData& notification_data, GLCanvas3D& canvas, int timestamp); + bool push_notification_data(NotificationManager::PopNotification* notification, GLCanvas3D& canvas, int timestamp); + //finds older notification of same type and moves it to the end of queue. returns true if found + bool find_older(NotificationManager::PopNotification* notification); + void sort_notifications(); + bool has_error_notification(); + + wxEvtHandler* m_evt_handler; + std::deque m_pop_notifications; + int m_next_id { 1 }; + long m_last_time { 0 }; + bool m_hovered { false }; + //timestamps used for slining finished - notification could be gone so it needs to be stored here + std::unordered_set m_used_timestamps; + bool m_in_preview; + + //prepared (basic) notifications + const std::vector basic_notifications = { + {NotificationType::SlicingNotPossible, NotificationLevel::RegularNotification, 10, _u8L("Slicing is not possible.")}, + {NotificationType::ExportToRemovableFinished, NotificationLevel::ImportantNotification, 0, _u8L("Exporting finished."), _u8L("Eject drive.") }, + {NotificationType::Mouse3dDisconnected, NotificationLevel::RegularNotification, 10, _u8L("3D Mouse disconnected.") }, + {NotificationType::Mouse3dConnected, NotificationLevel::RegularNotification, 5, _u8L("3D Mouse connected.") }, + {NotificationType::NewPresetsAviable, NotificationLevel::ImportantNotification, 20, _u8L("New Presets are available."), _u8L("See here.") }, + {NotificationType::PresetUpdateAviable, NotificationLevel::ImportantNotification, 20, _u8L("Configuration update is available."), _u8L("See more.")}, + {NotificationType::NewAppAviable, NotificationLevel::ImportantNotification, 20, _u8L("New version is available."), _u8L("See Releases page.")}, + //{NotificationType::NewAppAviable, NotificationLevel::ImportantNotification, 20, _u8L("New vesion of PrusaSlicer is available.", _u8L("Download page.") }, + //{NotificationType::LoadingFailed, NotificationLevel::RegularNotification, 20, _u8L("Loading of model has Failed") }, + //{NotificationType::DeviceEjected, NotificationLevel::RegularNotification, 10, _u8L("Removable device has been safely ejected")} // if we want changeble text (like here name of device), we need to do it as CustomNotification + }; +}; + +}//namespace GUI +}//namespace Slic3r + +#endif //slic3r_GUI_NotificationManager_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 336475d2e0..79fedfa527 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -1549,245 +1549,6 @@ void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bo DeleteWarningIcon(child); } } -/* -} -} -*/ -//----------------------------------------------------------------------------- -// DataViewBitmapText -//----------------------------------------------------------------------------- - -wxIMPLEMENT_DYNAMIC_CLASS(DataViewBitmapText, wxObject) - -IMPLEMENT_VARIANT_OBJECT(DataViewBitmapText) - -// --------------------------------------------------------- -// BitmapTextRenderer -// --------------------------------------------------------- - -#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING -BitmapTextRenderer::BitmapTextRenderer(wxDataViewCellMode mode /*= wxDATAVIEW_CELL_EDITABLE*/, - int align /*= wxDVR_DEFAULT_ALIGNMENT*/): -wxDataViewRenderer(wxT("PrusaDataViewBitmapText"), mode, align) -{ - SetMode(mode); - SetAlignment(align); -} -#endif // ENABLE_NONCUSTOM_DATA_VIEW_RENDERING - -bool BitmapTextRenderer::SetValue(const wxVariant &value) -{ - m_value << value; - return true; -} - -bool BitmapTextRenderer::GetValue(wxVariant& WXUNUSED(value)) const -{ - return false; -} - -#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING && wxUSE_ACCESSIBILITY -wxString BitmapTextRenderer::GetAccessibleDescription() const -{ - return m_value.GetText(); -} -#endif // wxUSE_ACCESSIBILITY && ENABLE_NONCUSTOM_DATA_VIEW_RENDERING - -bool BitmapTextRenderer::Render(wxRect rect, wxDC *dc, int state) -{ - int xoffset = 0; - - const wxBitmap& icon = m_value.GetBitmap(); - if (icon.IsOk()) - { -#ifdef __APPLE__ - wxSize icon_sz = icon.GetScaledSize(); -#else - wxSize icon_sz = icon.GetSize(); -#endif - dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon_sz.y) / 2); - xoffset = icon_sz.x + 4; - } - - RenderText(m_value.GetText(), xoffset, rect, dc, state); - - return true; -} - -wxSize BitmapTextRenderer::GetSize() const -{ - if (!m_value.GetText().empty()) - { - wxSize size = GetTextExtent(m_value.GetText()); - - if (m_value.GetBitmap().IsOk()) - size.x += m_value.GetBitmap().GetWidth() + 4; - return size; - } - return wxSize(80, 20); -} - - -wxWindow* BitmapTextRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelRect, const wxVariant& value) -{ - wxDataViewCtrl* const dv_ctrl = GetOwner()->GetOwner(); - ObjectDataViewModel* const model = dynamic_cast(dv_ctrl->GetModel()); - - if ( !(model->GetItemType(dv_ctrl->GetSelection()) & (itVolume | itObject)) ) - return nullptr; - - DataViewBitmapText data; - data << value; - - m_was_unusable_symbol = false; - - wxPoint position = labelRect.GetPosition(); - if (data.GetBitmap().IsOk()) { - const int bmp_width = data.GetBitmap().GetWidth(); - position.x += bmp_width; - labelRect.SetWidth(labelRect.GetWidth() - bmp_width); - } - - wxTextCtrl* text_editor = new wxTextCtrl(parent, wxID_ANY, data.GetText(), - position, labelRect.GetSize(), wxTE_PROCESS_ENTER); - text_editor->SetInsertionPointEnd(); - text_editor->SelectAll(); - - return text_editor; -} - -bool BitmapTextRenderer::GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) -{ - wxTextCtrl* text_editor = wxDynamicCast(ctrl, wxTextCtrl); - if (!text_editor || text_editor->GetValue().IsEmpty()) - return false; - - std::string chosen_name = Slic3r::normalize_utf8_nfc(text_editor->GetValue().ToUTF8()); - const char* unusable_symbols = "<>:/\\|?*\""; - for (size_t i = 0; i < std::strlen(unusable_symbols); i++) { - if (chosen_name.find_first_of(unusable_symbols[i]) != std::string::npos) { - m_was_unusable_symbol = true; - return false; - } - } - - // The icon can't be edited so get its old value and reuse it. - wxVariant valueOld; - GetView()->GetModel()->GetValue(valueOld, m_item, colName); - - DataViewBitmapText bmpText; - bmpText << valueOld; - - // But replace the text with the value entered by user. - bmpText.SetText(text_editor->GetValue()); - - value << bmpText; - return true; -} - -// ---------------------------------------------------------------------------- -// BitmapChoiceRenderer -// ---------------------------------------------------------------------------- - -bool BitmapChoiceRenderer::SetValue(const wxVariant& value) -{ - m_value << value; - return true; -} - -bool BitmapChoiceRenderer::GetValue(wxVariant& value) const -{ - value << m_value; - return true; -} - -bool BitmapChoiceRenderer::Render(wxRect rect, wxDC* dc, int state) -{ - int xoffset = 0; - - const wxBitmap& icon = m_value.GetBitmap(); - if (icon.IsOk()) - { - dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon.GetHeight()) / 2); - xoffset = icon.GetWidth() + 4; - - if (rect.height==0) - rect.height= icon.GetHeight(); - } - - RenderText(m_value.GetText(), xoffset, rect, dc, state); - - return true; -} - -wxSize BitmapChoiceRenderer::GetSize() const -{ - wxSize sz = GetTextExtent(m_value.GetText()); - - if (m_value.GetBitmap().IsOk()) - sz.x += m_value.GetBitmap().GetWidth() + 4; - - return sz; -} - - -wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelRect, const wxVariant& value) -{ - wxDataViewCtrl* const dv_ctrl = GetOwner()->GetOwner(); - ObjectDataViewModel* const model = dynamic_cast(dv_ctrl->GetModel()); - - if (!(model->GetItemType(dv_ctrl->GetSelection()) & (itVolume | itLayer | itObject))) - return nullptr; - - std::vector icons = get_extruder_color_icons(); - if (icons.empty()) - return nullptr; - - DataViewBitmapText data; - data << value; - - auto c_editor = new wxBitmapComboBox(parent, wxID_ANY, wxEmptyString, - labelRect.GetTopLeft(), wxSize(labelRect.GetWidth(), -1), - 0, nullptr , wxCB_READONLY); - - int i=0; - for (wxBitmap* bmp : icons) { - if (i==0) { - c_editor->Append(_(L("default")), *bmp); - ++i; - } - - c_editor->Append(wxString::Format("%d", i), *bmp); - ++i; - } - c_editor->SetSelection(atoi(data.GetText().c_str())); - - // to avoid event propagation to other sidebar items - c_editor->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { - evt.StopPropagation(); - // FinishEditing grabs new selection and triggers config update. We better call - // it explicitly, automatic update on KILL_FOCUS didn't work on Linux. - this->FinishEditing(); - }); - - return c_editor; -} - -bool BitmapChoiceRenderer::GetValueFromEditorCtrl(wxWindow* ctrl, wxVariant& value) -{ - wxBitmapComboBox* c = (wxBitmapComboBox*)ctrl; - int selection = c->GetSelection(); - if (selection < 0) - return false; - - DataViewBitmapText bmpText; - - bmpText.SetText(c->GetString(selection)); - bmpText.SetBitmap(c->GetItemBitmap(selection)); - - value << bmpText; - return true; -} } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index c184842664..12480139d2 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -2,9 +2,10 @@ #define slic3r_GUI_ObjectDataViewModel_hpp_ #include - #include +#include "ExtraRenderers.hpp" + namespace Slic3r { enum class ModelVolumeType : int; @@ -14,143 +15,6 @@ namespace GUI { typedef double coordf_t; typedef std::pair t_layer_height_range; -// ---------------------------------------------------------------------------- -// DataViewBitmapText: helper class used by BitmapTextRenderer -// ---------------------------------------------------------------------------- - -class DataViewBitmapText : public wxObject -{ -public: - DataViewBitmapText( const wxString &text = wxEmptyString, - const wxBitmap& bmp = wxNullBitmap) : - m_text(text), - m_bmp(bmp) - { } - - DataViewBitmapText(const DataViewBitmapText &other) - : wxObject(), - m_text(other.m_text), - m_bmp(other.m_bmp) - { } - - void SetText(const wxString &text) { m_text = text; } - wxString GetText() const { return m_text; } - void SetBitmap(const wxBitmap &bmp) { m_bmp = bmp; } - const wxBitmap &GetBitmap() const { return m_bmp; } - - bool IsSameAs(const DataViewBitmapText& other) const { - return m_text == other.m_text && m_bmp.IsSameAs(other.m_bmp); - } - - bool operator==(const DataViewBitmapText& other) const { - return IsSameAs(other); - } - - bool operator!=(const DataViewBitmapText& other) const { - return !IsSameAs(other); - } - -private: - wxString m_text; - wxBitmap m_bmp; - - wxDECLARE_DYNAMIC_CLASS(DataViewBitmapText); -}; -DECLARE_VARIANT_OBJECT(DataViewBitmapText) - -// ---------------------------------------------------------------------------- -// BitmapTextRenderer -// ---------------------------------------------------------------------------- -#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING -class BitmapTextRenderer : public wxDataViewRenderer -#else -class BitmapTextRenderer : public wxDataViewCustomRenderer -#endif //ENABLE_NONCUSTOM_DATA_VIEW_RENDERING -{ -public: - BitmapTextRenderer(wxWindow* parent, - wxDataViewCellMode mode = -#ifdef __WXOSX__ - wxDATAVIEW_CELL_INERT -#else - wxDATAVIEW_CELL_EDITABLE -#endif - - , int align = wxDVR_DEFAULT_ALIGNMENT -#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING - ); -#else - ) : - wxDataViewCustomRenderer(wxT("DataViewBitmapText"), mode, align), - m_parent(parent) - {} -#endif //ENABLE_NONCUSTOM_DATA_VIEW_RENDERING - - bool SetValue(const wxVariant& value); - bool GetValue(wxVariant& value) const; -#if ENABLE_NONCUSTOM_DATA_VIEW_RENDERING && wxUSE_ACCESSIBILITY - virtual wxString GetAccessibleDescription() const override; -#endif // wxUSE_ACCESSIBILITY && ENABLE_NONCUSTOM_DATA_VIEW_RENDERING - - virtual bool Render(wxRect cell, wxDC* dc, int state) override; - virtual wxSize GetSize() const override; - - bool HasEditorCtrl() const override - { -#ifdef __WXOSX__ - return false; -#else - return true; -#endif - } - wxWindow* CreateEditorCtrl(wxWindow* parent, - wxRect labelRect, - const wxVariant& value) override; - bool GetValueFromEditorCtrl(wxWindow* ctrl, - wxVariant& value) override; - bool WasCanceled() const { return m_was_unusable_symbol; } - -private: - DataViewBitmapText m_value; - bool m_was_unusable_symbol{ false }; - wxWindow* m_parent{ nullptr }; -}; - - -// ---------------------------------------------------------------------------- -// BitmapChoiceRenderer -// ---------------------------------------------------------------------------- - -class BitmapChoiceRenderer : public wxDataViewCustomRenderer -{ -public: - BitmapChoiceRenderer(wxDataViewCellMode mode = -#ifdef __WXOSX__ - wxDATAVIEW_CELL_INERT -#else - wxDATAVIEW_CELL_EDITABLE -#endif - , int align = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL - ) : wxDataViewCustomRenderer(wxT("DataViewBitmapText"), mode, align) {} - - bool SetValue(const wxVariant& value); - bool GetValue(wxVariant& value) const; - - virtual bool Render(wxRect cell, wxDC* dc, int state) override; - virtual wxSize GetSize() const override; - - bool HasEditorCtrl() const override { return true; } - wxWindow* CreateEditorCtrl(wxWindow* parent, - wxRect labelRect, - const wxVariant& value) override; - bool GetValueFromEditorCtrl(wxWindow* ctrl, - wxVariant& value) override; - -private: - DataViewBitmapText m_value; -}; - - // ---------------------------------------------------------------------------- // ObjectDataViewModelNode: a node inside ObjectDataViewModel // ---------------------------------------------------------------------------- diff --git a/src/slic3r/GUI/OpenGLManager.cpp b/src/slic3r/GUI/OpenGLManager.cpp index bdb005b1e2..13c58f8472 100644 --- a/src/slic3r/GUI/OpenGLManager.cpp +++ b/src/slic3r/GUI/OpenGLManager.cpp @@ -28,6 +28,13 @@ namespace Slic3r { namespace GUI { +// A safe wrapper around glGetString to report a "N/A" string in case glGetString returns nullptr. +inline std::string gl_get_string_safe(GLenum param, const std::string& default_value) +{ + const char* value = (const char*)::glGetString(param); + return std::string((value != nullptr) ? value : default_value); +} + const std::string& OpenGLManager::GLInfo::get_version() const { if (!m_detected) @@ -85,21 +92,10 @@ float OpenGLManager::GLInfo::get_max_anisotropy() const void OpenGLManager::GLInfo::detect() const { - const char* data = (const char*)::glGetString(GL_VERSION); - if (data != nullptr) - m_version = data; - - data = (const char*)::glGetString(GL_SHADING_LANGUAGE_VERSION); - if (data != nullptr) - m_glsl_version = data; - - data = (const char*)::glGetString(GL_VENDOR); - if (data != nullptr) - m_vendor = data; - - data = (const char*)::glGetString(GL_RENDERER); - if (data != nullptr) - m_renderer = data; + m_version = gl_get_string_safe(GL_VERSION, "N/A"); + m_glsl_version = gl_get_string_safe(GL_SHADING_LANGUAGE_VERSION, "N/A"); + m_vendor = gl_get_string_safe(GL_VENDOR, "N/A"); + m_renderer = gl_get_string_safe(GL_RENDERER, "N/A"); glsafe(::glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m_max_tex_size)); @@ -114,13 +110,13 @@ void OpenGLManager::GLInfo::detect() const m_detected = true; } -bool OpenGLManager::GLInfo::is_version_greater_or_equal_to(unsigned int major, unsigned int minor) const +static bool version_greater_or_equal_to(const std::string& version, unsigned int major, unsigned int minor) { - if (!m_detected) - detect(); + if (version == "N/A") + return false; std::vector tokens; - boost::split(tokens, m_version, boost::is_any_of(" "), boost::token_compress_on); + boost::split(tokens, version, boost::is_any_of(" "), boost::token_compress_on); if (tokens.empty()) return false; @@ -145,6 +141,22 @@ bool OpenGLManager::GLInfo::is_version_greater_or_equal_to(unsigned int major, u return gl_minor >= minor; } +bool OpenGLManager::GLInfo::is_version_greater_or_equal_to(unsigned int major, unsigned int minor) const +{ + if (!m_detected) + detect(); + + return version_greater_or_equal_to(m_version, major, minor); +} + +bool OpenGLManager::GLInfo::is_glsl_version_greater_or_equal_to(unsigned int major, unsigned int minor) const +{ + if (!m_detected) + detect(); + + return version_greater_or_equal_to(m_glsl_version, major, minor); +} + std::string OpenGLManager::GLInfo::to_string(bool format_as_html, bool extensions) const { if (!m_detected) @@ -159,15 +171,15 @@ std::string OpenGLManager::GLInfo::to_string(bool format_as_html, bool extension std::string line_end = format_as_html ? "
" : "\n"; out << h2_start << "OpenGL installation" << h2_end << line_end; - out << b_start << "GL version: " << b_end << (m_version.empty() ? "N/A" : m_version) << line_end; - out << b_start << "Vendor: " << b_end << (m_vendor.empty() ? "N/A" : m_vendor) << line_end; - out << b_start << "Renderer: " << b_end << (m_renderer.empty() ? "N/A" : m_renderer) << line_end; - out << b_start << "GLSL version: " << b_end << (m_glsl_version.empty() ? "N/A" : m_glsl_version) << line_end; + out << b_start << "GL version: " << b_end << m_version << line_end; + out << b_start << "Vendor: " << b_end << m_vendor << line_end; + out << b_start << "Renderer: " << b_end << m_renderer << line_end; + out << b_start << "GLSL version: " << b_end << m_glsl_version << line_end; if (extensions) { std::vector extensions_list; - std::string extensions_str = (const char*)::glGetString(GL_EXTENSIONS); + std::string extensions_str = gl_get_string_safe(GL_EXTENSIONS, ""); boost::split(extensions_list, extensions_str, boost::is_any_of(" "), boost::token_compress_off); if (!extensions_list.empty()) @@ -199,6 +211,8 @@ OpenGLManager::OSInfo OpenGLManager::s_os_info; OpenGLManager::~OpenGLManager() { + m_shaders_manager.shutdown(); + #if ENABLE_HACK_CLOSING_ON_OSX_10_9_5 #ifdef __APPLE__ // This is an ugly hack needed to solve the crash happening when closing the application on OSX 10.9.5 with newer wxWidgets @@ -240,19 +254,30 @@ bool OpenGLManager::init_gl() else s_framebuffers_type = EFramebufferType::Unknown; - if (! s_gl_info.is_version_greater_or_equal_to(2, 0)) { - // Complain about the OpenGL version. + bool valid_version = s_gl_info.is_version_greater_or_equal_to(2, 0); + if (!valid_version) { + // Complain about the OpenGL version. wxString message = from_u8((boost::format( _utf8(L("PrusaSlicer requires OpenGL 2.0 capable graphics driver to run correctly, \n" "while OpenGL version %s, render %s, vendor %s was detected."))) % s_gl_info.get_version() % s_gl_info.get_renderer() % s_gl_info.get_vendor()).str()); - message += "\n"; + message += "\n"; message += _L("You may need to update your graphics card driver."); #ifdef _WIN32 - message += "\n"; + message += "\n"; message += _L("As a workaround, you may run PrusaSlicer with a software rendered 3D graphics by running prusa-slicer.exe with the --sw_renderer parameter."); #endif wxMessageBox(message, wxString("PrusaSlicer - ") + _L("Unsupported OpenGL version"), wxOK | wxICON_ERROR); } + + if (valid_version) { + // load shaders + auto [result, error] = m_shaders_manager.init(); + if (!result) { + wxString message = from_u8((boost::format( + _utf8(L("Unable to load the following shaders:\n%s"))) % error).str()); + wxMessageBox(message, wxString("PrusaSlicer - ") + _L("Error loading shaders"), wxOK | wxICON_ERROR); + } + } } return true; @@ -260,8 +285,7 @@ bool OpenGLManager::init_gl() wxGLContext* OpenGLManager::init_glcontext(wxGLCanvas& canvas) { - if (m_context == nullptr) - { + if (m_context == nullptr) { m_context = new wxGLContext(&canvas); #if ENABLE_HACK_CLOSING_ON_OSX_10_9_5 diff --git a/src/slic3r/GUI/OpenGLManager.hpp b/src/slic3r/GUI/OpenGLManager.hpp index 9d7ee5babb..c89cdf3a61 100644 --- a/src/slic3r/GUI/OpenGLManager.hpp +++ b/src/slic3r/GUI/OpenGLManager.hpp @@ -1,6 +1,8 @@ #ifndef slic3r_OpenGLManager_hpp_ #define slic3r_OpenGLManager_hpp_ +#include "GLShadersManager.hpp" + class wxWindow; class wxGLCanvas; class wxGLContext; @@ -41,6 +43,7 @@ public: float get_max_anisotropy() const; bool is_version_greater_or_equal_to(unsigned int major, unsigned int minor) const; + bool is_glsl_version_greater_or_equal_to(unsigned int major, unsigned int minor) const; std::string to_string(bool format_as_html, bool extensions) const; @@ -70,6 +73,7 @@ private: bool m_gl_initialized{ false }; wxGLContext* m_context{ nullptr }; + GLShadersManager m_shaders_manager; static GLInfo s_gl_info; #if ENABLE_HACK_CLOSING_ON_OSX_10_9_5 #ifdef __APPLE__ @@ -86,9 +90,11 @@ public: ~OpenGLManager(); bool init_gl(); - wxGLContext* init_glcontext(wxGLCanvas& canvas); + GLShaderProgram* get_shader(const std::string& shader_name) { return m_shaders_manager.get_shader(shader_name); } + GLShaderProgram* get_current_shader() { return m_shaders_manager.get_current_shader(); } + static bool are_compressed_textures_supported() { return s_compressed_textures_supported; } static bool can_multisample() { return s_multisample == EMultisampleState::Enabled; } static bool are_framebuffers_supported() { return (s_framebuffers_type != EFramebufferType::Unknown); } diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 819c214a85..14defd9a96 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -7,6 +7,7 @@ #include #include #include +#include "libslic3r/Exception.hpp" #include "libslic3r/Utils.hpp" #include "I18N.hpp" @@ -64,7 +65,7 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co break; case coNone: break; default: - throw /*//!ConfigGUITypeError("")*/std::logic_error("This control doesn't exist till now"); break; + throw Slic3r::LogicError("This control doesn't exist till now"); break; } } // Grab a reference to fields for convenience @@ -466,7 +467,8 @@ void ConfigOptionsGroup::back_to_config_value(const DynamicPrintConfig& config, } else if (m_opt_map.find(opt_key) == m_opt_map.end() || // This option don't have corresponded field - opt_key == "bed_shape" || opt_key == "compatible_printers" || opt_key == "compatible_prints" ) { + opt_key == "bed_shape" || opt_key == "filament_ramming_parameters" || + opt_key == "compatible_printers" || opt_key == "compatible_prints" ) { value = get_config_value(config, opt_key); change_opt_value(*m_config, opt_key, value); return; @@ -619,7 +621,7 @@ boost::any ConfigOptionsGroup::config_value(const std::string& opt_key, int opt_ // Aggregate the strings the old way. // Currently used for the post_process config value only. if (opt_index != -1) - throw std::out_of_range("Can't deserialize option indexed value"); + throw Slic3r::OutOfRange("Can't deserialize option indexed value"); // return join(';', m_config->get(opt_key)}); return get_config_value(*m_config, opt_key); } @@ -699,6 +701,10 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config ret = config.option(opt_key)->values; break; } + if (opt_key == "filament_ramming_parameters") { + ret = config.opt_string(opt_key, static_cast(idx)); + break; + } if (config.option(opt_key)->values.empty()) ret = text_value; else if (opt->gui_flags.compare("serialized") == 0) { @@ -729,31 +735,34 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config opt_key == "fill_pattern" ) { ret = static_cast(config.option>(opt_key)->value); } - else if (opt_key.compare("ironing_type") == 0 ) { + else if (opt_key == "ironing_type") { ret = static_cast(config.option>(opt_key)->value); } - else if (opt_key.compare("gcode_flavor") == 0 ) { + else if (opt_key == "gcode_flavor") { ret = static_cast(config.option>(opt_key)->value); } - else if (opt_key.compare("support_material_pattern") == 0) { + else if (opt_key == "support_material_pattern") { ret = static_cast(config.option>(opt_key)->value); } - else if (opt_key.compare("seam_position") == 0) { + else if (opt_key == "seam_position") { ret = static_cast(config.option>(opt_key)->value); } - else if (opt_key.compare("host_type") == 0) { + else if (opt_key == "host_type") { ret = static_cast(config.option>(opt_key)->value); } - else if (opt_key.compare("display_orientation") == 0) { + else if (opt_key == "display_orientation") { ret = static_cast(config.option>(opt_key)->value); } - else if (opt_key.compare("support_pillar_connection_mode") == 0) { + else if (opt_key == "support_pillar_connection_mode") { ret = static_cast(config.option>(opt_key)->value); } + else if (opt_key == "printhost_authorization_type") { + ret = static_cast(config.option>(opt_key)->value); + } } break; case coPoints: - if (opt_key.compare("bed_shape") == 0) + if (opt_key == "bed_shape") ret = config.option(opt_key)->values; else ret = config.option(opt_key)->get_at(idx); diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index 2e6f9aa0f4..edd4a15bc9 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -149,6 +149,13 @@ public: return true; } + void show_field(const t_config_option_key& opt_key, bool show = true) { + Field* field = get_field(opt_key); + field->getWindow()->Show(show); + field->getLabel()->Show(show); + } + void hide_field(const t_config_option_key& opt_key) { show_field(opt_key, false); } + void set_name(const wxString& new_name) { stb->SetLabel(new_name); } diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp new file mode 100644 index 0000000000..3d832ae569 --- /dev/null +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -0,0 +1,564 @@ +#include "PhysicalPrinterDialog.hpp" +#include "PresetComboBoxes.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/PresetBundle.hpp" + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "MainFrame.hpp" +#include "format.hpp" +#include "Tab.hpp" +#include "wxExtensions.hpp" +#include "PrintHostDialogs.hpp" +#include "../Utils/ASCIIFolding.hpp" +#include "../Utils/PrintHost.hpp" +#include "../Utils/FixModelByWin10.hpp" +#include "../Utils/UndoRedo.hpp" +#include "RemovableDriveManager.hpp" +#include "BitmapCache.hpp" +#include "BonjourDialog.hpp" + +using Slic3r::GUI::format_wxstr; + +//static const std::pair THUMBNAIL_SIZE_3MF = { 256, 256 }; + +namespace Slic3r { +namespace GUI { + +#define BORDER_W 10 + +//------------------------------------------ +// PresetForPrinter +//------------------------------------------ + +PresetForPrinter::PresetForPrinter(PhysicalPrinterDialog* parent, const std::string& preset_name) : + m_parent(parent) +{ + m_sizer = new wxBoxSizer(wxVERTICAL); + + m_delete_preset_btn = new ScalableButton(parent, wxID_ANY, "cross", "", wxDefaultSize, wxDefaultPosition, /*wxBU_LEFT | */wxBU_EXACTFIT); + m_delete_preset_btn->SetFont(wxGetApp().normal_font()); + m_delete_preset_btn->SetToolTip(_L("Delete this preset from this printer device")); + m_delete_preset_btn->Bind(wxEVT_BUTTON, &PresetForPrinter::DeletePreset, this); + + m_presets_list = new PresetComboBox(parent, Preset::TYPE_PRINTER); + m_presets_list->set_printer_technology(parent->get_printer_technology()); + + m_presets_list->set_selection_changed_function([this](int selection) { + std::string selected_string = Preset::remove_suffix_modified(m_presets_list->GetString(selection).ToUTF8().data()); + Preset* preset = wxGetApp().preset_bundle->printers.find_preset(selected_string); + assert(preset); + Preset& edited_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + if (preset->name == edited_preset.name) + preset = &edited_preset; + + // if created physical printer doesn't have any settings, use the settings from the selected preset + if (m_parent->get_printer()->has_empty_config()) { + // update Print Host upload from the selected preset + m_parent->get_printer()->update_from_preset(*preset); + // update values in parent (PhysicalPrinterDialog) + m_parent->update(); + } + + // update PrinterTechnology if it was changed + if (m_presets_list->set_printer_technology(preset->printer_technology())) + m_parent->set_printer_technology(preset->printer_technology()); + + update_full_printer_name(); + }); + m_presets_list->update(preset_name); + + m_info_line = new wxStaticText(parent, wxID_ANY, _L("This printer will be shown in the presets list as") + ":"); + + m_full_printer_name = new wxStaticText(parent, wxID_ANY, ""); + m_full_printer_name->SetFont(wxGetApp().bold_font()); + + wxBoxSizer* preset_sizer = new wxBoxSizer(wxHORIZONTAL); + preset_sizer->Add(m_presets_list , 1, wxEXPAND); + preset_sizer->Add(m_delete_preset_btn , 0, wxEXPAND | wxLEFT, BORDER_W); + + wxBoxSizer* name_sizer = new wxBoxSizer(wxHORIZONTAL); + name_sizer->Add(m_info_line, 0, wxEXPAND); + name_sizer->Add(m_full_printer_name, 0, wxEXPAND | wxLEFT, BORDER_W); + + m_sizer->Add(preset_sizer , 0, wxEXPAND); + m_sizer->Add(name_sizer, 0, wxEXPAND); +} + +PresetForPrinter::~PresetForPrinter() +{ + m_presets_list->Destroy(); + m_delete_preset_btn->Destroy(); + m_info_line->Destroy(); + m_full_printer_name->Destroy(); +} + +void PresetForPrinter::DeletePreset(wxEvent& event) +{ + m_parent->DeletePreset(this); +} + +void PresetForPrinter::update_full_printer_name() +{ + wxString printer_name = m_parent->get_printer_name(); + wxString preset_name = m_presets_list->GetString(m_presets_list->GetSelection()); + + m_full_printer_name->SetLabelText(printer_name + " * " + preset_name); +} + +std::string PresetForPrinter::get_preset_name() +{ + return into_u8(m_presets_list->GetString(m_presets_list->GetSelection())); +} + +void PresetForPrinter::SuppressDelete() +{ + m_delete_preset_btn->Enable(false); + + // this case means that now we have only one related preset for the printer + // So, allow any selection + m_presets_list->set_printer_technology(ptAny); + m_presets_list->update(); +} + +void PresetForPrinter::AllowDelete() +{ + if (!m_delete_preset_btn->IsEnabled()) + m_delete_preset_btn->Enable(); + + m_presets_list->set_printer_technology(m_parent->get_printer_technology()); + m_presets_list->update(); +} + +void PresetForPrinter::msw_rescale() +{ + m_presets_list->msw_rescale(); + m_delete_preset_btn->msw_rescale(); +} + + +//------------------------------------------ +// PhysicalPrinterDialog +//------------------------------------------ + +PhysicalPrinterDialog::PhysicalPrinterDialog(wxString printer_name) : + DPIDialog(NULL, wxID_ANY, _L("Physical Printer"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), -1), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + m_printer("", wxGetApp().preset_bundle->physical_printers.default_config()) +{ + SetFont(wxGetApp().normal_font()); + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + m_default_name = _L("Type here the name of your printer device"); + bool new_printer = true; + + if (printer_name.IsEmpty()) + printer_name = m_default_name; + else { + std::string full_name = into_u8(printer_name); + printer_name = from_u8(PhysicalPrinter::get_short_name(full_name)); + new_printer = false; + } + + wxStaticText* label_top = new wxStaticText(this, wxID_ANY, _L("Descriptive name for the printer device") + ":"); + + m_add_preset_btn = new ScalableButton(this, wxID_ANY, "add_copies", "", wxDefaultSize, wxDefaultPosition, /*wxBU_LEFT | */wxBU_EXACTFIT); + m_add_preset_btn->SetFont(wxGetApp().normal_font()); + m_add_preset_btn->SetToolTip(_L("Add preset for this printer device")); + m_add_preset_btn->Bind(wxEVT_BUTTON, &PhysicalPrinterDialog::AddPreset, this); + + m_printer_name = new wxTextCtrl(this, wxID_ANY, printer_name, wxDefaultPosition, wxDefaultSize); + m_printer_name->Bind(wxEVT_TEXT, [this](wxEvent&) { this->update_full_printer_names(); }); + + PhysicalPrinterCollection& printers = wxGetApp().preset_bundle->physical_printers; + PhysicalPrinter* printer = printers.find_printer(into_u8(printer_name)); + if (!printer) { + const Preset& preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + m_printer = PhysicalPrinter(into_u8(printer_name), m_printer.config, preset); + // if printer_name is empty it means that new printer is created, so enable all items in the preset list + m_presets.emplace_back(new PresetForPrinter(this, preset.name)); + } + else + { + const std::set& preset_names = printer->get_preset_names(); + for (const std::string& preset_name : preset_names) + m_presets.emplace_back(new PresetForPrinter(this, preset_name)); + m_printer = *printer; + } + + if (m_presets.size() == 1) + m_presets.front()->SuppressDelete(); + + update_full_printer_names(); + + m_config = &m_printer.config; + + m_optgroup = new ConfigOptionsGroup(this, _L("Print Host upload"), m_config); + build_printhost_settings(m_optgroup); + + wxStdDialogButtonSizer* btns = this->CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxButton* btnOK = static_cast(this->FindWindowById(wxID_OK, this)); + btnOK->Bind(wxEVT_BUTTON, &PhysicalPrinterDialog::OnOK, this); + + wxBoxSizer* nameSizer = new wxBoxSizer(wxHORIZONTAL); + nameSizer->Add(m_printer_name, 1, wxEXPAND); + nameSizer->Add(m_add_preset_btn, 0, wxEXPAND | wxLEFT, BORDER_W); + + m_presets_sizer = new wxBoxSizer(wxVERTICAL); + for (PresetForPrinter* preset : m_presets) + m_presets_sizer->Add(preset->sizer(), 1, wxEXPAND | wxTOP, BORDER_W); + + wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); + + topSizer->Add(label_top , 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, BORDER_W); + topSizer->Add(nameSizer , 0, wxEXPAND | wxLEFT | wxRIGHT, BORDER_W); + topSizer->Add(m_presets_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, BORDER_W); + topSizer->Add(m_optgroup->sizer , 1, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, BORDER_W); + topSizer->Add(btns , 0, wxEXPAND | wxALL, BORDER_W); + + SetSizer(topSizer); + topSizer->SetSizeHints(this); + + if (new_printer) { + m_printer_name->SetFocus(); + m_printer_name->SelectAll(); + } +} + +PhysicalPrinterDialog::~PhysicalPrinterDialog() +{ + for (PresetForPrinter* preset : m_presets) { + delete preset; + preset = nullptr; + } +} + +void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgroup) +{ + m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (opt_key == "printhost_authorization_type") + this->update(); + }; + + m_optgroup->append_single_option_line("host_type"); + + auto create_sizer_with_btn = [this](wxWindow* parent, ScalableButton** btn, const std::string& icon_name, const wxString& label) { + *btn = new ScalableButton(parent, wxID_ANY, icon_name, label, wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); + (*btn)->SetFont(wxGetApp().normal_font()); + + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(*btn); + return sizer; + }; + + auto printhost_browse = [=](wxWindow* parent) + { + auto sizer = create_sizer_with_btn(parent, &m_printhost_browse_btn, "browse", _L("Browse") + " " + dots); + m_printhost_browse_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) { + BonjourDialog dialog(this, Preset::printer_technology(m_printer.config)); + if (dialog.show_and_lookup()) { + m_optgroup->set_value("print_host", std::move(dialog.get_selected()), true); + m_optgroup->get_field("print_host")->field_changed(); + } + }); + + return sizer; + }; + + auto print_host_test = [=](wxWindow* parent) { + auto sizer = create_sizer_with_btn(parent, &m_printhost_test_btn, "test", _L("Test")); + + m_printhost_test_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + std::unique_ptr host(PrintHost::get_print_host(m_config)); + if (!host) { + const wxString text = _L("Could not get a valid Printer Host reference"); + show_error(this, text); + return; + } + wxString msg; + if (host->test(msg)) { + show_info(this, host->get_test_ok_msg(), _L("Success!")); + } + else { + show_error(this, host->get_test_failed_msg(msg)); + } + }); + + return sizer; + }; + + // Set a wider width for a better alignment + Option option = m_optgroup->get_option("print_host"); + option.opt.width = Field::def_width_wider(); + Line host_line = m_optgroup->create_single_option_line(option); + host_line.append_widget(printhost_browse); + host_line.append_widget(print_host_test); + m_optgroup->append_line(host_line); + + m_optgroup->append_single_option_line("printhost_authorization_type"); + + option = m_optgroup->get_option("printhost_apikey"); + option.opt.width = Field::def_width_wider(); + m_optgroup->append_single_option_line(option); + + const auto ca_file_hint = _u8L("HTTPS CA file is optional. It is only needed if you use HTTPS with a self-signed certificate."); + + if (Http::ca_file_supported()) { + option = m_optgroup->get_option("printhost_cafile"); + option.opt.width = Field::def_width_wider(); + Line cafile_line = m_optgroup->create_single_option_line(option); + + auto printhost_cafile_browse = [=](wxWindow* parent) { + auto sizer = create_sizer_with_btn(parent, &m_printhost_cafile_browse_btn, "browse", _L("Browse") + " " + dots); + m_printhost_cafile_browse_btn->Bind(wxEVT_BUTTON, [this, m_optgroup](wxCommandEvent e) { + static const auto filemasks = _L("Certificate files (*.crt, *.pem)|*.crt;*.pem|All files|*.*"); + wxFileDialog openFileDialog(this, _L("Open CA certificate file"), "", "", filemasks, wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (openFileDialog.ShowModal() != wxID_CANCEL) { + m_optgroup->set_value("printhost_cafile", std::move(openFileDialog.GetPath()), true); + m_optgroup->get_field("printhost_cafile")->field_changed(); + } + }); + + return sizer; + }; + + cafile_line.append_widget(printhost_cafile_browse); + m_optgroup->append_line(cafile_line); + + Line cafile_hint{ "", "" }; + cafile_hint.full_width = 1; + cafile_hint.widget = [this, ca_file_hint](wxWindow* parent) { + auto txt = new wxStaticText(parent, wxID_ANY, ca_file_hint); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(txt); + return sizer; + }; + m_optgroup->append_line(cafile_hint); + } + else { + Line line{ "", "" }; + line.full_width = 1; + + line.widget = [ca_file_hint](wxWindow* parent) { + std::string info = _u8L("HTTPS CA File") + ":\n\t" + + (boost::format(_u8L("On this system, %s uses HTTPS certificates from the system Certificate Store or Keychain.")) % SLIC3R_APP_NAME).str() + + "\n\t" + _u8L("To use a custom CA file, please import your CA file into Certificate Store / Keychain."); + + //auto txt = new wxStaticText(parent, wxID_ANY, from_u8((boost::format("%1%\n\n\t%2%") % info % ca_file_hint).str())); + auto txt = new wxStaticText(parent, wxID_ANY, from_u8((boost::format("%1%\n\t%2%") % info % ca_file_hint).str())); + txt->SetFont(wxGetApp().normal_font()); + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(txt, 1, wxEXPAND); + return sizer; + }; + + m_optgroup->append_line(line); + } + + for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) { + option = m_optgroup->get_option(opt_key); + option.opt.width = Field::def_width_wider(); + m_optgroup->append_single_option_line(option); + } + + update(); +} + +void PhysicalPrinterDialog::update() +{ + m_optgroup->reload_config(); + + const PrinterTechnology tech = Preset::printer_technology(*m_config); + // Only offer the host type selection for FFF, for SLA it's always the SL1 printer (at the moment) + if (tech == ptFFF) { + m_optgroup->show_field("host_type"); + m_optgroup->hide_field("printhost_authorization_type"); + for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) + m_optgroup->hide_field(opt_key); + } + else { + m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false); + m_optgroup->hide_field("host_type"); + + m_optgroup->show_field("printhost_authorization_type"); + + AuthorizationType auth_type = m_config->option>("printhost_authorization_type")->value; + m_optgroup->show_field("printhost_apikey", auth_type == AuthorizationType::atKeyPassword); + + for (const std::string& opt_key : std::vector{ "printhost_user", "printhost_password" }) + m_optgroup->show_field(opt_key, auth_type == AuthorizationType::atUserPassword); + } + + this->Layout(); +} + + +wxString PhysicalPrinterDialog::get_printer_name() +{ + return m_printer_name->GetValue(); +} + +void PhysicalPrinterDialog::update_full_printer_names() +{ + for (PresetForPrinter* preset : m_presets) + preset->update_full_printer_name(); + + this->Layout(); +} + +void PhysicalPrinterDialog::set_printer_technology(PrinterTechnology pt) +{ + m_config->set_key_value("printer_technology", new ConfigOptionEnum(pt)); + update(); +} + +PrinterTechnology PhysicalPrinterDialog::get_printer_technology() +{ + return m_printer.printer_technology(); +} + +void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + const int& em = em_unit(); + + m_printhost_browse_btn->msw_rescale(); + m_printhost_test_btn->msw_rescale(); + if (m_printhost_cafile_browse_btn) + m_printhost_cafile_browse_btn->msw_rescale(); + + m_optgroup->msw_rescale(); + + msw_buttons_rescale(this, em, { wxID_OK, wxID_CANCEL }); + + for (PresetForPrinter* preset : m_presets) + preset->msw_rescale(); + + const wxSize& size = wxSize(45 * em, 35 * em); + SetMinSize(size); + + Fit(); + Refresh(); +} + +void PhysicalPrinterDialog::OnOK(wxEvent& event) +{ + wxString printer_name = m_printer_name->GetValue(); + if (printer_name.IsEmpty()) { + warning_catcher(this, _L("The supplied name is empty. It can't be saved.")); + return; + } + if (printer_name == m_default_name) { + warning_catcher(this, _L("You should to change a name of your printer device. It can't be saved.")); + return; + } + + PhysicalPrinterCollection& printers = wxGetApp().preset_bundle->physical_printers; + const PhysicalPrinter* existing = printers.find_printer(into_u8(printer_name)); + if (existing && into_u8(printer_name) != printers.get_selected_printer_name()) + { + wxString msg_text = from_u8((boost::format(_u8L("Printer with name \"%1%\" already exists.")) % printer_name).str()); + msg_text += "\n" + _L("Replace?"); + wxMessageDialog dialog(nullptr, msg_text, _L("Warning"), wxICON_WARNING | wxYES | wxNO); + + if (dialog.ShowModal() == wxID_NO) + return; + } + + std::set repeat_presets; + m_printer.reset_presets(); + for (PresetForPrinter* preset : m_presets) { + if (!m_printer.add_preset(preset->get_preset_name())) + repeat_presets.emplace(preset->get_preset_name()); + } + + if (!repeat_presets.empty()) + { + wxString repeatable_presets = "\n"; + for (const std::string& preset_name : repeat_presets) + repeatable_presets += " " + from_u8(preset_name) + "\n"; + repeatable_presets += "\n"; + + wxString msg_text = from_u8((boost::format(_u8L("Next printer preset(s) is(are) duplicated:%1%" + "Should I add it(they) just once for the printer \"%2%\" and close the Editing Dialog?")) % repeatable_presets % printer_name).str()); + wxMessageDialog dialog(nullptr, msg_text, _L("Warning"), wxICON_WARNING | wxYES | wxNO); + if (dialog.ShowModal() == wxID_NO) + return; + } + + std::string renamed_from; + // temporary save previous printer name if it was edited + if (m_printer.name != into_u8(m_default_name) && + m_printer.name != into_u8(printer_name)) + renamed_from = m_printer.name; + + //update printer name, if it was changed + m_printer.set_name(into_u8(printer_name)); + + // save new physical printer + printers.save_printer(m_printer, renamed_from); + + if (m_printer.preset_names.find(printers.get_selected_printer_preset_name()) == m_printer.preset_names.end()) { + // select first preset for this printer + printers.select_printer(m_printer); + // refresh preset list on Printer Settings Tab + wxGetApp().get_tab(Preset::TYPE_PRINTER)->select_preset(printers.get_selected_printer_preset_name()); + } + else + wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_preset_choice(); + + event.Skip(); +} + +void PhysicalPrinterDialog::AddPreset(wxEvent& event) +{ + m_presets.emplace_back(new PresetForPrinter(this)); + // enable DELETE button for the first preset, if was disabled + m_presets.front()->AllowDelete(); + + m_presets_sizer->Add(m_presets.back()->sizer(), 1, wxEXPAND | wxTOP, BORDER_W); + update_full_printer_names(); + + this->Fit(); +} + +void PhysicalPrinterDialog::DeletePreset(PresetForPrinter* preset_for_printer) +{ + if (m_presets.size() == 1) { + wxString msg_text = _L("It's not possible to delete last related preset for the printer."); + wxMessageDialog dialog(nullptr, msg_text, _L("Infornation"), wxICON_INFORMATION | wxOK); + dialog.ShowModal(); + return; + } + + assert(preset_for_printer); + auto it = std::find(m_presets.begin(), m_presets.end(), preset_for_printer); + if (it == m_presets.end()) + return; + + const int remove_id = it - m_presets.begin(); + m_presets_sizer->Remove(remove_id); + delete preset_for_printer; + m_presets.erase(it); + + if (m_presets.size() == 1) + m_presets.front()->SuppressDelete(); + + this->Layout(); + this->Fit(); +} + + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp new file mode 100644 index 0000000000..3d0cf2d9f2 --- /dev/null +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -0,0 +1,105 @@ +#ifndef slic3r_PhysicalPrinterDialog_hpp_ +#define slic3r_PhysicalPrinterDialog_hpp_ + +#include + +#include + +#include "libslic3r/Preset.hpp" +#include "GUI_Utils.hpp" + +class wxString; +class wxTextCtrl; +class wxStaticText; +class ScalableButton; +class wxBoxSizer; + +namespace Slic3r { + +namespace GUI { + +class PresetComboBox; + +//------------------------------------------ +// PresetForPrinter +//------------------------------------------ +//static std::string g_info_string = " (modified)"; +class PhysicalPrinterDialog; +class PresetForPrinter +{ + PhysicalPrinterDialog* m_parent { nullptr }; + + PresetComboBox* m_presets_list { nullptr }; + ScalableButton* m_delete_preset_btn { nullptr }; + wxStaticText* m_info_line { nullptr }; + wxStaticText* m_full_printer_name { nullptr }; + + wxBoxSizer* m_sizer { nullptr }; + + void DeletePreset(wxEvent& event); + +public: + PresetForPrinter(PhysicalPrinterDialog* parent, const std::string& preset_name = ""); + ~PresetForPrinter(); + + wxBoxSizer* sizer() { return m_sizer; } + void update_full_printer_name(); + std::string get_preset_name(); + void SuppressDelete(); + void AllowDelete(); + + void msw_rescale(); + void on_sys_color_changed() {}; +}; + + +//------------------------------------------ +// PhysicalPrinterDialog +//------------------------------------------ + +class ConfigOptionsGroup; +class PhysicalPrinterDialog : public DPIDialog +{ + PhysicalPrinter m_printer; + wxString m_default_name; + DynamicPrintConfig* m_config { nullptr }; + + wxTextCtrl* m_printer_name { nullptr }; + std::vector m_presets; + + ConfigOptionsGroup* m_optgroup { nullptr }; + + ScalableButton* m_add_preset_btn {nullptr}; + ScalableButton* m_printhost_browse_btn {nullptr}; + ScalableButton* m_printhost_test_btn {nullptr}; + ScalableButton* m_printhost_cafile_browse_btn {nullptr}; + + wxBoxSizer* m_presets_sizer {nullptr}; + + void build_printhost_settings(ConfigOptionsGroup* optgroup); + void OnOK(wxEvent& event); + void AddPreset(wxEvent& event); + +public: + PhysicalPrinterDialog(wxString printer_name); + ~PhysicalPrinterDialog(); + + void update(); + wxString get_printer_name(); + void update_full_printer_names(); + PhysicalPrinter* get_printer() {return &m_printer; } + void set_printer_technology(PrinterTechnology pt); + PrinterTechnology get_printer_technology(); + + void DeletePreset(PresetForPrinter* preset_for_printer); + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + void on_sys_color_changed() override {}; +}; + + +} // namespace GUI +} // namespace Slic3r + +#endif diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ec13610b8c..ec632611f0 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -33,7 +32,11 @@ #include "libslic3r/Format/STL.hpp" #include "libslic3r/Format/AMF.hpp" #include "libslic3r/Format/3mf.hpp" +#if ENABLE_GCODE_VIEWER +#include "libslic3r/GCode/GCodeProcessor.hpp" +#else #include "libslic3r/GCode/PreviewData.hpp" +#endif // ENABLE_GCODE_VIEWER #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/SLA/Hollowing.hpp" @@ -44,6 +47,7 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/PresetBundle.hpp" #include "GUI.hpp" #include "GUI_App.hpp" @@ -66,7 +70,6 @@ #include "Jobs/ArrangeJob.hpp" #include "Jobs/RotoptimizeJob.hpp" #include "Jobs/SLAImportJob.hpp" -#include "PresetBundle.hpp" #include "BackgroundSlicingProcess.hpp" #include "ProgressStatusBar.hpp" #include "PrintHostDialogs.hpp" @@ -75,8 +78,11 @@ #include "../Utils/PrintHost.hpp" #include "../Utils/FixModelByWin10.hpp" #include "../Utils/UndoRedo.hpp" +#include "../Utils/PresetUpdater.hpp" #include "RemovableDriveManager.hpp" #include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "PresetComboBoxes.hpp" #ifdef __APPLE__ #include "Gizmos/GLGizmosManager.hpp" @@ -101,7 +107,8 @@ namespace GUI { wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); -wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, wxCommandEvent); +wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, SlicingProcessCompletedEvent); +wxDEFINE_EVENT(EVT_EXPORT_BEGAN, wxCommandEvent); // Sidebar widgets @@ -253,153 +260,6 @@ void SlicedInfo::SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const w info_vec[idx].second->Show(show); } -PresetComboBox::PresetComboBox(wxWindow *parent, Preset::Type preset_type) : -PresetBitmapComboBox(parent, wxSize(15 * wxGetApp().em_unit(), -1)), - preset_type(preset_type), - last_selected(wxNOT_FOUND), - m_em_unit(wxGetApp().em_unit()) -{ - SetFont(wxGetApp().normal_font()); -#ifdef _WIN32 - // Workaround for ignoring CBN_EDITCHANGE events, which are processed after the content of the combo box changes, so that - // the index of the item inside CBN_EDITCHANGE may no more be valid. - EnableTextChangedEvents(false); -#endif /* _WIN32 */ - Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &evt) { - auto selected_item = evt.GetSelection(); - - auto marker = reinterpret_cast(this->GetClientData(selected_item)); - if (marker >= LABEL_ITEM_MARKER && marker < LABEL_ITEM_MAX) { - this->SetSelection(this->last_selected); - evt.StopPropagation(); - if (marker >= LABEL_ITEM_WIZARD_PRINTERS) { - ConfigWizard::StartPage sp = ConfigWizard::SP_WELCOME; - switch (marker) { - case LABEL_ITEM_WIZARD_PRINTERS: sp = ConfigWizard::SP_PRINTERS; break; - case LABEL_ITEM_WIZARD_FILAMENTS: sp = ConfigWizard::SP_FILAMENTS; break; - case LABEL_ITEM_WIZARD_MATERIALS: sp = ConfigWizard::SP_MATERIALS; break; - } - wxTheApp->CallAfter([sp]() { wxGetApp().run_wizard(ConfigWizard::RR_USER, sp); }); - } - } else if ( this->last_selected != selected_item || - wxGetApp().get_tab(this->preset_type)->get_presets()->current_is_dirty() ) { - this->last_selected = selected_item; - evt.SetInt(this->preset_type); - evt.Skip(); - } else { - evt.StopPropagation(); - } - }); - - if (preset_type == Slic3r::Preset::TYPE_FILAMENT) - { - Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &event) { - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - const Preset* selected_preset = preset_bundle->filaments.find_preset(preset_bundle->filament_presets[extruder_idx]); - // Wide icons are shown if the currently selected preset is not compatible with the current printer, - // and red flag is drown in front of the selected preset. - bool wide_icons = selected_preset != nullptr && !selected_preset->is_compatible; - float scale = m_em_unit*0.1f; - - int shifl_Left = wide_icons ? int(scale * 16 + 0.5) : 0; -#if defined(wxBITMAPCOMBOBOX_OWNERDRAWN_BASED) - shifl_Left += int(scale * 4 + 0.5f); // IMAGE_SPACING_RIGHT = 4 for wxBitmapComboBox -> Space left of image -#endif - int icon_right_pos = shifl_Left + int(scale * (24+4) + 0.5); - int mouse_pos = event.GetLogicalPosition(wxClientDC(this)).x; - if (mouse_pos < shifl_Left || mouse_pos > icon_right_pos ) { - // Let the combo box process the mouse click. - event.Skip(); - return; - } - - // Swallow the mouse click and open the color picker. - - // get current color - DynamicPrintConfig* cfg = wxGetApp().get_tab(Preset::TYPE_PRINTER)->get_config(); - auto colors = static_cast(cfg->option("extruder_colour")->clone()); - wxColour clr(colors->values[extruder_idx]); - if (!clr.IsOk()) - clr = wxColour(0,0,0); // Don't set alfa to transparence - - auto data = new wxColourData(); - data->SetChooseFull(1); - data->SetColour(clr); - - wxColourDialog dialog(this, data); - dialog.CenterOnParent(); - if (dialog.ShowModal() == wxID_OK) - { - colors->values[extruder_idx] = dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - - DynamicPrintConfig cfg_new = *cfg; - cfg_new.set_key_value("extruder_colour", colors); - - wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new); - preset_bundle->update_plater_filament_ui(extruder_idx, this); - wxGetApp().plater()->on_config_change(cfg_new); - } - }); - } - - edit_btn = new ScalableButton(parent, wxID_ANY, "cog"); - edit_btn->SetToolTip(_L("Click to edit preset")); - - edit_btn->Bind(wxEVT_BUTTON, ([preset_type, this](wxCommandEvent) - { - Tab* tab = wxGetApp().get_tab(preset_type); - if (!tab) - return; - - int page_id = wxGetApp().tab_panel()->FindPage(tab); - if (page_id == wxNOT_FOUND) - return; - - wxGetApp().tab_panel()->SetSelection(page_id); - - // Switch to Settings NotePad - wxGetApp().mainframe->select_tab(); - - /* In a case of a multi-material printing, for editing another Filament Preset - * it's needed to select this preset for the "Filament settings" Tab - */ - if (preset_type == Preset::TYPE_FILAMENT && wxGetApp().extruders_edited_cnt() > 1) - { - const std::string& selected_preset = GetString(GetSelection()).ToUTF8().data(); - - // Call select_preset() only if there is new preset and not just modified - if ( !boost::algorithm::ends_with(selected_preset, Preset::suffix_modified()) ) - { - const std::string& preset_name = wxGetApp().preset_bundle->filaments.get_preset_name_by_alias(selected_preset); - tab->select_preset(/*selected_preset*/preset_name); - } - } - })); -} - -PresetComboBox::~PresetComboBox() -{ - if (edit_btn) - edit_btn->Destroy(); -} - - -void PresetComboBox::set_label_marker(int item, LabelItemType label_item_type) -{ - this->SetClientData(item, (void*)label_item_type); -} - -void PresetComboBox::check_selection(int selection) -{ - this->last_selected = selection; -} - -void PresetComboBox::msw_rescale() -{ - m_em_unit = wxGetApp().em_unit(); - edit_btn->msw_rescale(); -} - // Frequently changed parameters class FreqChangedParams : public OG_Settings @@ -697,12 +557,12 @@ struct Sidebar::priv ModeSizer *mode_sizer; wxFlexGridSizer *sizer_presets; - PresetComboBox *combo_print; - std::vector combos_filament; + PlaterPresetComboBox *combo_print; + std::vector combos_filament; wxBoxSizer *sizer_filaments; - PresetComboBox *combo_sla_print; - PresetComboBox *combo_sla_material; - PresetComboBox *combo_printer; + PlaterPresetComboBox *combo_sla_print; + PlaterPresetComboBox *combo_sla_material; + PlaterPresetComboBox *combo_printer; wxBoxSizer *sizer_params; FreqChangedParams *frequently_changed_parameters{ nullptr }; @@ -716,7 +576,7 @@ struct Sidebar::priv wxButton *btn_export_gcode; wxButton *btn_reslice; ScalableButton *btn_send_gcode; - ScalableButton *btn_remove_device; + ScalableButton *btn_eject_device; ScalableButton* btn_export_gcode_removable; //exports to removable drives (appears only if removable drive is connected) bool is_collapsed {false}; @@ -801,10 +661,10 @@ Sidebar::Sidebar(Plater *parent) p->sizer_filaments = new wxBoxSizer(wxVERTICAL); - auto init_combo = [this](PresetComboBox **combo, wxString label, Preset::Type preset_type, bool filament) { + auto init_combo = [this](PlaterPresetComboBox **combo, wxString label, Preset::Type preset_type, bool filament) { auto *text = new wxStaticText(p->presets_panel, wxID_ANY, label + " :"); text->SetFont(wxGetApp().small_font()); - *combo = new PresetComboBox(p->presets_panel, preset_type); + *combo = new PlaterPresetComboBox(p->presets_panel, preset_type); auto combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); combo_and_btn_sizer->Add(*combo, 1, wxEXPAND); @@ -889,12 +749,12 @@ Sidebar::Sidebar(Plater *parent) }; init_scalable_btn(&p->btn_send_gcode , "export_gcode", _L("Send to printer") + "\tCtrl+Shift+G"); - init_scalable_btn(&p->btn_remove_device, "eject_sd" , _L("Remove device") + "\tCtrl+T"); + init_scalable_btn(&p->btn_eject_device, "eject_sd" , _L("Remove device") + "\tCtrl+T"); init_scalable_btn(&p->btn_export_gcode_removable, "export_to_sd", _L("Export to SD card / Flash drive") + "\tCtrl+U"); // regular buttons "Slice now" and "Export G-code" - const int scaled_height = p->btn_remove_device->GetBitmapHeight() + 4; + const int scaled_height = p->btn_eject_device->GetBitmapHeight() + 4; auto init_btn = [this](wxButton **btn, wxString label, const int button_height) { *btn = new wxButton(this, wxID_ANY, label, wxDefaultPosition, wxSize(-1, button_height), wxBU_EXACTFIT); @@ -912,7 +772,7 @@ Sidebar::Sidebar(Plater *parent) complect_btns_sizer->Add(p->btn_export_gcode, 1, wxEXPAND); complect_btns_sizer->Add(p->btn_send_gcode); complect_btns_sizer->Add(p->btn_export_gcode_removable); - complect_btns_sizer->Add(p->btn_remove_device); + complect_btns_sizer->Add(p->btn_eject_device); btns_sizer->Add(p->btn_reslice, 0, wxEXPAND | wxTOP, margin_5); @@ -929,20 +789,20 @@ Sidebar::Sidebar(Plater *parent) { const bool export_gcode_after_slicing = wxGetKeyState(WXK_SHIFT); if (export_gcode_after_slicing) - p->plater->export_gcode(); + p->plater->export_gcode(true); else p->plater->reslice(); p->plater->select_view_3D("Preview"); }); p->btn_send_gcode->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { p->plater->send_gcode(); }); - p->btn_remove_device->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { p->plater->eject_drive(); }); + p->btn_eject_device->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { p->plater->eject_drive(); }); p->btn_export_gcode_removable->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { p->plater->export_gcode(true); }); } Sidebar::~Sidebar() {} -void Sidebar::init_filament_combo(PresetComboBox **combo, const int extr_idx) { - *combo = new PresetComboBox(p->presets_panel, Slic3r::Preset::TYPE_FILAMENT); +void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int extr_idx) { + *combo = new PlaterPresetComboBox(p->presets_panel, Slic3r::Preset::TYPE_FILAMENT); // # copy icons from first choice // $choice->SetItemBitmap($_, $choices->[0]->GetItemBitmap($_)) for 0..$#presets; @@ -977,18 +837,18 @@ void Sidebar::update_all_preset_comboboxes() // Update the print choosers to only contain the compatible presets, update the dirty flags. if (print_tech == ptFFF) - preset_bundle.prints.update_plater_ui(p->combo_print); + p->combo_print->update(); else { - preset_bundle.sla_prints.update_plater_ui(p->combo_sla_print); - preset_bundle.sla_materials.update_plater_ui(p->combo_sla_material); + p->combo_sla_print->update(); + p->combo_sla_material->update(); } // Update the printer choosers, update the dirty flags. - preset_bundle.printers.update_plater_ui(p->combo_printer); + p->combo_printer->update(); // Update the filament choosers to only contain the compatible presets, update the color preview, // update the dirty flags. if (print_tech == ptFFF) { - for (size_t i = 0; i < p->combos_filament.size(); ++i) - preset_bundle.update_plater_filament_ui(i, p->combos_filament[i]); + for (PlaterPresetComboBox* cb : p->combos_filament) + cb->update(); } } @@ -1010,23 +870,22 @@ void Sidebar::update_presets(Preset::Type preset_type) preset_bundle.set_filament_preset(0, name); } - for (size_t i = 0; i < filament_cnt; i++) { - preset_bundle.update_plater_filament_ui(i, p->combos_filament[i]); - } + for (size_t i = 0; i < filament_cnt; i++) + p->combos_filament[i]->update(); break; } case Preset::TYPE_PRINT: - preset_bundle.prints.update_plater_ui(p->combo_print); + p->combo_print->update(); break; case Preset::TYPE_SLA_PRINT: - preset_bundle.sla_prints.update_plater_ui(p->combo_sla_print); + p->combo_sla_print->update(); break; case Preset::TYPE_SLA_MATERIAL: - preset_bundle.sla_materials.update_plater_ui(p->combo_sla_material); + p->combo_sla_material->update(); break; case Preset::TYPE_PRINTER: @@ -1062,18 +921,14 @@ void Sidebar::msw_rescale() p->mode_sizer->msw_rescale(); - // Rescale preset comboboxes in respect to the current em_unit ... - for (PresetComboBox* combo : std::vector { p->combo_print, + for (PlaterPresetComboBox* combo : std::vector { p->combo_print, p->combo_sla_print, p->combo_sla_material, p->combo_printer } ) combo->msw_rescale(); - for (PresetComboBox* combo : p->combos_filament) + for (PlaterPresetComboBox* combo : p->combos_filament) combo->msw_rescale(); - // ... then refill them and set min size to correct layout of the sidebar - update_all_preset_comboboxes(); - p->frequently_changed_parameters->msw_rescale(); p->object_list->msw_rescale(); p->object_manipulation->msw_rescale(); @@ -1083,9 +938,9 @@ void Sidebar::msw_rescale() p->object_info->msw_rescale(); p->btn_send_gcode->msw_rescale(); - p->btn_remove_device->msw_rescale(); + p->btn_eject_device->msw_rescale(); p->btn_export_gcode_removable->msw_rescale(); - const int scaled_height = p->btn_remove_device->GetBitmap().GetHeight() + 4; + const int scaled_height = p->btn_eject_device->GetBitmap().GetHeight() + 4; p->btn_export_gcode->SetMinSize(wxSize(-1, scaled_height)); p->btn_reslice ->SetMinSize(wxSize(-1, scaled_height)); @@ -1094,27 +949,21 @@ void Sidebar::msw_rescale() void Sidebar::sys_color_changed() { - // Update preset comboboxes in respect to the system color ... - // combo->msw_rescale() updates icon on button, so use it - for (PresetComboBox* combo : std::vector{ p->combo_print, + for (PlaterPresetComboBox* combo : std::vector{ p->combo_print, p->combo_sla_print, p->combo_sla_material, p->combo_printer }) combo->msw_rescale(); - for (PresetComboBox* combo : p->combos_filament) + for (PlaterPresetComboBox* combo : p->combos_filament) combo->msw_rescale(); - // ... then refill them and set min size to correct layout of the sidebar - update_all_preset_comboboxes(); - p->object_list->sys_color_changed(); p->object_manipulation->sys_color_changed(); -// p->object_settings->msw_rescale(); p->object_layers->sys_color_changed(); // btn...->msw_rescale() updates icon on button, so use it p->btn_send_gcode->msw_rescale(); - p->btn_remove_device->msw_rescale(); + p->btn_eject_device->msw_rescale(); p->btn_export_gcode_removable->msw_rescale(); p->scrolled->Layout(); @@ -1316,48 +1165,59 @@ void Sidebar::update_sliced_info_sizer() wxString::Format("%.2f", ps.total_cost); p->sliced_info->SetTextAndShow(siCost, info_text, new_label); +#if ENABLE_GCODE_VIEWER + // hide the estimate time + p->sliced_info->SetTextAndShow(siEstimatedTime, "N/A"); +#else if (ps.estimated_normal_print_time == "N/A" && ps.estimated_silent_print_time == "N/A") p->sliced_info->SetTextAndShow(siEstimatedTime, "N/A"); else { - new_label = _L("Estimated printing time") +":"; + new_label = _L("Estimated printing time") + ":"; info_text = ""; wxString str_color = _L("Color"); wxString str_pause = _L("Pause"); - auto fill_labels = [str_color, str_pause](const std::vector>& times, - wxString& new_label, wxString& info_text) - { - int color_change_count = 0; - for (auto time : times) - if (time.first == CustomGCode::ColorChange) - color_change_count++; - - for (int i = (int)times.size() - 1; i >= 0; --i) + auto fill_labels = [str_color, str_pause](const std::vector>& times, + wxString& new_label, wxString& info_text) { - if (i == 0 || times[i - 1].first == CustomGCode::PausePrint) - new_label += format_wxstr("\n - %1%%2%", str_color + " ", color_change_count); - else if (times[i - 1].first == CustomGCode::ColorChange) - new_label += format_wxstr("\n - %1%%2%", str_color + " ", color_change_count--); + int color_change_count = 0; + for (auto time : times) + if (time.first == CustomGCode::ColorChange) + color_change_count++; - if (i != (int)times.size() - 1 && times[i].first == CustomGCode::PausePrint) - new_label += format_wxstr(" -> %1%", str_pause); + for (int i = (int)times.size() - 1; i >= 0; --i) + { + if (i == 0 || times[i - 1].first == CustomGCode::PausePrint) + new_label += format_wxstr("\n - %1%%2%", str_color + " ", color_change_count); + else if (times[i - 1].first == CustomGCode::ColorChange) + new_label += format_wxstr("\n - %1%%2%", str_color + " ", color_change_count--); - info_text += format_wxstr("\n%1%", times[i].second); - } - }; + if (i != (int)times.size() - 1 && times[i].first == CustomGCode::PausePrint) + new_label += format_wxstr(" -> %1%", str_pause); + + info_text += format_wxstr("\n%1%", times[i].second); + } + }; if (ps.estimated_normal_print_time != "N/A") { new_label += format_wxstr("\n - %1%", _L("normal mode")); info_text += format_wxstr("\n%1%", ps.estimated_normal_print_time); fill_labels(ps.estimated_normal_custom_gcode_print_times, new_label, info_text); + + // uncomment next line to not disappear slicing finished notif when colapsing sidebar before time estimate + //if (p->plater->is_sidebar_collapsed()) + p->plater->get_notification_manager()->set_slicing_complete_large(p->plater->is_sidebar_collapsed()); + p->plater->get_notification_manager()->set_slicing_complete_print_time("Estimated printing time: " + ps.estimated_normal_print_time); + } if (ps.estimated_silent_print_time != "N/A") { new_label += format_wxstr("\n - %1%", _L("stealth mode")); info_text += format_wxstr("\n%1%", ps.estimated_silent_print_time); fill_labels(ps.estimated_silent_custom_gcode_print_times, new_label, info_text); } - p->sliced_info->SetTextAndShow(siEstimatedTime, info_text, new_label); + p->sliced_info->SetTextAndShow(siEstimatedTime, info_text, new_label); } +#endif // !ENABLE_GCODE_VIEWER // if there is a wipe tower, insert number of toolchanges info into the array: p->sliced_info->SetTextAndShow(siWTNumbetOfToolchanges, is_wipe_tower ? wxString::Format("%.d", ps.total_toolchanges) : "N/A"); @@ -1366,6 +1226,8 @@ void Sidebar::update_sliced_info_sizer() p->sliced_info->SetTextAndShow(siMateril_unit, "N/A"); } } + + Layout(); } void Sidebar::show_sliced_info_sizer(const bool show) @@ -1385,15 +1247,16 @@ void Sidebar::enable_buttons(bool enable) p->btn_reslice->Enable(enable); p->btn_export_gcode->Enable(enable); p->btn_send_gcode->Enable(enable); - p->btn_remove_device->Enable(enable); + p->btn_eject_device->Enable(enable); p->btn_export_gcode_removable->Enable(enable); } -bool Sidebar::show_reslice(bool show) const { return p->btn_reslice->Show(show); } -bool Sidebar::show_export(bool show) const { return p->btn_export_gcode->Show(show); } -bool Sidebar::show_send(bool show) const { return p->btn_send_gcode->Show(show); } -bool Sidebar::show_disconnect(bool show) const { return p->btn_remove_device->Show(show); } -bool Sidebar::show_export_removable(bool show)const { return p->btn_export_gcode_removable->Show(show); } +bool Sidebar::show_reslice(bool show) const { return p->btn_reslice->Show(show); } +bool Sidebar::show_export(bool show) const { return p->btn_export_gcode->Show(show); } +bool Sidebar::show_send(bool show) const { return p->btn_send_gcode->Show(show); } +bool Sidebar::show_export_removable(bool show) const { return p->btn_export_gcode_removable->Show(show); } +bool Sidebar::show_eject(bool show) const { return p->btn_eject_device->Show(show); } +bool Sidebar::get_eject_shown() const { return p->btn_eject_device->IsShown(); } bool Sidebar::is_multifilament() { @@ -1458,7 +1321,7 @@ void Sidebar::update_ui_from_settings() update_sliced_info_sizer(); } -std::vector& Sidebar::combos_filament() +std::vector& Sidebar::combos_filament() { return p->combos_filament; } @@ -1486,20 +1349,54 @@ private: Plater *plater; static const std::regex pattern_drop; +#if ENABLE_GCODE_VIEWER + static const std::regex pattern_gcode_drop; +#endif // ENABLE_GCODE_VIEWER }; const std::regex PlaterDropTarget::pattern_drop(".*[.](stl|obj|amf|3mf|prusa)", std::regex::icase); +#if ENABLE_GCODE_VIEWER +const std::regex PlaterDropTarget::pattern_gcode_drop(".*[.](gcode)", std::regex::icase); +#endif // ENABLE_GCODE_VIEWER bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) { std::vector paths; - for (const auto &filename : filenames) { - fs::path path(into_path(filename)); - if (std::regex_match(path.string(), pattern_drop)) { - paths.push_back(std::move(path)); - } else { + +#if ENABLE_GCODE_VIEWER +#ifdef WIN32 + // hides the system icon + this->MSWUpdateDragImageOnLeave(); +#endif // WIN32 + + if (wxGetApp().is_gcode_viewer()) { + // gcode section + for (const auto& filename : filenames) { + fs::path path(into_path(filename)); + if (std::regex_match(path.string(), pattern_gcode_drop)) + paths.push_back(std::move(path)); + } + + if (paths.size() > 1) { + wxMessageDialog((wxWindow*)plater, _L("You can open only one .gcode file at a time."), + wxString(SLIC3R_APP_NAME) + " - " + _L("Drag and drop G-code file"), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal(); return false; } + else if (paths.size() == 1) { + plater->load_gcode(from_path(paths.front())); + return true; + } + return false; + } +#endif // ENABLE_GCODE_VIEWER + + // editor section + for (const auto &filename : filenames) { + fs::path path(into_path(filename)); + if (std::regex_match(path.string(), pattern_drop)) + paths.push_back(std::move(path)); + else + return false; } wxString snapshot_label; @@ -1527,13 +1424,10 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi // because right now the plater is not cleared, we set the project file (from the latest imported .3mf or .amf file) // only if not set yet // if res is empty no data has been loaded - if (!res.empty() && plater->get_project_filename().empty()) - { - for (std::vector::const_reverse_iterator it = paths.rbegin(); it != paths.rend(); ++it) - { + if (!res.empty() && plater->get_project_filename().empty()) { + for (std::vector::const_reverse_iterator it = paths.rbegin(); it != paths.rend(); ++it) { std::string filename = (*it).filename().string(); - if (boost::algorithm::iends_with(filename, ".3mf") || boost::algorithm::iends_with(filename, ".amf")) - { + if (boost::algorithm::iends_with(filename, ".3mf") || boost::algorithm::iends_with(filename, ".amf")) { plater->set_project_filename(from_path(*it)); break; } @@ -1574,7 +1468,11 @@ struct Plater::priv Slic3r::SLAPrint sla_print; Slic3r::Model model; PrinterTechnology printer_technology = ptFFF; +#if ENABLE_GCODE_VIEWER + Slic3r::GCodeProcessor::Result gcode_result; +#else Slic3r::GCodePreviewData gcode_preview_data; +#endif // ENABLE_GCODE_VIEWER // GUI elements wxSizer* panel_sizer{ nullptr }; @@ -1591,6 +1489,7 @@ struct Plater::priv GLToolbar view_toolbar; GLToolbar collapse_toolbar; Preview *preview; + NotificationManager* notification_manager { nullptr }; BackgroundSlicingProcess background_process; bool suppressed_backround_processing_update { false }; @@ -1686,6 +1585,15 @@ struct Plater::priv bool init_view_toolbar(); bool init_collapse_toolbar(); +#if ENABLE_GCODE_VIEWER + void update_preview_bottom_toolbar(); + void update_preview_moves_slider(); +#endif // ENABLE_GCODE_VIEWER + +#if ENABLE_GCODE_VIEWER + void reset_gcode_toolpaths(); +#endif // ENABLE_GCODE_VIEWER + void reset_all_gizmos(); void update_ui_from_settings(); void update_main_toolbar_tooltips(); @@ -1774,8 +1682,18 @@ struct Plater::priv void on_select_preset(wxCommandEvent&); void on_slicing_update(SlicingStatusEvent&); void on_slicing_completed(wxCommandEvent&); - void on_process_completed(wxCommandEvent&); + void on_process_completed(SlicingProcessCompletedEvent&); + void on_export_began(wxCommandEvent&); void on_layer_editing_toggled(bool enable); + void on_slicing_began(); + + void clear_warnings(); + void add_warning(const Slic3r::PrintStateBase::Warning &warning, size_t oid); + void actualizate_warnings(const Model& model, size_t print_oid); + // Displays dialog window with list of warnings. + // Returns true if user clicks OK. + // Returns true if current_warnings vector is empty without showning the dialog + bool warnings_dialog(); void on_action_add(SimpleEvent&); void on_action_split_objects(SimpleEvent&); @@ -1796,7 +1714,7 @@ struct Plater::priv // triangulate the bed and store the triangles into m_bed.m_triangles, // fills the m_bed.m_grid_lines and sets m_bed.m_origin. // Sets m_bed.m_polygon to limit the object placement. - void set_bed_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model); + void set_bed_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom = false); bool can_delete() const; bool can_delete_all() const; @@ -1826,7 +1744,7 @@ struct Plater::priv // Flag indicating that the G-code export targets a removable device, therefore the show_action_buttons() needs to be called at any case when the background processing finishes. bool writing_to_removable_device = { false }; bool inside_snapshot_capture() { return m_prevent_snapshots != 0; } - + bool process_completed_with_error { false }; private: bool init_object_menu(); bool init_common_menu(wxMenu* menu, const bool is_part = false); @@ -1854,6 +1772,11 @@ private: * */ std::string m_last_fff_printer_profile_name; std::string m_last_sla_printer_profile_name; + + // vector of all warnings generated by last slicing + std::vector> current_warnings; + bool show_warning_dialog { false }; + }; const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf|prusa)", std::regex::icase); @@ -1887,7 +1810,11 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) background_process.set_fff_print(&fff_print); background_process.set_sla_print(&sla_print); +#if ENABLE_GCODE_VIEWER + background_process.set_gcode_result(&gcode_result); +#else background_process.set_gcode_preview_data(&gcode_preview_data); +#endif // ENABLE_GCODE_VIEWER background_process.set_thumbnail_cb([this](ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) { std::packaged_task task([this](ThumbnailsList& thumbnails, const Vec2ds& sizes, bool printable_only, bool parts_only, bool show_bed, bool transparent_background) { @@ -1899,6 +1826,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) }); background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); background_process.set_finished_event(EVT_PROCESS_COMPLETED); + background_process.set_export_began_event(EVT_EXPORT_BEGAN); // Default printer technology for default config. background_process.select_technology(this->printer_technology); // Register progress callback from the Print class to the Plater. @@ -1911,7 +1839,11 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) this->q->Bind(EVT_SLICING_UPDATE, &priv::on_slicing_update, this); view3D = new View3D(q, &model, config, &background_process); +#if ENABLE_GCODE_VIEWER + preview = new Preview(q, &model, config, &background_process, &gcode_result, [this]() { schedule_background_process(); }); +#else preview = new Preview(q, &model, config, &background_process, &gcode_preview_data, [this]() { schedule_background_process(); }); +#endif // ENABLE_GCODE_VIEWER #ifdef __APPLE__ // set default view_toolbar icons size equal to GLGizmosManager::Default_Icons_Size @@ -1991,27 +1923,23 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_OBJECTS, &priv::on_action_split_objects, this); view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_VOLUMES, &priv::on_action_split_volumes, this); view3D_canvas->Bind(EVT_GLTOOLBAR_LAYERSEDITING, &priv::on_action_layersediting, this); - view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [this](SimpleEvent&) - { - set_bed_shape(config->option("bed_shape")->values, - config->option("bed_custom_texture")->value, - config->option("bed_custom_model")->value); - }); + view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); // Preview events: preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_QUESTION_MARK, [this](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); - preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [this](SimpleEvent&) - { - set_bed_shape(config->option("bed_shape")->values, - config->option("bed_custom_texture")->value, - config->option("bed_custom_model")->value); - }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_TAB, [this](SimpleEvent&) { select_next_view_3D(); }); +#if ENABLE_GCODE_VIEWER + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_MOVE_LAYERS_SLIDER, [this](wxKeyEvent& evt) { preview->move_layers_slider(evt); }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_EDIT_COLOR_CHANGE, [this](wxKeyEvent& evt) { preview->edit_layers_slider(evt); }); +#else preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_MOVE_DOUBLE_SLIDER, [this](wxKeyEvent& evt) { preview->move_double_slider(evt); }); preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_EDIT_COLOR_CHANGE, [this](wxKeyEvent& evt) { preview->edit_double_slider(evt); }); +#endif // ENABLE_GCODE_VIEWER - q->Bind(EVT_SLICING_COMPLETED, &priv::on_slicing_completed, this); + q->Bind(EVT_SLICING_COMPLETED, &priv::on_slicing_completed, this); q->Bind(EVT_PROCESS_COMPLETED, &priv::on_process_completed, this); + q->Bind(EVT_EXPORT_BEGAN, &priv::on_export_began, this); q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview"); }); @@ -2019,7 +1947,13 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) q->SetDropTarget(new PlaterDropTarget(q)); // if my understanding is right, wxWindow takes the owenership q->Layout(); +#if ENABLE_GCODE_VIEWER + set_current_panel(wxGetApp().is_editor() ? (wxPanel*)view3D : (wxPanel*)preview); + if (wxGetApp().is_gcode_viewer()) + preview->hide_layers_slider(); +#else set_current_panel(view3D); +#endif // ENABLE_GCODE_VIEWER // updates camera type from .ini file camera.set_type(get_config("use_perspective_camera")); @@ -2038,23 +1972,39 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) }); #endif /* _WIN32 */ - this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this](RemovableDriveEjectEvent &evt) { - if (evt.data.second) { - this->show_action_buttons(this->ready_to_slice); - Slic3r::GUI::show_info(this->q, format_wxstr(_L("Unmounting successful. The device %s(%s) can now be safely removed from the computer."), - evt.data.first.name, evt.data.first.path)); - } else - Slic3r::GUI::show_info(this->q, format_wxstr(_L("Ejecting of device %s(%s) has failed."), - evt.data.first.name, evt.data.first.path)); - }); - this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this](RemovableDrivesChangedEvent &) { this->show_action_buttons(this->ready_to_slice); }); - // Start the background thread and register this window as a target for update events. - wxGetApp().removable_drive_manager()->init(this->q); + notification_manager = new NotificationManager(this->q); +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor()) { +#endif // ENABLE_GCODE_VIEWER + this->q->Bind(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, [this](EjectDriveNotificationClickedEvent&) { this->q->eject_drive(); }); + this->q->Bind(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, [this](ExportGcodeNotificationClickedEvent&) { this->q->export_gcode(true); }); + this->q->Bind(EVT_PRESET_UPDATE_AVIABLE_CLICKED, [this](PresetUpdateAviableClickedEvent&) { wxGetApp().get_preset_updater()->on_update_notification_confirm(); }); + this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this, q](RemovableDriveEjectEvent &evt) { + if (evt.data.second) { + this->show_action_buttons(this->ready_to_slice); + notification_manager->push_notification(format(_L("Unmounting successful. The device %s(%s) can now be safely removed from the computer."),evt.data.first.name, evt.data.first.path), + NotificationManager::NotificationLevel::RegularNotification, *q->get_current_canvas3D()); + } else { + notification_manager->push_notification(format(_L("Ejecting of device %s(%s) has failed."), evt.data.first.name, evt.data.first.path), + NotificationManager::NotificationLevel::ErrorNotification, *q->get_current_canvas3D()); + } + }); + this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this, q](RemovableDrivesChangedEvent &) { + this->show_action_buttons(this->ready_to_slice); + if (!this->sidebar->get_eject_shown()) { + notification_manager->close_notification_of_type(NotificationType::ExportToRemovableFinished); + } + }); + // Start the background thread and register this window as a target for update events. + wxGetApp().removable_drive_manager()->init(this->q); #ifdef _WIN32 - // Trigger enumeration of removable media on Win32 notification. - this->q->Bind(EVT_VOLUME_ATTACHED, [this](VolumeAttachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); - this->q->Bind(EVT_VOLUME_DETACHED, [this](VolumeDetachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); + // Trigger enumeration of removable media on Win32 notification. + this->q->Bind(EVT_VOLUME_ATTACHED, [this](VolumeAttachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); + this->q->Bind(EVT_VOLUME_DETACHED, [this](VolumeDetachedEvent &evt) { wxGetApp().removable_drive_manager()->volumes_changed(); }); #endif /* _WIN32 */ +#if ENABLE_GCODE_VIEWER + } +#endif // ENABLE_GCODE_VIEWER // Initialize the Undo / Redo stack with a first snapshot. this->take_snapshot(_L("New Project")); @@ -2134,6 +2084,10 @@ void Plater::priv::select_view_3D(const std::string& name) set_current_panel(view3D); else if (name == "Preview") set_current_panel(preview); + +#if ENABLE_GCODE_VIEWER + wxGetApp().update_ui_from_settings(); +#endif // ENABLE_GCODE_VIEWER } void Plater::priv::select_next_view_3D() @@ -2269,6 +2223,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (!config.empty()) { Preset::normalize(config); wxGetApp().preset_bundle->load_config_model(filename.string(), std::move(config)); + if (printer_technology == ptFFF) + CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, &wxGetApp().preset_bundle->project_config); wxGetApp().load_current_presets(); is_project_file = true; } @@ -2646,8 +2602,10 @@ void Plater::priv::deselect_all() void Plater::priv::remove(size_t obj_idx) { +#if !ENABLE_GCODE_VIEWER // Prevent toolpaths preview from rendering while we modify the Print object preview->set_enabled(false); +#endif // !ENABLE_GCODE_VIEWER if (view3D->is_layers_editing_enabled()) view3D->enable_layers_editing(false); @@ -2675,14 +2633,23 @@ void Plater::priv::reset() { Plater::TakeSnapshot snapshot(q, _L("Reset Project")); + clear_warnings(); + set_project_filename(wxEmptyString); +#if !ENABLE_GCODE_VIEWER // Prevent toolpaths preview from rendering while we modify the Print object preview->set_enabled(false); +#endif // !ENABLE_GCODE_VIEWER if (view3D->is_layers_editing_enabled()) view3D->enable_layers_editing(false); +#if ENABLE_GCODE_VIEWER + reset_gcode_toolpaths(); + gcode_result.reset(); +#endif // ENABLE_GCODE_VIEWER + // Stop and reset the Print content. this->background_process.reset(); model.clear_objects(); @@ -2828,10 +2795,19 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool this->sidebar->show_sliced_info_sizer(false); // Reset preview canvases. If the print has been invalidated, the preview canvases will be cleared. // Otherwise they will be just refreshed. +#if ENABLE_GCODE_VIEWER + if (this->preview != nullptr) { + // If the preview is not visible, the following line just invalidates the preview, + // but the G-code paths or SLA preview are calculated first once the preview is made visible. + reset_gcode_toolpaths(); + this->preview->reload_print(); + } +#else if (this->preview != nullptr) // If the preview is not visible, the following line just invalidates the preview, // but the G-code paths or SLA preview are calculated first once the preview is made visible. this->preview->reload_print(); +#endif // ENABLE_GCODE_VIEWER // In FDM mode, we need to reload the 3D scene because of the wipe tower preview box. // In SLA mode, we need to reload the 3D scene every time to show the support structures. if (this->printer_technology == ptSLA || (this->printer_technology == ptFFF && this->config->opt_bool("wipe_tower"))) @@ -2844,22 +2820,13 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors. std::string err = this->background_process.validate(); if (err.empty()) { + notification_manager->set_all_slicing_errors_gray(true); if (invalidated != Print::APPLY_STATUS_UNCHANGED && this->background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; } else { - // The print is not valid. - // Only show the error message immediately, if the top level parent of this window is active. - auto p = dynamic_cast(this->q); - while (p->GetParent()) - p = p->GetParent(); - auto *top_level_wnd = dynamic_cast(p); - if (! postpone_error_messages && top_level_wnd && top_level_wnd->IsActive()) { - // The error returned from the Print needs to be translated into the local language. - GUI::show_error(this->q, err); - } else { - // Show the error message once the main window gets activated. - this->delayed_error_message = err; - } + // The print is not valid. + // Show error as notification. + notification_manager->push_slicing_error_notification(err, *q->get_current_canvas3D()); return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; } } else if (! this->delayed_error_message.empty()) { @@ -2867,6 +2834,14 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; } + //actualizate warnings + if (invalidated != Print::APPLY_STATUS_UNCHANGED) { + actualizate_warnings(this->q->model(), this->background_process.current_print()->id().id); + notification_manager->set_all_slicing_warnings_gray(true); + show_warning_dialog = false; + process_completed_with_error = false; + } + if (invalidated != Print::APPLY_STATUS_UNCHANGED && was_running && ! this->background_process.running() && (return_state & UPDATE_BACKGROUND_PROCESS_RESTART) == 0) { // The background processing was killed and it will not be restarted. @@ -2929,6 +2904,8 @@ bool Plater::priv::restart_background_process(unsigned int state) this->statusbar()->set_status_text(_L("Cancelling")); this->background_process.stop(); }); + if (!show_warning_dialog) + on_slicing_began(); return true; } } @@ -2955,6 +2932,7 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) return; + show_warning_dialog = true; if (! output_path.empty()) { background_process.schedule_export(output_path.string(), output_path_on_removable_media); } else { @@ -3314,6 +3292,8 @@ void Plater::priv::set_current_panel(wxPanel* panel) // sets the canvas as dirty to force a render at the 1st idle event (wxWidgets IsShownOnScreen() is buggy and cannot be used reliably) view3D->set_as_dirty(); view_toolbar.select_item("3D"); + if(notification_manager != nullptr) + notification_manager->set_in_preview(false); } else if (current_panel == preview) { @@ -3328,6 +3308,8 @@ void Plater::priv::set_current_panel(wxPanel* panel) preview->set_as_dirty(); view_toolbar.select_item("Preview"); + if (notification_manager != nullptr) + notification_manager->set_in_preview(true); } current_panel->SetFocusFromKbd(); @@ -3336,7 +3318,7 @@ void Plater::priv::set_current_panel(wxPanel* panel) void Plater::priv::on_select_preset(wxCommandEvent &evt) { auto preset_type = static_cast(evt.GetInt()); - auto *combo = static_cast(evt.GetEventObject()); + auto *combo = static_cast(evt.GetEventObject()); // see https://github.com/prusa3d/PrusaSlicer/issues/3889 // Under OSX: in case of use of a same names written in different case (like "ENDER" and "Ender"), @@ -3355,19 +3337,27 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt) //! instead of //! combo->GetStringSelection().ToUTF8().data()); - const std::string preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, + std::string preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, Preset::remove_suffix_modified(combo->GetString(selection).ToUTF8().data())); if (preset_type == Preset::TYPE_FILAMENT) { wxGetApp().preset_bundle->set_filament_preset(idx, preset_name); } + bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); // TODO: ? if (preset_type == Preset::TYPE_FILAMENT && sidebar->is_multifilament()) { // Only update the plater UI for the 2nd and other filaments. - wxGetApp().preset_bundle->update_plater_filament_ui(idx, combo); + combo->update(); } - else { + else if (select_preset) { + if (preset_type == Preset::TYPE_PRINTER) { + PhysicalPrinterCollection& physical_printers = wxGetApp().preset_bundle->physical_printers; + if(combo->is_selected_physical_printer()) + preset_name = physical_printers.get_selected_printer_preset_name(); + else + physical_printers.unselect_printer(); + } wxWindowUpdateLocker noUpdates(sidebar->presets_panel()); wxGetApp().get_tab(preset_type)->select_preset(preset_name); } @@ -3433,11 +3423,20 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) state = print_object->step_state_with_warnings(static_cast(warning_step)); } // Now process state.warnings. + for (auto const& warning : state.warnings) { + if (warning.current) { + notification_manager->push_slicing_warning_notification(warning.message, false, *q->get_current_canvas3D(), object_id.id, warning_step); + add_warning(warning, object_id.id); + } + } } } -void Plater::priv::on_slicing_completed(wxCommandEvent &) +void Plater::priv::on_slicing_completed(wxCommandEvent & evt) { + //notification_manager->push_notification(NotificationType::SlicingComplete, *q->get_current_canvas3D(), evt.GetInt()); + notification_manager->push_slicing_complete_notification(*q->get_current_canvas3D(), evt.GetInt(), is_sidebar_collapsed()); + switch (this->printer_technology) { case ptFFF: this->update_fff_scene(); @@ -3450,9 +3449,68 @@ void Plater::priv::on_slicing_completed(wxCommandEvent &) break; default: break; } -} -void Plater::priv::on_process_completed(wxCommandEvent &evt) +} +void Plater::priv::on_export_began(wxCommandEvent& evt) +{ + if (show_warning_dialog) + warnings_dialog(); +} +void Plater::priv::on_slicing_began() +{ + clear_warnings(); + notification_manager->close_notification_of_type(NotificationType::SlicingComplete); +} +void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, size_t oid) +{ + for (auto const& it : current_warnings) { + if (warning.message_id == it.first.message_id) { + if (warning.message_id != 0 || (warning.message_id == 0 && warning.message == it.first.message)) + return; + } + } + current_warnings.emplace_back(std::pair(warning, oid)); +} +void Plater::priv::actualizate_warnings(const Model& model, size_t print_oid) +{ + if (model.objects.size() == 0) { + clear_warnings(); + return; + } + std::vector living_oids; + living_oids.push_back(model.id().id); + living_oids.push_back(print_oid); + for (auto it = model.objects.begin(); it != model.objects.end(); ++it) { + living_oids.push_back((*it)->id().id); + } + notification_manager->compare_warning_oids(living_oids); +} +void Plater::priv::clear_warnings() +{ + notification_manager->close_slicing_errors_and_warnings(); + this->current_warnings.clear(); +} +bool Plater::priv::warnings_dialog() +{ + if (current_warnings.empty()) + return true; + std::string text = _u8L("There are active warnings concerning sliced models:\n"); + bool empt = true; + for (auto const& it : current_warnings) { + int next_n = it.first.message.find_first_of('\n', 0); + text += "\n"; + if (next_n != std::string::npos) + text += it.first.message.substr(0, next_n); + else + text += it.first.message; + } + //text += "\n\nDo you still wish to export?"; + wxMessageDialog msg_wingow(this->q, text, wxString(SLIC3R_APP_NAME " ") + _L("generated warnings"), wxOK); + const auto res = msg_wingow.ShowModal(); + return res == wxID_OK; + +} +void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) { // Stop the background task, wait until the thread goes into the "Idle" state. // At this point of time the thread should be either finished or canceled, @@ -3461,28 +3519,30 @@ void Plater::priv::on_process_completed(wxCommandEvent &evt) this->statusbar()->reset_cancel_callback(); this->statusbar()->stop_busy(); - const bool canceled = evt.GetInt() < 0; - const bool error = evt.GetInt() == 0; - const bool success = evt.GetInt() > 0; // Reset the "export G-code path" name, so that the automatic background processing will be enabled again. this->background_process.reset_export(); - if (error) { - wxString message = evt.GetString(); - if (message.IsEmpty()) - message = _L("Export failed"); - if (q->m_tracking_popup_menu) - // We don't want to pop-up a message box when tracking a pop-up menu. - // We postpone the error message instead. - q->m_tracking_popup_menu_error_message = message; - else - show_error(q, message); - this->statusbar()->set_status_text(message); + if (evt.error()) { + std::string message = evt.format_error_message(); + if (evt.critical_error()) { + if (q->m_tracking_popup_menu) + // We don't want to pop-up a message box when tracking a pop-up menu. + // We postpone the error message instead. + q->m_tracking_popup_menu_error_message = message; + else + show_error(q, message); + } else + notification_manager->push_slicing_error_notification(message, *q->get_current_canvas3D()); + this->statusbar()->set_status_text(from_u8(message)); + const wxString invalid_str = _L("Invalid data"); + for (auto btn : { ActionButtonType::abReslice, ActionButtonType::abSendGCode, ActionButtonType::abExport }) + sidebar->set_btn_label(btn, invalid_str); + process_completed_with_error = true; } - if (canceled) + if (evt.cancelled()) this->statusbar()->set_status_text(_L("Cancelled")); - this->sidebar->show_sliced_info_sizer(success); + this->sidebar->show_sliced_info_sizer(evt.success()); // This updates the "Slice now", "Export G-code", "Arrange" buttons status. // Namely, it refreshes the "Out of print bed" property of all the ModelObjects, and it enables @@ -3503,15 +3563,21 @@ void Plater::priv::on_process_completed(wxCommandEvent &evt) default: break; } - - if (canceled) { + if (evt.cancelled()) { if (wxGetApp().get_mode() == comSimple) sidebar->set_btn_label(ActionButtonType::abReslice, "Slice now"); show_action_buttons(true); } - else if (this->writing_to_removable_device || wxGetApp().get_mode() == comSimple) + else if (wxGetApp().get_mode() == comSimple) + { show_action_buttons(false); - this->writing_to_removable_device = false; + } + else if (this->writing_to_removable_device) + { + show_action_buttons(false); + notification_manager->push_notification(NotificationType::ExportToRemovableFinished, *q->get_current_canvas3D()); + } + this->writing_to_removable_device = false; } void Plater::priv::on_layer_editing_toggled(bool enable) @@ -3938,7 +4004,11 @@ bool Plater::priv::init_view_toolbar() return false; view_toolbar.select_item("3D"); - view_toolbar.set_enabled(true); + +#if ENABLE_GCODE_VIEWER + if (wxGetApp().is_editor()) +#endif // ENABLE_GCODE_VIEWER + view_toolbar.set_enabled(true); return true; } @@ -3988,6 +4058,25 @@ bool Plater::priv::init_collapse_toolbar() return true; } +#if ENABLE_GCODE_VIEWER +void Plater::priv::update_preview_bottom_toolbar() +{ + preview->update_bottom_toolbar(); +} + +void Plater::priv::update_preview_moves_slider() +{ + preview->update_moves_slider(); +} +#endif // ENABLE_GCODE_VIEWER + +#if ENABLE_GCODE_VIEWER +void Plater::priv::reset_gcode_toolpaths() +{ + preview->get_canvas3d()->reset_gcode_toolpaths(); +} +#endif // ENABLE_GCODE_VIEWER + bool Plater::priv::can_set_instance_to_object() const { const int obj_idx = get_selected_object_idx(); @@ -4062,11 +4151,10 @@ bool Plater::priv::can_reload_from_disk() const return !paths.empty(); } -void Plater::priv::set_bed_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model) +void Plater::priv::set_bed_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) { - bool new_shape = bed.set_shape(shape, custom_texture, custom_model); - if (new_shape) - { + bool new_shape = bed.set_shape(shape, custom_texture, custom_model, force_as_custom); + if (new_shape) { if (view3D) view3D->bed_shape_changed(); if (preview) preview->bed_shape_changed(); } @@ -4142,7 +4230,12 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice) const this->ready_to_slice = ready_to_slice; wxWindowUpdateLocker noUpdater(sidebar); - const auto prin_host_opt = config->option("print_host"); + + DynamicPrintConfig* selected_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); + if (!selected_printer_config) + selected_printer_config = config; + + const auto prin_host_opt = selected_printer_config->option("print_host"); const bool send_gcode_shown = prin_host_opt != nullptr && !prin_host_opt->value.empty(); // when a background processing is ON, export_btn and/or send_btn are showing @@ -4153,7 +4246,7 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice) const sidebar->show_export(true) | sidebar->show_send(send_gcode_shown) | sidebar->show_export_removable(removable_media_status.has_removable_drives) | - sidebar->show_disconnect(removable_media_status.has_eject)) + sidebar->show_eject(removable_media_status.has_eject)) sidebar->Layout(); } else @@ -4165,7 +4258,7 @@ void Plater::priv::show_action_buttons(const bool ready_to_slice) const sidebar->show_export(!ready_to_slice) | sidebar->show_send(send_gcode_shown && !ready_to_slice) | sidebar->show_export_removable(!ready_to_slice && removable_media_status.has_removable_drives) | - sidebar->show_disconnect(!ready_to_slice && removable_media_status.has_eject)) + sidebar->show_eject(!ready_to_slice && removable_media_status.has_eject)) sidebar->Layout(); } } @@ -4514,6 +4607,44 @@ void Plater::extract_config_from_project() load_files(input_paths, false, true); } +#if ENABLE_GCODE_VIEWER +void Plater::load_gcode() +{ + // Ask user for a gcode file name. + wxString input_file; + wxGetApp().load_gcode(this, input_file); + // And finally load the gcode file. + load_gcode(input_file); +} + +void Plater::load_gcode(const wxString& filename) +{ + if (filename.empty() || m_last_loaded_gcode == filename) + return; + + m_last_loaded_gcode = filename; + + // cleanup view before to start loading/processing + p->gcode_result.reset(); + reset_gcode_toolpaths(); + p->preview->reload_print(false); + p->get_current_canvas3D()->render(); + + wxBusyCursor wait; + + // process gcode + GCodeProcessor processor; + processor.enable_producers(true); + processor.enable_machine_envelope_processing(true); + processor.process_file(filename.ToUTF8().data()); + p->gcode_result = std::move(processor.extract_result()); + + // show results + p->preview->reload_print(false); + p->preview->get_canvas3d()->zoom_to_gcode(); +} +#endif // ENABLE_GCODE_VIEWER + std::vector Plater::load_files(const std::vector& input_files, bool load_model, bool load_config, bool imperial_units /*= false*/) { return p->load_files(input_files, load_model, load_config, imperial_units); } // To be called when providing a list of files to the GUI slic3r on command line. @@ -4728,6 +4859,9 @@ void Plater::export_gcode(bool prefer_removable) if (p->model.objects.empty()) return; + if (p->process_completed_with_error)//here + return; + // If possible, remove accents from accented latin characters. // This function is useful for generating file names to be processed by legacy firmwares. fs::path default_output_file; @@ -4987,7 +5121,6 @@ void Plater::export_toolpaths_to_obj() const p->preview->get_canvas3d()->export_toolpaths_to_obj(into_u8(path).c_str()); } - void Plater::reslice() { // Stop arrange and (or) optimize rotation tasks. @@ -5012,6 +5145,9 @@ void Plater::reslice() if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) return; +#if ENABLE_GCODE_VIEWER + bool clean_gcode_toolpaths = true; +#endif // ENABLE_GCODE_VIEWER if (p->background_process.running()) { if (wxGetApp().get_mode() == comSimple) @@ -5024,9 +5160,19 @@ void Plater::reslice() } else if (!p->background_process.empty() && !p->background_process.idle()) p->show_action_buttons(true); +#if ENABLE_GCODE_VIEWER + else + clean_gcode_toolpaths = false; + + if (clean_gcode_toolpaths) + reset_gcode_toolpaths(); + // update type of preview + p->preview->update_view_type(!clean_gcode_toolpaths); +#else // update type of preview p->preview->update_view_type(true); +#endif // ENABLE_GCODE_VIEWER } void Plater::reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages) @@ -5069,7 +5215,9 @@ void Plater::send_gcode() { if (p->model.objects.empty()) { return; } - PrintHostJob upload_job(p->config); + // if physical_printer is selected, send gcode for this printer + DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); + PrintHostJob upload_job(physical_printer_config ? physical_printer_config : p->config); if (upload_job.empty()) { return; } // Obtain default output path @@ -5180,12 +5328,12 @@ void Plater::on_extruders_change(size_t num_extruders) size_t i = choices.size(); while ( i < num_extruders ) { - PresetComboBox* choice/*{ nullptr }*/; + PlaterPresetComboBox* choice/*{ nullptr }*/; sidebar().init_filament_combo(&choice, i); choices.push_back(choice); // initialize selection - wxGetApp().preset_bundle->update_plater_filament_ui(i, choice); + choice->update(); ++i; } @@ -5231,6 +5379,9 @@ void Plater::on_config_change(const DynamicPrintConfig &config) this->set_printer_technology(config.opt_enum(opt_key)); // print technology is changed, so we should to update a search list p->sidebar->update_searcher(); +#if ENABLE_GCODE_VIEWER + p->reset_gcode_toolpaths(); +#endif // ENABLE_GCODE_VIEWER } else if ((opt_key == "bed_shape") || (opt_key == "bed_custom_texture") || (opt_key == "bed_custom_model")) { bed_shape_changed = true; @@ -5262,9 +5413,7 @@ void Plater::on_config_change(const DynamicPrintConfig &config) } if (bed_shape_changed) - p->set_bed_shape(p->config->option("bed_shape")->values, - p->config->option("bed_custom_texture")->value, - p->config->option("bed_custom_model")->value); + set_bed_shape(); if (update_scheduled) update(); @@ -5275,11 +5424,24 @@ void Plater::on_config_change(const DynamicPrintConfig &config) void Plater::set_bed_shape() const { - p->set_bed_shape(p->config->option("bed_shape")->values, +#if ENABLE_GCODE_VIEWER + set_bed_shape(p->config->option("bed_shape")->values, + p->config->option("bed_custom_texture")->value, + p->config->option("bed_custom_model")->value); +#else + p->set_bed_shape(p->config->option("bed_shape")->values, p->config->option("bed_custom_texture")->value, p->config->option("bed_custom_model")->value); +#endif // ENABLE_GCODE_VIEWER } +#if ENABLE_GCODE_VIEWER +void Plater::set_bed_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) const +{ + p->set_bed_shape(shape, custom_texture, custom_model, force_as_custom); +} +#endif // ENABLE_GCODE_VIEWER + void Plater::force_filament_colors_update() { bool update_scheduled = false; @@ -5440,24 +5602,39 @@ PrinterTechnology Plater::printer_technology() const const DynamicPrintConfig * Plater::config() const { return p->config; } +#if ENABLE_GCODE_VIEWER +bool Plater::set_printer_technology(PrinterTechnology printer_technology) +#else void Plater::set_printer_technology(PrinterTechnology printer_technology) +#endif // ENABLE_GCODE_VIEWER { p->printer_technology = printer_technology; +#if ENABLE_GCODE_VIEWER + bool ret = p->background_process.select_technology(printer_technology); + if (ret) { + // Update the active presets. + } +#else if (p->background_process.select_technology(printer_technology)) { // Update the active presets. } +#endif // ENABLE_GCODE_VIEWER //FIXME for SLA synchronize //p->background_process.apply(Model)! p->label_btn_export = printer_technology == ptFFF ? L("Export G-code") : L("Export"); p->label_btn_send = printer_technology == ptFFF ? L("Send G-code") : L("Send to printer"); - if (wxGetApp().mainframe) + if (wxGetApp().mainframe != nullptr) wxGetApp().mainframe->update_menubar(); p->update_main_toolbar_tooltips(); p->sidebar->get_searcher().set_printer_technology(printer_technology); + +#if ENABLE_GCODE_VIEWER + return ret; +#endif // ENABLE_GCODE_VIEWER } void Plater::changed_object(int obj_idx) @@ -5605,11 +5782,25 @@ bool Plater::init_view_toolbar() return p->init_view_toolbar(); } +#if ENABLE_GCODE_VIEWER +void Plater::enable_view_toolbar(bool enable) +{ + p->view_toolbar.set_enabled(enable); +} +#endif // ENABLE_GCODE_VIEWER + bool Plater::init_collapse_toolbar() { return p->init_collapse_toolbar(); } +#if ENABLE_GCODE_VIEWER +void Plater::enable_collapse_toolbar(bool enable) +{ + p->collapse_toolbar.set_enabled(enable); +} +#endif // ENABLE_GCODE_VIEWER + const Camera& Plater::get_camera() const { return p->camera; @@ -5663,6 +5854,23 @@ GLToolbar& Plater::get_collapse_toolbar() return p->collapse_toolbar; } +#if ENABLE_GCODE_VIEWER +void Plater::update_preview_bottom_toolbar() +{ + p->update_preview_bottom_toolbar(); +} + +void Plater::update_preview_moves_slider() +{ + p->update_preview_moves_slider(); +} + +void Plater::reset_gcode_toolpaths() +{ + p->reset_gcode_toolpaths(); +} +#endif // ENABLE_GCODE_VIEWER + const Mouse3DController& Plater::get_mouse3d_controller() const { return p->mouse3d_controller; @@ -5673,6 +5881,16 @@ Mouse3DController& Plater::get_mouse3d_controller() return p->mouse3d_controller; } +const NotificationManager* Plater::get_notification_manager() const +{ + return p->notification_manager; +} + +NotificationManager* Plater::get_notification_manager() +{ + return p->notification_manager; +} + bool Plater::can_delete() const { return p->can_delete(); } bool Plater::can_delete_all() const { return p->can_delete_all(); } bool Plater::can_increase_instances() const { return p->can_increase_instances(); } @@ -5720,6 +5938,9 @@ bool Plater::can_undo() const { return p->undo_redo_stack().has_undo_snapshot(); bool Plater::can_redo() const { return p->undo_redo_stack().has_redo_snapshot(); } bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); } const UndoRedo::Stack& Plater::undo_redo_stack_main() const { return p->undo_redo_stack_main(); } +#if ENABLE_GCODE_VIEWER +void Plater::clear_undo_redo_stack_main() { p->undo_redo_stack_main().clear(); } +#endif // ENABLE_GCODE_VIEWER void Plater::enter_gizmos_stack() { p->enter_gizmos_stack(); } void Plater::leave_gizmos_stack() { p->leave_gizmos_stack(); } bool Plater::inside_snapshot_capture() { return p->inside_snapshot_capture(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 5d60e006b0..cc80186202 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -6,14 +6,12 @@ #include #include -#include -#include "Preset.hpp" #include "Selection.hpp" +#include "libslic3r/Preset.hpp" #include "libslic3r/BoundingBox.hpp" #include "Jobs/Job.hpp" -#include "wxExtensions.hpp" #include "Search.hpp" class wxButton; @@ -47,49 +45,17 @@ class ObjectLayers; class ObjectList; class GLCanvas3D; class Mouse3DController; +class NotificationManager; struct Camera; class Bed3D; class GLToolbar; +class PlaterPresetComboBox; using t_optgroups = std::vector >; class Plater; enum class ActionButtonType : int; -class PresetComboBox : public PresetBitmapComboBox -{ -public: - PresetComboBox(wxWindow *parent, Preset::Type preset_type); - ~PresetComboBox(); - - ScalableButton* edit_btn { nullptr }; - - enum LabelItemType { - LABEL_ITEM_MARKER = 0xffffff01, - LABEL_ITEM_WIZARD_PRINTERS, - LABEL_ITEM_WIZARD_FILAMENTS, - LABEL_ITEM_WIZARD_MATERIALS, - - LABEL_ITEM_MAX, - }; - - void set_label_marker(int item, LabelItemType label_item_type = LABEL_ITEM_MARKER); - void set_extruder_idx(const int extr_idx) { extruder_idx = extr_idx; } - int get_extruder_idx() const { return extruder_idx; } - int em_unit() const { return m_em_unit; } - void check_selection(int selection); - - void msw_rescale(); - -private: - typedef std::size_t Marker; - - Preset::Type preset_type; - int last_selected; - int extruder_idx = -1; - int m_em_unit; -}; - class Sidebar : public wxPanel { ConfigOptionMode m_mode; @@ -101,7 +67,7 @@ public: Sidebar &operator=(const Sidebar &) = delete; ~Sidebar(); - void init_filament_combo(PresetComboBox **combo, const int extr_idx); + void init_filament_combo(PlaterPresetComboBox **combo, const int extr_idx); void remove_unused_filament_combos(const size_t current_extruder_count); void update_all_preset_comboboxes(); void update_presets(Slic3r::Preset::Type preset_type); @@ -130,8 +96,9 @@ public: bool show_reslice(bool show) const; bool show_export(bool show) const; bool show_send(bool show) const; - bool show_disconnect(bool show)const; + bool show_eject(bool show)const; bool show_export_removable(bool show) const; + bool get_eject_shown() const; bool is_multifilament(); void update_mode(); bool is_collapsed(); @@ -139,7 +106,7 @@ public: void update_searcher(); void update_ui_from_settings(); - std::vector& combos_filament(); + std::vector& combos_filament(); Search::OptionsSearcher& get_searcher(); std::string& get_search_line(); @@ -173,6 +140,10 @@ public: void add_model(bool imperial_units = false); void import_sl1_archive(); void extract_config_from_project(); +#if ENABLE_GCODE_VIEWER + void load_gcode(); + void load_gcode(const wxString& filename); +#endif // ENABLE_GCODE_VIEWER std::vector load_files(const std::vector& input_files, bool load_model = true, bool load_config = true, bool imperial_units = false); // To be called when providing a list of files to the GUI slic3r on command line. @@ -220,7 +191,7 @@ public: void cut(size_t obj_idx, size_t instance_idx, coordf_t z, bool keep_upper = true, bool keep_lower = true, bool rotate_lower = false); - void export_gcode(bool prefer_removable = true); + void export_gcode(bool prefer_removable); void export_stl(bool extended = false, bool selection_only = false); void export_amf(); void export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path()); @@ -252,6 +223,9 @@ public: bool search_string_getter(int idx, const char** label, const char** tooltip); // For the memory statistics. const Slic3r::UndoRedo::Stack& undo_redo_stack_main() const; +#if ENABLE_GCODE_VIEWER + void clear_undo_redo_stack_main(); +#endif // ENABLE_GCODE_VIEWER // Enter / leave the Gizmos specific Undo / Redo stack. To be used by the SLA support point editing gizmo. void enter_gizmos_stack(); void leave_gizmos_stack(); @@ -289,7 +263,11 @@ public: PrinterTechnology printer_technology() const; const DynamicPrintConfig * config() const; +#if ENABLE_GCODE_VIEWER + bool set_printer_technology(PrinterTechnology printer_technology); +#else void set_printer_technology(PrinterTechnology printer_technology); +#endif // ENABLE_GCODE_VIEWER void copy_selection_to_clipboard(); void paste_from_clipboard(); @@ -315,7 +293,13 @@ public: void sys_color_changed(); bool init_view_toolbar(); +#if ENABLE_GCODE_VIEWER + void enable_view_toolbar(bool enable); +#endif // ENABLE_GCODE_VIEWER bool init_collapse_toolbar(); +#if ENABLE_GCODE_VIEWER + void enable_collapse_toolbar(bool enable); +#endif // ENABLE_GCODE_VIEWER const Camera& get_camera() const; Camera& get_camera(); @@ -334,10 +318,24 @@ public: const GLToolbar& get_collapse_toolbar() const; GLToolbar& get_collapse_toolbar(); +#if ENABLE_GCODE_VIEWER + void update_preview_bottom_toolbar(); + void update_preview_moves_slider(); + + void reset_gcode_toolpaths(); + void reset_last_loaded_gcode() { m_last_loaded_gcode = ""; } +#endif // ENABLE_GCODE_VIEWER + const Mouse3DController& get_mouse3d_controller() const; Mouse3DController& get_mouse3d_controller(); void set_bed_shape() const; +#if ENABLE_GCODE_VIEWER + void set_bed_shape(const Pointfs& shape, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom = false) const; +#endif // ENABLE_GCODE_VIEWER + + const NotificationManager* get_notification_manager() const; + NotificationManager* get_notification_manager(); // ROII wrapper for suppressing the Undo / Redo snapshot to be taken. class SuppressSnapshots @@ -388,6 +386,10 @@ private: bool m_tracking_popup_menu = false; wxString m_tracking_popup_menu_error_message; +#if ENABLE_GCODE_VIEWER + wxString m_last_loaded_gcode; +#endif // ENABLE_GCODE_VIEWER + void suppress_snapshots(); void allow_snapshots(); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 02e4a899d2..a3a23fd851 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1,8 +1,8 @@ #include "Preferences.hpp" -#include "AppConfig.hpp" #include "OptionsGroup.hpp" #include "GUI_App.hpp" #include "I18N.hpp" +#include "libslic3r/AppConfig.hpp" namespace Slic3r { namespace GUI { @@ -131,6 +131,15 @@ void PreferencesDialog::build() option = Option(def, "use_inches"); m_optgroup_general->append_single_option_line(option); */ + + // Show/Hide splash screen + def.label = L("Show splash screen"); + def.type = coBool; + def.tooltip = L("Show splash screen"); + def.set_default_value(new ConfigOptionBool{ app_config->get("show_splash_screen") == "1" }); + option = Option(def, "show_splash_screen"); + m_optgroup_general->append_single_option_line(option); + m_optgroup_camera = std::make_shared(this, _(L("Camera"))); m_optgroup_camera->label_width = 40; m_optgroup_camera->m_on_change = [this](t_config_option_key opt_key, boost::any value) { @@ -234,30 +243,6 @@ void PreferencesDialog::accept() } } -#if !ENABLE_LAYOUT_NO_RESTART - if (m_settings_layout_changed) { - // the dialog needs to be destroyed before the call to recreate_gui() - // or sometimes the application crashes into wxDialogBase() destructor - // so we put it into an inner scope - wxMessageDialog dialog(nullptr, - _L("Switching the settings layout mode will trigger application restart.\n" - "You will lose content of the plater.") + "\n\n" + - _L("Do you want to proceed?"), - wxString(SLIC3R_APP_NAME) + " - " + _L("Switching the settings layout mode"), - wxICON_QUESTION | wxOK | wxCANCEL); - - if (dialog.ShowModal() == wxID_CANCEL) - { - int selection = app_config->get("old_settings_layout_mode") == "1" ? 0 : - app_config->get("new_settings_layout_mode") == "1" ? 1 : - app_config->get("dlg_settings_layout_mode") == "1" ? 2 : 0; - - m_layout_mode_box->SetSelection(selection); - return; - } - } -#endif // !ENABLE_LAYOUT_NO_RESTART - for (std::map::iterator it = m_values.begin(); it != m_values.end(); ++it) app_config->set(it->first, it->second); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp new file mode 100644 index 0000000000..8bc9393873 --- /dev/null +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -0,0 +1,1369 @@ +#include "PresetComboBoxes.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/PresetBundle.hpp" + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "MainFrame.hpp" +#include "format.hpp" +#include "Tab.hpp" +#include "ConfigWizard.hpp" +#include "../Utils/ASCIIFolding.hpp" +#include "../Utils/FixModelByWin10.hpp" +#include "../Utils/UndoRedo.hpp" +#include "BitmapCache.hpp" +#include "PhysicalPrinterDialog.hpp" + +using Slic3r::GUI::format_wxstr; + +namespace Slic3r { +namespace GUI { + +#define BORDER_W 10 + +// --------------------------------- +// *** PresetComboBox *** +// --------------------------------- + +/* For PresetComboBox we use bitmaps that are created from images that are already scaled appropriately for Retina + * (Contrary to the intuition, the `scale` argument for Bitmap's constructor doesn't mean + * "please scale this to such and such" but rather + * "the wxImage is already sized for backing scale such and such". ) + * Unfortunately, the constructor changes the size of wxBitmap too. + * Thus We need to use unscaled size value for bitmaps that we use + * to avoid scaled size of control items. + * For this purpose control drawing methods and + * control size calculation methods (virtual) are overridden. + **/ + +PresetComboBox::PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size) : + wxBitmapComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, 0, nullptr, wxCB_READONLY), + m_type(preset_type), + m_last_selected(wxNOT_FOUND), + m_em_unit(em_unit(this)), + m_preset_bundle(wxGetApp().preset_bundle) +{ + SetFont(wxGetApp().normal_font()); +#ifdef _WIN32 + // Workaround for ignoring CBN_EDITCHANGE events, which are processed after the content of the combo box changes, so that + // the index of the item inside CBN_EDITCHANGE may no more be valid. + EnableTextChangedEvents(false); +#endif /* _WIN32 */ + + switch (m_type) + { + case Preset::TYPE_PRINT: { + m_collection = &m_preset_bundle->prints; + m_main_bitmap_name = "cog"; + break; + } + case Preset::TYPE_FILAMENT: { + m_collection = &m_preset_bundle->filaments; + m_main_bitmap_name = "spool"; + break; + } + case Preset::TYPE_SLA_PRINT: { + m_collection = &m_preset_bundle->sla_prints; + m_main_bitmap_name = "cog"; + break; + } + case Preset::TYPE_SLA_MATERIAL: { + m_collection = &m_preset_bundle->sla_materials; + m_main_bitmap_name = "resin"; + break; + } + case Preset::TYPE_PRINTER: { + m_collection = &m_preset_bundle->printers; + m_main_bitmap_name = "printer"; + break; + } + default: break; + } + + m_bitmapCompatible = ScalableBitmap(this, "flag_green"); + m_bitmapIncompatible = ScalableBitmap(this, "flag_red"); + + // parameters for an icon's drawing + fill_width_height(); + + Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { + // see https://github.com/prusa3d/PrusaSlicer/issues/3889 + // Under OSX: in case of use of a same names written in different case (like "ENDER" and "Ender") + // m_presets_choice->GetSelection() will return first item, because search in PopupListCtrl is case-insensitive. + // So, use GetSelection() from event parameter + auto selected_item = evt.GetSelection(); + + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + if (marker >= LABEL_ITEM_DISABLED && marker < LABEL_ITEM_MAX) + this->SetSelection(this->m_last_selected); + else if (on_selection_changed && (m_last_selected != selected_item || m_collection->current_is_dirty())) { + m_last_selected = selected_item; + on_selection_changed(selected_item); + evt.StopPropagation(); + } + evt.Skip(); + }); +} + +PresetComboBox::~PresetComboBox() +{ +} + +BitmapCache& PresetComboBox::bitmap_cache() +{ + static BitmapCache bmps; + return bmps; +} + +void PresetComboBox::set_label_marker(int item, LabelItemType label_item_type) +{ + this->SetClientData(item, (void*)label_item_type); +} + +bool PresetComboBox::set_printer_technology(PrinterTechnology pt) +{ + if (printer_technology != pt) { + printer_technology = pt; + return true; + } + return false; +} + +void PresetComboBox::invalidate_selection() +{ + m_last_selected = INT_MAX; // this value means that no one item is selected +} + +void PresetComboBox::validate_selection(bool predicate/*=false*/) +{ + if (predicate || + // just in case: mark m_last_selected as a first added element + m_last_selected == INT_MAX) + m_last_selected = GetCount() - 1; +} + +void PresetComboBox::update_selection() +{ + /* If selected_preset_item is still equal to INT_MAX, it means that + * there is no presets added to the list. + * So, select last combobox item ("Add/Remove preset") + */ + validate_selection(); + + SetSelection(m_last_selected); + SetToolTip(GetString(m_last_selected)); +} + +void PresetComboBox::update(std::string select_preset_name) +{ + Freeze(); + Clear(); + invalidate_selection(); + + const std::deque& presets = m_collection->get_presets(); + + std::map> nonsys_presets; + std::map incomp_presets; + + wxString selected = ""; + if (!presets.front().is_visible) + set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + + for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) + { + const Preset& preset = presets[i]; + if (!preset.is_visible || !preset.is_compatible) + continue; + + // marker used for disable incompatible printer models for the selected physical printer + bool is_enabled = m_type == Preset::TYPE_PRINTER && printer_technology != ptAny ? preset.printer_technology() == printer_technology : true; + if (select_preset_name.empty() && is_enabled) + select_preset_name = preset.name; + + std::string bitmap_key = "cb"; + if (m_type == Preset::TYPE_PRINTER) { + bitmap_key += "_printer"; + if (preset.printer_technology() == ptSLA) + bitmap_key += "_sla"; + } + std::string main_icon_name = m_type == Preset::TYPE_PRINTER && preset.printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; + + wxBitmap* bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default); + assert(bmp); + + if (!is_enabled) + incomp_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), bmp); + else if (preset.is_default || preset.is_system) + { + Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), *bmp); + validate_selection(preset.name == select_preset_name); + } + else + { + nonsys_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), std::pair(bmp, is_enabled)); + if (preset.name == select_preset_name || (select_preset_name.empty() && is_enabled)) + selected = wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()); + } + if (i + 1 == m_collection->num_default_presets()) + set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + } + if (!nonsys_presets.empty()) + { + set_label_marker(Append(separator(L("User presets")), wxNullBitmap)); + for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + int item_id = Append(it->first, *it->second.first); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + if (!incomp_presets.empty()) + { + set_label_marker(Append(separator(L("Incompatible presets")), wxNullBitmap)); + for (std::map::iterator it = incomp_presets.begin(); it != incomp_presets.end(); ++it) { + set_label_marker(Append(it->first, *it->second), LABEL_ITEM_DISABLED); + } + } + + update_selection(); + Thaw(); +} + +void PresetComboBox::update() +{ + this->update(into_u8(this->GetString(this->GetSelection()))); +} + +void PresetComboBox::msw_rescale() +{ + m_em_unit = em_unit(this); + + m_bitmapIncompatible.msw_rescale(); + m_bitmapCompatible.msw_rescale(); + + // parameters for an icon's drawing + fill_width_height(); + + // update the control to redraw the icons + update(); +} + +void PresetComboBox::fill_width_height() +{ + // To avoid asserts, each added bitmap to wxBitmapCombobox should be the same size, so + // set a bitmap's height to m_bitmapCompatible->GetHeight() and norm_icon_width to m_bitmapCompatible->GetWidth() + icon_height = m_bitmapCompatible.GetBmpHeight(); + norm_icon_width = m_bitmapCompatible.GetBmpWidth(); + + /* It's supposed that standard size of an icon is 16px*16px for 100% scaled display. + * So set sizes for solid_colored icons used for filament preset + * and scale them in respect to em_unit value + */ + const float scale_f = (float)m_em_unit * 0.1f; + + thin_icon_width = lroundf(8 * scale_f); // analogue to 8px; + wide_icon_width = norm_icon_width + thin_icon_width; + + space_icon_width = lroundf(2 * scale_f); + thin_space_icon_width = 2 * space_icon_width; + wide_space_icon_width = 3 * space_icon_width; +} + +wxString PresetComboBox::separator(const std::string& label) +{ + return wxString::FromUTF8(separator_head()) + _(label) + wxString::FromUTF8(separator_tail()); +} + +wxBitmap* PresetComboBox::get_bmp( std::string bitmap_key, bool wide_icons, const std::string& main_icon_name, + bool is_compatible/* = true*/, bool is_system/* = false*/, bool is_single_bar/* = false*/, + std::string filament_rgb/* = ""*/, std::string extruder_rgb/* = ""*/) +{ + // If the filament preset is not compatible and there is a "red flag" icon loaded, show it left + // to the filament color image. + if (wide_icons) + bitmap_key += is_compatible ? ",cmpt" : ",ncmpt"; + + bitmap_key += is_system ? ",syst" : ",nsyst"; + bitmap_key += ",h" + std::to_string(icon_height); + + wxBitmap* bmp = bitmap_cache().find(bitmap_key); + if (bmp == nullptr) { + // Create the bitmap with color bars. + std::vector bmps; + if (wide_icons) + // Paint a red flag for incompatible presets. + bmps.emplace_back(is_compatible ? bitmap_cache().mkclear(norm_icon_width, icon_height) : m_bitmapIncompatible.bmp()); + + if (m_type == Preset::TYPE_FILAMENT) + { + unsigned char rgb[3]; + // Paint the color bars. + bitmap_cache().parse_color(filament_rgb, rgb); + bmps.emplace_back(bitmap_cache().mksolid(is_single_bar ? wide_icon_width : norm_icon_width, icon_height, rgb)); + if (!is_single_bar) { + bitmap_cache().parse_color(extruder_rgb, rgb); + bmps.emplace_back(bitmap_cache().mksolid(thin_icon_width, icon_height, rgb)); + } + // Paint a lock at the system presets. + bmps.emplace_back(bitmap_cache().mkclear(space_icon_width, icon_height)); + } + else + { + // Paint the color bars. + bmps.emplace_back(bitmap_cache().mkclear(thin_space_icon_width, icon_height)); + bmps.emplace_back(create_scaled_bitmap(main_icon_name)); + // Paint a lock at the system presets. + bmps.emplace_back(bitmap_cache().mkclear(wide_space_icon_width, icon_height)); + } + bmps.emplace_back(is_system ? create_scaled_bitmap("lock_closed") : bitmap_cache().mkclear(norm_icon_width, icon_height)); + bmp = bitmap_cache().insert(bitmap_key, bmps); + } + + return bmp; +} + +wxBitmap* PresetComboBox::get_bmp( std::string bitmap_key, const std::string& main_icon_name, const std::string& next_icon_name, + bool is_enabled/* = true*/, bool is_compatible/* = true*/, bool is_system/* = false*/) +{ + bitmap_key += !is_enabled ? "_disabled" : ""; + bitmap_key += is_compatible ? ",cmpt" : ",ncmpt"; + bitmap_key += is_system ? ",syst" : ",nsyst"; + bitmap_key += ",h" + std::to_string(icon_height); + + wxBitmap* bmp = bitmap_cache().find(bitmap_key); + if (bmp == nullptr) { + // Create the bitmap with color bars. + std::vector bmps; + bmps.emplace_back(m_type == Preset::TYPE_PRINTER ? create_scaled_bitmap(main_icon_name, this, 16, !is_enabled) : + is_compatible ? m_bitmapCompatible.bmp() : m_bitmapIncompatible.bmp()); + // Paint a lock at the system presets. + bmps.emplace_back(is_system ? create_scaled_bitmap(next_icon_name, this, 16, !is_enabled) : bitmap_cache().mkclear(norm_icon_width, icon_height)); + bmp = bitmap_cache().insert(bitmap_key, bmps); + } + + return bmp; +} + +bool PresetComboBox::is_selected_physical_printer() +{ + auto selected_item = this->GetSelection(); + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + return marker == LABEL_ITEM_PHYSICAL_PRINTER; +} + +bool PresetComboBox::selection_is_changed_according_to_physical_printers() +{ + if (m_type != Preset::TYPE_PRINTER || !is_selected_physical_printer()) + return false; + + PhysicalPrinterCollection& physical_printers = m_preset_bundle->physical_printers; + + std::string selected_string = this->GetString(this->GetSelection()).ToUTF8().data(); + + std::string old_printer_full_name, old_printer_preset; + if (physical_printers.has_selection()) { + old_printer_full_name = physical_printers.get_selected_full_printer_name(); + old_printer_preset = physical_printers.get_selected_printer_preset_name(); + } + else + old_printer_preset = m_collection->get_edited_preset().name; + // Select related printer preset on the Printer Settings Tab + physical_printers.select_printer(selected_string); + std::string preset_name = physical_printers.get_selected_printer_preset_name(); + + // if new preset wasn't selected, there is no need to call update preset selection + if (old_printer_preset == preset_name) { + // we need just to update according Plater<->Tab PresetComboBox + if (dynamic_cast(this)!=nullptr) { + wxGetApp().get_tab(m_type)->update_preset_choice(); + // Synchronize config.ini with the current selections. + m_preset_bundle->export_selections(*wxGetApp().app_config); + } + else if (dynamic_cast(this)!=nullptr) + wxGetApp().sidebar().update_presets(m_type); + + this->update(); + return true; + } + + Tab* tab = wxGetApp().get_tab(Preset::TYPE_PRINTER); + if (tab) + tab->select_preset(preset_name, false, old_printer_full_name); + return true; +} + +#ifdef __APPLE__ +bool PresetComboBox::OnAddBitmap(const wxBitmap& bitmap) +{ + if (bitmap.IsOk()) + { + // we should use scaled! size values of bitmap + int width = (int)bitmap.GetScaledWidth(); + int height = (int)bitmap.GetScaledHeight(); + + if (m_usedImgSize.x < 0) + { + // If size not yet determined, get it from this image. + m_usedImgSize.x = width; + m_usedImgSize.y = height; + + // Adjust control size to vertically fit the bitmap + wxWindow* ctrl = GetControl(); + ctrl->InvalidateBestSize(); + wxSize newSz = ctrl->GetBestSize(); + wxSize sz = ctrl->GetSize(); + if (newSz.y > sz.y) + ctrl->SetSize(sz.x, newSz.y); + else + DetermineIndent(); + } + + wxCHECK_MSG(width == m_usedImgSize.x && height == m_usedImgSize.y, + false, + "you can only add images of same size"); + + return true; + } + + return false; +} + +void PresetComboBox::OnDrawItem(wxDC& dc, + const wxRect& rect, + int item, + int flags) const +{ + const wxBitmap& bmp = *(wxBitmap*)m_bitmaps[item]; + if (bmp.IsOk()) + { + // we should use scaled! size values of bitmap + wxCoord w = bmp.GetScaledWidth(); + wxCoord h = bmp.GetScaledHeight(); + + const int imgSpacingLeft = 4; + + // Draw the image centered + dc.DrawBitmap(bmp, + rect.x + (m_usedImgSize.x - w) / 2 + imgSpacingLeft, + rect.y + (rect.height - h) / 2, + true); + } + + wxString text = GetString(item); + if (!text.empty()) + dc.DrawText(text, + rect.x + m_imgAreaWidth + 1, + rect.y + (rect.height - dc.GetCharHeight()) / 2); +} +#endif + + +// --------------------------------- +// *** PlaterPresetComboBox *** +// --------------------------------- + +PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset_type) : + PresetComboBox(parent, preset_type, wxSize(15 * wxGetApp().em_unit(), -1)) +{ + Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &evt) { + auto selected_item = evt.GetSelection(); + + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + if (marker >= LABEL_ITEM_MARKER && marker < LABEL_ITEM_MAX) { + this->SetSelection(this->m_last_selected); + evt.StopPropagation(); + if (marker == LABEL_ITEM_WIZARD_PRINTERS) + show_add_menu(); + else + { + ConfigWizard::StartPage sp = ConfigWizard::SP_WELCOME; + switch (marker) { + case LABEL_ITEM_WIZARD_FILAMENTS: sp = ConfigWizard::SP_FILAMENTS; break; + case LABEL_ITEM_WIZARD_MATERIALS: sp = ConfigWizard::SP_MATERIALS; break; + default: break; + } + wxTheApp->CallAfter([sp]() { wxGetApp().run_wizard(ConfigWizard::RR_USER, sp); }); + } + } else if (marker == LABEL_ITEM_PHYSICAL_PRINTER || this->m_last_selected != selected_item || m_collection->current_is_dirty() ) { + this->m_last_selected = selected_item; + evt.SetInt(this->m_type); + evt.Skip(); + } else { + evt.StopPropagation(); + } + }); + + if (m_type == Preset::TYPE_FILAMENT) + { + Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent &event) { + const Preset* selected_preset = m_collection->find_preset(m_preset_bundle->filament_presets[m_extruder_idx]); + // Wide icons are shown if the currently selected preset is not compatible with the current printer, + // and red flag is drown in front of the selected preset. + bool wide_icons = selected_preset && !selected_preset->is_compatible; + float scale = m_em_unit*0.1f; + + int shifl_Left = wide_icons ? int(scale * 16 + 0.5) : 0; +#if defined(wxBITMAPCOMBOBOX_OWNERDRAWN_BASED) + shifl_Left += int(scale * 4 + 0.5f); // IMAGE_SPACING_RIGHT = 4 for wxBitmapComboBox -> Space left of image +#endif + int icon_right_pos = shifl_Left + int(scale * (24+4) + 0.5); + int mouse_pos = event.GetLogicalPosition(wxClientDC(this)).x; + if (mouse_pos < shifl_Left || mouse_pos > icon_right_pos ) { + // Let the combo box process the mouse click. + event.Skip(); + return; + } + + // Swallow the mouse click and open the color picker. + + // get current color + DynamicPrintConfig* cfg = wxGetApp().get_tab(Preset::TYPE_PRINTER)->get_config(); + auto colors = static_cast(cfg->option("extruder_colour")->clone()); + wxColour clr(colors->values[m_extruder_idx]); + if (!clr.IsOk()) + clr = wxColour(0,0,0); // Don't set alfa to transparence + + auto data = new wxColourData(); + data->SetChooseFull(1); + data->SetColour(clr); + + wxColourDialog dialog(this, data); + dialog.CenterOnParent(); + if (dialog.ShowModal() == wxID_OK) + { + colors->values[m_extruder_idx] = dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + + DynamicPrintConfig cfg_new = *cfg; + cfg_new.set_key_value("extruder_colour", colors); + + wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new); + this->update(); + wxGetApp().plater()->on_config_change(cfg_new); + } + }); + } + + edit_btn = new ScalableButton(parent, wxID_ANY, "cog"); + edit_btn->SetToolTip(_L("Click to edit preset")); + + edit_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent) + { + // In a case of a physical printer, for its editing open PhysicalPrinterDialog + if (m_type == Preset::TYPE_PRINTER/* && this->is_selected_physical_printer()*/) { + this->show_edit_menu(); + return; + } + + if (!switch_to_tab()) + return; + + /* In a case of a multi-material printing, for editing another Filament Preset + * it's needed to select this preset for the "Filament settings" Tab + */ + if (m_type == Preset::TYPE_FILAMENT && wxGetApp().extruders_edited_cnt() > 1) + { + const std::string& selected_preset = GetString(GetSelection()).ToUTF8().data(); + + // Call select_preset() only if there is new preset and not just modified + if ( !boost::algorithm::ends_with(selected_preset, Preset::suffix_modified()) ) + { + const std::string& preset_name = wxGetApp().preset_bundle->filaments.get_preset_name_by_alias(selected_preset); + wxGetApp().get_tab(m_type)->select_preset(preset_name); + } + } + }); +} + +PlaterPresetComboBox::~PlaterPresetComboBox() +{ + if (edit_btn) + edit_btn->Destroy(); +} + +bool PlaterPresetComboBox::switch_to_tab() +{ + Tab* tab = wxGetApp().get_tab(m_type); + if (!tab) + return false; + + int page_id = wxGetApp().tab_panel()->FindPage(tab); + if (page_id == wxNOT_FOUND) + return false; + + wxGetApp().tab_panel()->SetSelection(page_id); + // Switch to Settings NotePad + wxGetApp().mainframe->select_tab(); + return true; +} + +void PlaterPresetComboBox::show_add_menu() +{ + wxMenu* menu = new wxMenu(); + + append_menu_item(menu, wxID_ANY, _L("Add/Remove presets"), "", + [this](wxCommandEvent&) { + wxTheApp->CallAfter([]() { wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_PRINTERS); }); + }, "edit_uni", menu, []() { return true; }, wxGetApp().plater()); + + append_menu_item(menu, wxID_ANY, _L("Add physical printer"), "", + [this](wxCommandEvent&) { + PhysicalPrinterDialog dlg(wxEmptyString); + if (dlg.ShowModal() == wxID_OK) + update(); + }, "edit_uni", menu, []() { return true; }, wxGetApp().plater()); + + wxGetApp().plater()->PopupMenu(menu); +} + +void PlaterPresetComboBox::show_edit_menu() +{ + wxMenu* menu = new wxMenu(); + + append_menu_item(menu, wxID_ANY, _L("Edit preset"), "", + [this](wxCommandEvent&) { this->switch_to_tab(); }, "cog", menu, []() { return true; }, wxGetApp().plater()); + + if (this->is_selected_physical_printer()) { + append_menu_item(menu, wxID_ANY, _L("Edit physical printer"), "", + [this](wxCommandEvent&) { + PhysicalPrinterDialog dlg(this->GetString(this->GetSelection())); + if (dlg.ShowModal() == wxID_OK) + update(); + }, "cog", menu, []() { return true; }, wxGetApp().plater()); + + append_menu_item(menu, wxID_ANY, _L("Delete physical printer"), "", + [this](wxCommandEvent&) { + const std::string& printer_name = m_preset_bundle->physical_printers.get_selected_full_printer_name(); + if (printer_name.empty()) + return; + + const wxString msg = from_u8((boost::format(_u8L("Are you sure you want to delete \"%1%\" printer?")) % printer_name).str()); + if (wxMessageDialog(this, msg, _L("Delete Physical Printer"), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION).ShowModal() != wxID_YES) + return; + + m_preset_bundle->physical_printers.delete_selected_printer(); + + wxGetApp().get_tab(m_type)->update_preset_choice(); + update(); + }, "cross", menu, []() { return true; }, wxGetApp().plater()); + } + else + append_menu_item(menu, wxID_ANY, _L("Add/Remove presets"), "", + [this](wxCommandEvent&) { + wxTheApp->CallAfter([]() { wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_PRINTERS); }); + }, "edit_uni", menu, []() { return true; }, wxGetApp().plater()); + + append_menu_item(menu, wxID_ANY, _L("Add physical printer"), "", + [this](wxCommandEvent&) { + PhysicalPrinterDialog dlg(wxEmptyString); + if (dlg.ShowModal() == wxID_OK) + update(); + }, "edit_uni", menu, []() { return true; }, wxGetApp().plater()); + + wxGetApp().plater()->PopupMenu(menu); +} + +// Only the compatible presets are shown. +// If an incompatible preset is selected, it is shown as well. +void PlaterPresetComboBox::update() +{ + if (m_type == Preset::TYPE_FILAMENT && + (m_preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA || + m_preset_bundle->filament_presets.size() <= m_extruder_idx) ) + return; + + // Otherwise fill in the list from scratch. + this->Freeze(); + this->Clear(); + invalidate_selection(); + + const Preset* selected_filament_preset; + std::string extruder_color; + if (m_type == Preset::TYPE_FILAMENT) + { + unsigned char rgb[3]; + extruder_color = m_preset_bundle->printers.get_edited_preset().config.opt_string("extruder_colour", (unsigned int)m_extruder_idx); + if (!bitmap_cache().parse_color(extruder_color, rgb)) + // Extruder color is not defined. + extruder_color.clear(); + selected_filament_preset = m_collection->find_preset(m_preset_bundle->filament_presets[m_extruder_idx]); + assert(selected_filament_preset); + } + + bool has_selection = m_collection->get_selected_idx() != size_t(-1); + const Preset* selected_preset = m_type == Preset::TYPE_FILAMENT ? selected_filament_preset : has_selection ? &m_collection->get_selected_preset() : nullptr; + // Show wide icons if the currently selected preset is not compatible with the current printer, + // and draw a red flag in front of the selected preset. + bool wide_icons = selected_preset && !selected_preset->is_compatible; + + std::map nonsys_presets; + + wxString selected_user_preset = ""; + wxString tooltip = ""; + const std::deque& presets = m_collection->get_presets(); + + if (!presets.front().is_visible) + this->set_label_marker(this->Append(separator(L("System presets")), wxNullBitmap)); + + for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) + { + const Preset& preset = presets[i]; + bool is_selected = m_type == Preset::TYPE_FILAMENT ? + m_preset_bundle->filament_presets[m_extruder_idx] == preset.name : + // The case, when some physical printer is selected + m_type == Preset::TYPE_PRINTER && m_preset_bundle->physical_printers.has_selection() ? false : + i == m_collection->get_selected_idx(); + + if (!preset.is_visible || (!preset.is_compatible && !is_selected)) + continue; + + std::string bitmap_key, filament_rgb, extruder_rgb; + std::string bitmap_type_name = bitmap_key = m_type == Preset::TYPE_PRINTER && preset.printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; + + bool single_bar = false; + if (m_type == Preset::TYPE_FILAMENT) + { + // Assign an extruder color to the selected item if the extruder color is defined. + filament_rgb = preset.config.opt_string("filament_colour", 0); + extruder_rgb = (is_selected && !extruder_color.empty()) ? extruder_color : filament_rgb; + single_bar = filament_rgb == extruder_rgb; + + bitmap_key += single_bar ? filament_rgb : filament_rgb + extruder_rgb; + } + + wxBitmap* bmp = get_bmp(bitmap_key, wide_icons, bitmap_type_name, + preset.is_compatible, preset.is_system || preset.is_default, + single_bar, filament_rgb, extruder_rgb); + assert(bmp); + + const std::string name = preset.alias.empty() ? preset.name : preset.alias; + if (preset.is_default || preset.is_system) { + Append(wxString::FromUTF8((name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), *bmp); + validate_selection(is_selected); + if (is_selected) + tooltip = wxString::FromUTF8(preset.name.c_str()); + } + else + { + nonsys_presets.emplace(wxString::FromUTF8((name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), bmp); + if (is_selected) { + selected_user_preset = wxString::FromUTF8((name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()); + tooltip = wxString::FromUTF8(preset.name.c_str()); + } + } + if (i + 1 == m_collection->num_default_presets()) + set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + } + if (!nonsys_presets.empty()) + { + set_label_marker(Append(separator(L("User presets")), wxNullBitmap)); + for (std::map::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + Append(it->first, *it->second); + validate_selection(it->first == selected_user_preset); + } + } + + if (m_type == Preset::TYPE_PRINTER) + { + // add Physical printers, if any exists + if (!m_preset_bundle->physical_printers.empty()) { + set_label_marker(Append(separator(L("Physical printers")), wxNullBitmap)); + const PhysicalPrinterCollection& ph_printers = m_preset_bundle->physical_printers; + + for (PhysicalPrinterCollection::ConstIterator it = ph_printers.begin(); it != ph_printers.end(); ++it) { + for (const std::string preset_name : it->get_preset_names()) { + Preset* preset = m_collection->find_preset(preset_name); + if (!preset) + continue; + std::string main_icon_name, bitmap_key = main_icon_name = preset->printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; + wxBitmap* bmp = get_bmp(main_icon_name, wide_icons, main_icon_name); + assert(bmp); + + set_label_marker(Append(wxString::FromUTF8((it->get_full_name(preset_name) + (preset->is_dirty ? Preset::suffix_modified() : "")).c_str()), *bmp), LABEL_ITEM_PHYSICAL_PRINTER); + validate_selection(ph_printers.is_selected(it, preset_name)); + } + } + } + } + + if (m_type == Preset::TYPE_PRINTER || m_type == Preset::TYPE_SLA_MATERIAL) { + wxBitmap* bmp = get_bmp("edit_preset_list", wide_icons, "edit_uni"); + assert(bmp); + + if (m_type == Preset::TYPE_SLA_MATERIAL) + set_label_marker(Append(separator(L("Add/Remove materials")), *bmp), LABEL_ITEM_WIZARD_MATERIALS); + else + set_label_marker(Append(separator(L("Add/Remove printers")), *bmp), LABEL_ITEM_WIZARD_PRINTERS); + } + + update_selection(); + Thaw(); + + if (!tooltip.IsEmpty()) + SetToolTip(tooltip); + + // Update control min size after rescale (changed Display DPI under MSW) + if (GetMinWidth() != 20 * m_em_unit) + SetMinSize(wxSize(20 * m_em_unit, GetSize().GetHeight())); +} + +void PlaterPresetComboBox::msw_rescale() +{ + PresetComboBox::msw_rescale(); + edit_btn->msw_rescale(); +} + + +// --------------------------------- +// *** TabPresetComboBox *** +// --------------------------------- + +TabPresetComboBox::TabPresetComboBox(wxWindow* parent, Preset::Type preset_type) : + PresetComboBox(parent, preset_type, wxSize(35 * wxGetApp().em_unit(), -1)) +{ + Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { + // see https://github.com/prusa3d/PrusaSlicer/issues/3889 + // Under OSX: in case of use of a same names written in different case (like "ENDER" and "Ender") + // m_presets_choice->GetSelection() will return first item, because search in PopupListCtrl is case-insensitive. + // So, use GetSelection() from event parameter + auto selected_item = evt.GetSelection(); + + auto marker = reinterpret_cast(this->GetClientData(selected_item)); + if (marker >= LABEL_ITEM_DISABLED && marker < LABEL_ITEM_MAX) { + this->SetSelection(this->m_last_selected); + if (marker == LABEL_ITEM_WIZARD_PRINTERS) + wxTheApp->CallAfter([this]() { + wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_PRINTERS); + + // update combobox if its parent is a PhysicalPrinterDialog + PhysicalPrinterDialog* parent = dynamic_cast(this->GetParent()); + if (parent != nullptr) + update(); + }); + } + else if (on_selection_changed && (m_last_selected != selected_item || m_collection->current_is_dirty()) ) { + m_last_selected = selected_item; + on_selection_changed(selected_item); + } + + evt.StopPropagation(); + }); +} + +// Update the choice UI from the list of presets. +// If show_incompatible, all presets are shown, otherwise only the compatible presets are shown. +// If an incompatible preset is selected, it is shown as well. +void TabPresetComboBox::update() +{ + Freeze(); + Clear(); + invalidate_selection(); + + const std::deque& presets = m_collection->get_presets(); + + std::map> nonsys_presets; + wxString selected = ""; + if (!presets.front().is_visible) + set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + int idx_selected = m_collection->get_selected_idx(); + + PrinterTechnology proper_pt = ptAny; + if (m_type == Preset::TYPE_PRINTER && m_preset_bundle->physical_printers.has_selection()) { + std::string sel_preset_name = m_preset_bundle->physical_printers.get_selected_printer_preset_name(); + Preset* preset = m_collection->find_preset(sel_preset_name); + if (preset) + proper_pt = preset->printer_technology(); + else + m_preset_bundle->physical_printers.unselect_printer(); + } + + for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) + { + const Preset& preset = presets[i]; + if (!preset.is_visible || (!show_incompatible && !preset.is_compatible && i != idx_selected)) + continue; + + // marker used for disable incompatible printer models for the selected physical printer + bool is_enabled = true; + + std::string bitmap_key = "tab"; + if (m_type == Preset::TYPE_PRINTER) { + bitmap_key += "_printer"; + if (preset.printer_technology() == ptSLA) + bitmap_key += "_sla"; + } + std::string main_icon_name = m_type == Preset::TYPE_PRINTER && preset.printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; + + wxBitmap* bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default); + assert(bmp); + + if (preset.is_default || preset.is_system) { + int item_id = Append(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), *bmp); + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(i == idx_selected); + } + else + { + std::pair pair(bmp, is_enabled); + nonsys_presets.emplace(wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()), std::pair(bmp, is_enabled)); + if (i == idx_selected) + selected = wxString::FromUTF8((preset.name + (preset.is_dirty ? Preset::suffix_modified() : "")).c_str()); + } + if (i + 1 == m_collection->num_default_presets()) + set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + } + if (!nonsys_presets.empty()) + { + set_label_marker(Append(separator(L("User presets")), wxNullBitmap)); + for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + int item_id = Append(it->first, *it->second.first); + bool is_enabled = it->second.second; + if (!is_enabled) + set_label_marker(item_id, LABEL_ITEM_DISABLED); + validate_selection(it->first == selected); + } + } + + if (m_type == Preset::TYPE_PRINTER) + { + // add Physical printers, if any exists + if (!m_preset_bundle->physical_printers.empty()) { + set_label_marker(Append(separator(L("Physical printers")), wxNullBitmap)); + const PhysicalPrinterCollection& ph_printers = m_preset_bundle->physical_printers; + + for (PhysicalPrinterCollection::ConstIterator it = ph_printers.begin(); it != ph_printers.end(); ++it) { + for (const std::string preset_name : it->get_preset_names()) { + Preset* preset = m_collection->find_preset(preset_name); + if (!preset) + continue; + std::string main_icon_name = preset->printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; + + wxBitmap* bmp = get_bmp(main_icon_name, main_icon_name, "", true, true, false); + assert(bmp); + + set_label_marker(Append(wxString::FromUTF8((it->get_full_name(preset_name) + (preset->is_dirty ? Preset::suffix_modified() : "")).c_str()), *bmp), LABEL_ITEM_PHYSICAL_PRINTER); + validate_selection(ph_printers.is_selected(it, preset_name)); + } + } + } + + // add "Add/Remove printers" item + std::string icon_name = "edit_uni"; + wxBitmap* bmp = get_bmp("edit_preset_list, tab,", icon_name, ""); + assert(bmp); + + set_label_marker(Append(separator(L("Add/Remove printers")), *bmp), LABEL_ITEM_WIZARD_PRINTERS); + } + + update_selection(); + Thaw(); +} + +void TabPresetComboBox::msw_rescale() +{ + PresetComboBox::msw_rescale(); + wxSize sz = wxSize(35 * m_em_unit, -1); + SetMinSize(sz); + SetSize(sz); +} + +void TabPresetComboBox::update_dirty() +{ + // 1) Update the dirty flag of the current preset. + m_collection->update_dirty(); + + // 2) Update the labels. + wxWindowUpdateLocker noUpdates(this); + for (unsigned int ui_id = 0; ui_id < GetCount(); ++ui_id) { + auto marker = reinterpret_cast(this->GetClientData(ui_id)); + if (marker >= LABEL_ITEM_MARKER) + continue; + + std::string old_label = GetString(ui_id).utf8_str().data(); + std::string preset_name = Preset::remove_suffix_modified(old_label); + std::string ph_printer_name; + + if (marker == LABEL_ITEM_PHYSICAL_PRINTER) { + ph_printer_name = PhysicalPrinter::get_short_name(preset_name); + preset_name = PhysicalPrinter::get_preset_name(preset_name); + } + + const Preset* preset = m_collection->find_preset(preset_name, false); + if (preset) { + std::string new_label = preset->is_dirty ? preset->name + Preset::suffix_modified() : preset->name; + + if (marker == LABEL_ITEM_PHYSICAL_PRINTER) + new_label = ph_printer_name + PhysicalPrinter::separator() + new_label; + + if (old_label != new_label) + SetString(ui_id, wxString::FromUTF8(new_label.c_str())); + } + } +#ifdef __APPLE__ + // wxWidgets on OSX do not upload the text of the combo box line automatically. + // Force it to update by re-selecting. + SetSelection(GetSelection()); +#endif /* __APPLE __ */ +} + + +//----------------------------------------------- +// SavePresetDialog::Item +//----------------------------------------------- + +SavePresetDialog::Item::Item(Preset::Type type, const std::string& suffix, wxBoxSizer* sizer, SavePresetDialog* parent): + m_type(type), + m_parent(parent) +{ + Tab* tab = wxGetApp().get_tab(m_type); + assert(tab); + m_presets = tab->get_presets(); + + const Preset& sel_preset = m_presets->get_selected_preset(); + std::string preset_name = sel_preset.is_default ? "Untitled" : + sel_preset.is_system ? (boost::format(("%1% - %2%")) % sel_preset.name % suffix).str() : + sel_preset.name; + + // if name contains extension + if (boost::iends_with(preset_name, ".ini")) { + size_t len = preset_name.length() - 4; + preset_name.resize(len); + } + + std::vector values; + for (const Preset& preset : *m_presets) { + if (preset.is_default || preset.is_system || preset.is_external) + continue; + values.push_back(preset.name); + } + + wxStaticText* label_top = new wxStaticText(m_parent, wxID_ANY, from_u8((boost::format(_utf8(L("Save %s as:"))) % into_u8(tab->title())).str())); + + m_valid_bmp = new wxStaticBitmap(m_parent, wxID_ANY, create_scaled_bitmap("tick_mark", m_parent)); + + m_combo = new wxComboBox(m_parent, wxID_ANY, from_u8(preset_name), wxDefaultPosition, wxSize(35 * wxGetApp().em_unit(), -1)); + for (const std::string& value : values) + m_combo->Append(from_u8(value)); + + m_combo->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update(); }); +#ifdef __WXOSX__ + // Under OSX wxEVT_TEXT wasn't invoked after change selection in combobox, + // So process wxEVT_COMBOBOX too + m_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { update(); }); +#endif //__WXOSX__ + + m_valid_label = new wxStaticText(m_parent, wxID_ANY, ""); + m_valid_label->SetFont(wxGetApp().bold_font()); + + wxBoxSizer* combo_sizer = new wxBoxSizer(wxHORIZONTAL); + combo_sizer->Add(m_valid_bmp, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, BORDER_W); + combo_sizer->Add(m_combo, 1, wxEXPAND, BORDER_W); + + sizer->Add(label_top, 0, wxEXPAND | wxTOP| wxBOTTOM, BORDER_W); + sizer->Add(combo_sizer, 0, wxEXPAND | wxBOTTOM, BORDER_W); + sizer->Add(m_valid_label, 0, wxEXPAND | wxLEFT, 3*BORDER_W); + + if (m_type == Preset::TYPE_PRINTER) + m_parent->add_info_for_edit_ph_printer(sizer); + + update(); +} + +void SavePresetDialog::Item::update() +{ + m_preset_name = into_u8(m_combo->GetValue()); + + m_valid_type = Valid; + wxString info_line; + + const char* unusable_symbols = "<>[]:/\\|?*\""; + + const std::string unusable_suffix = PresetCollection::get_suffix_modified();//"(modified)"; + for (size_t i = 0; i < std::strlen(unusable_symbols); i++) { + if (m_preset_name.find_first_of(unusable_symbols[i]) != std::string::npos) { + info_line = _L("The supplied name is not valid;") + "\n" + + _L("the following characters are not allowed:") + " " + unusable_symbols; + m_valid_type = NoValid; + break; + } + } + + if (m_valid_type == Valid && m_preset_name.find(unusable_suffix) != std::string::npos) { + info_line = _L("The supplied name is not valid;") + "\n" + + _L("the following suffix is not allowed:") + "\n\t" + + from_u8(PresetCollection::get_suffix_modified()); + m_valid_type = NoValid; + } + + if (m_valid_type == Valid && m_preset_name == "- default -") { + info_line = _L("The supplied name is not available."); + m_valid_type = NoValid; + } + + const Preset* existing = m_presets->find_preset(m_preset_name, false); + if (m_valid_type == Valid && existing && (existing->is_default || existing->is_system)) { + info_line = _L("Cannot overwrite a system profile."); + m_valid_type = NoValid; + } + + if (m_valid_type == Valid && existing && (existing->is_external)) { + info_line = _L("Cannot overwrite an external profile."); + m_valid_type = NoValid; + } + + if (m_valid_type == Valid && existing && m_preset_name != m_presets->get_selected_preset_name()) + { + info_line = from_u8((boost::format(_u8L("Preset with name \"%1%\" already exists.")) % m_preset_name).str()); + if (!existing->is_compatible) + info_line += "\n" + _L("And selected preset is imcopatible with selected printer."); + info_line += "\n" + _L("Note: This preset will be replaced after saving"); + m_valid_type = Warning; + } + + if (m_valid_type == Valid && m_preset_name.empty()) { + info_line = _L("The empty name is not available."); + m_valid_type = NoValid; + } + + m_valid_label->SetLabel(info_line); + m_valid_label->Show(!info_line.IsEmpty()); + + update_valid_bmp(); + + if (m_type == Preset::TYPE_PRINTER) + m_parent->update_info_for_edit_ph_printer(m_preset_name); + + m_parent->layout(); +} + +void SavePresetDialog::Item::update_valid_bmp() +{ + std::string bmp_name = m_valid_type == Warning ? "exclamation" : + m_valid_type == NoValid ? "cross" : "tick_mark" ; + m_valid_bmp->SetBitmap(create_scaled_bitmap(bmp_name, m_parent)); +} + +void SavePresetDialog::Item::accept() +{ + if (m_valid_type == Warning) + m_presets->delete_preset(m_preset_name); +} + + +//----------------------------------------------- +// SavePresetDialog +//----------------------------------------------- + +SavePresetDialog::SavePresetDialog(Preset::Type type, std::string suffix) + : DPIDialog(nullptr, wxID_ANY, _L("Save preset"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER) +{ + build(std::vector{type}, suffix); +} + +SavePresetDialog::SavePresetDialog(std::vector types, std::string suffix) + : DPIDialog(nullptr, wxID_ANY, _L("Save preset"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER) +{ + build(types, suffix); +} + +SavePresetDialog::~SavePresetDialog() +{ + for (auto item : m_items) { + delete item; + } +} + +void SavePresetDialog::build(std::vector types, std::string suffix) +{ + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#if ENABLE_WX_3_1_3_DPI_CHANGED_EVENT && defined(__WXMSW__) + // ys_FIXME! temporary workaround for correct font scaling + // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, + // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT + this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); +#endif // ENABLE_WX_3_1_3_DPI_CHANGED_EVENT + + if (suffix.empty()) + suffix = _CTX_utf8(L_CONTEXT("Copy", "PresetName"), "PresetName"); + + wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); + + m_presets_sizer = new wxBoxSizer(wxVERTICAL); + + // Add first item + for (Preset::Type type : types) + AddItem(type, suffix); + + // Add dialog's buttons + wxStdDialogButtonSizer* btns = this->CreateStdDialogButtonSizer(wxOK | wxCANCEL); + wxButton* btnOK = static_cast(this->FindWindowById(wxID_OK, this)); + btnOK->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { accept(); }); + btnOK->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(enable_ok_btn()); }); + + topSizer->Add(m_presets_sizer, 0, wxEXPAND | wxALL, BORDER_W); + topSizer->Add(btns, 0, wxEXPAND | wxALL, BORDER_W); + + SetSizer(topSizer); + topSizer->SetSizeHints(this); +} + +void SavePresetDialog::AddItem(Preset::Type type, const std::string& suffix) +{ + m_items.emplace_back(new Item{type, suffix, m_presets_sizer, this}); +} + +std::string SavePresetDialog::get_name() +{ + return m_items.front()->preset_name(); +} + +std::string SavePresetDialog::get_name(Preset::Type type) +{ + for (const Item* item : m_items) + if (item->type() == type) + return item->preset_name(); + return ""; +} + +bool SavePresetDialog::enable_ok_btn() const +{ + for (const Item* item : m_items) + if (!item->is_valid()) + return false; + + return true; +} + +void SavePresetDialog::add_info_for_edit_ph_printer(wxBoxSizer* sizer) +{ + PhysicalPrinterCollection& printers = wxGetApp().preset_bundle->physical_printers; + m_ph_printer_name = printers.get_selected_printer_name(); + m_old_preset_name = printers.get_selected_printer_preset_name(); + + wxString msg_text = from_u8((boost::format(_u8L("You have selected physical printer \"%1%\" \n" + "with related printer preset \"%2%\"")) % + m_ph_printer_name % m_old_preset_name).str()); + m_label = new wxStaticText(this, wxID_ANY, msg_text); + m_label->SetFont(wxGetApp().bold_font()); + + wxString choices[] = {"","",""}; + + m_action_radio_box = new wxRadioBox(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, + WXSIZEOF(choices), choices, 3, wxRA_SPECIFY_ROWS); + m_action_radio_box->SetSelection(0); + m_action_radio_box->Bind(wxEVT_RADIOBOX, [this](wxCommandEvent& e) { + m_action = (ActionType)e.GetSelection(); }); + m_action = ChangePreset; + + m_radio_sizer = new wxBoxSizer(wxHORIZONTAL); + m_radio_sizer->Add(m_action_radio_box, 1, wxEXPAND | wxTOP, 2*BORDER_W); + + sizer->Add(m_label, 0, wxEXPAND | wxLEFT | wxTOP, 3*BORDER_W); + sizer->Add(m_radio_sizer, 1, wxEXPAND | wxLEFT, 3*BORDER_W); +} + +void SavePresetDialog::update_info_for_edit_ph_printer(const std::string& preset_name) +{ + bool show = wxGetApp().preset_bundle->physical_printers.has_selection() && m_old_preset_name != preset_name; + + m_label->Show(show); + m_radio_sizer->ShowItems(show); + if (!show) { + this->SetMinSize(wxSize(100,50)); + return; + } + + wxString msg_text = from_u8((boost::format(_u8L("What would you like to do with \"%1%\" preset after saving?")) % preset_name).str()); + m_action_radio_box->SetLabel(msg_text); + + wxString choices[] = { from_u8((boost::format(_u8L("Change \"%1%\" to \"%2%\" for this physical printer \"%3%\"")) % m_old_preset_name % preset_name % m_ph_printer_name).str()), + from_u8((boost::format(_u8L("Add \"%1%\" as a next preset for the the physical printer \"%2%\"")) % preset_name % m_ph_printer_name).str()), + from_u8((boost::format(_u8L("Just switch to \"%1%\" preset")) % preset_name).str()) }; + + int n = 0; + for(const wxString& label: choices) + m_action_radio_box->SetString(n++, label); +} + +void SavePresetDialog::layout() +{ + this->Layout(); + this->Fit(); +} + +void SavePresetDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + const int& em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_OK, wxID_CANCEL }); + + for (Item* item : m_items) + item->update_valid_bmp(); + + //const wxSize& size = wxSize(45 * em, 35 * em); + SetMinSize(/*size*/wxSize(100, 50)); + + Fit(); + Refresh(); +} + +void SavePresetDialog::update_physical_printers(const std::string& preset_name) +{ + if (m_action == UndefAction) + return; + + PhysicalPrinterCollection& physical_printers = wxGetApp().preset_bundle->physical_printers; + if (!physical_printers.has_selection()) + return; + + std::string printer_preset_name = physical_printers.get_selected_printer_preset_name(); + + if (m_action == Switch) + // unselect physical printer, if it was selected + physical_printers.unselect_printer(); + else + { + PhysicalPrinter printer = physical_printers.get_selected_printer(); + + if (m_action == ChangePreset) + printer.delete_preset(printer_preset_name); + + if (printer.add_preset(preset_name)) + physical_printers.save_printer(printer); + + physical_printers.select_printer(printer.get_full_name(preset_name)); + } +} + +void SavePresetDialog::accept() +{ + for (Item* item : m_items) { + item->accept(); + if (item->type() == Preset::TYPE_PRINTER) + update_physical_printers(item->preset_name()); + } + + EndModal(wxID_OK); +} + + + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp new file mode 100644 index 0000000000..e33a2d753d --- /dev/null +++ b/src/slic3r/GUI/PresetComboBoxes.hpp @@ -0,0 +1,278 @@ +#ifndef slic3r_PresetComboBoxes_hpp_ +#define slic3r_PresetComboBoxes_hpp_ + +#include +#include + +#include "libslic3r/Preset.hpp" +#include "wxExtensions.hpp" +#include "GUI_Utils.hpp" + +class wxString; +class wxTextCtrl; +class wxStaticText; +class ScalableButton; +class wxBoxSizer; +class wxComboBox; +class wxStaticBitmap; +class wxRadioBox; + +namespace Slic3r { + +namespace GUI { + +class BitmapCache; + +// --------------------------------- +// *** PresetComboBox *** +// --------------------------------- + +// BitmapComboBox used to presets list on Sidebar and Tabs +class PresetComboBox : public wxBitmapComboBox +{ +public: + PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size = wxDefaultSize); + ~PresetComboBox(); + + enum LabelItemType { + LABEL_ITEM_PHYSICAL_PRINTER = 0xffffff01, + LABEL_ITEM_DISABLED, + LABEL_ITEM_MARKER, + LABEL_ITEM_PHYSICAL_PRINTERS, + LABEL_ITEM_WIZARD_PRINTERS, + LABEL_ITEM_WIZARD_FILAMENTS, + LABEL_ITEM_WIZARD_MATERIALS, + + LABEL_ITEM_MAX, + }; + + void set_label_marker(int item, LabelItemType label_item_type = LABEL_ITEM_MARKER); + bool set_printer_technology(PrinterTechnology pt); + + void set_selection_changed_function(std::function sel_changed) { on_selection_changed = sel_changed; } + + bool is_selected_physical_printer(); + + // Return true, if physical printer was selected + // and next internal selection was accomplished + bool selection_is_changed_according_to_physical_printers(); + + void update(std::string select_preset); + + virtual void update(); + virtual void msw_rescale(); + +protected: + typedef std::size_t Marker; + std::function on_selection_changed { nullptr }; + + Preset::Type m_type; + std::string m_main_bitmap_name; + + PresetBundle* m_preset_bundle {nullptr}; + PresetCollection* m_collection {nullptr}; + + // Caching bitmaps for the all bitmaps, used in preset comboboxes + static BitmapCache& bitmap_cache(); + + // Indicator, that the preset is compatible with the selected printer. + ScalableBitmap m_bitmapCompatible; + // Indicator, that the preset is NOT compatible with the selected printer. + ScalableBitmap m_bitmapIncompatible; + + int m_last_selected; + int m_em_unit; + + // parameters for an icon's drawing + int icon_height; + int norm_icon_width; + int thin_icon_width; + int wide_icon_width; + int space_icon_width; + int thin_space_icon_width; + int wide_space_icon_width; + + PrinterTechnology printer_technology {ptAny}; + + void invalidate_selection(); + void validate_selection(bool predicate = false); + void update_selection(); + +#ifdef __linux__ + static const char* separator_head() { return "------- "; } + static const char* separator_tail() { return " -------"; } +#else // __linux__ + static const char* separator_head() { return "————— "; } + static const char* separator_tail() { return " —————"; } +#endif // __linux__ + static wxString separator(const std::string& label); + + wxBitmap* get_bmp( std::string bitmap_key, bool wide_icons, const std::string& main_icon_name, + bool is_compatible = true, bool is_system = false, bool is_single_bar = false, + std::string filament_rgb = "", std::string extruder_rgb = ""); + + wxBitmap* get_bmp( std::string bitmap_key, const std::string& main_icon_name, const std::string& next_icon_name, + bool is_enabled = true, bool is_compatible = true, bool is_system = false); + +#ifdef __APPLE__ + /* For PresetComboBox we use bitmaps that are created from images that are already scaled appropriately for Retina + * (Contrary to the intuition, the `scale` argument for Bitmap's constructor doesn't mean + * "please scale this to such and such" but rather + * "the wxImage is already sized for backing scale such and such". ) + * Unfortunately, the constructor changes the size of wxBitmap too. + * Thus We need to use unscaled size value for bitmaps that we use + * to avoid scaled size of control items. + * For this purpose control drawing methods and + * control size calculation methods (virtual) are overridden. + **/ + virtual bool OnAddBitmap(const wxBitmap& bitmap) override; + virtual void OnDrawItem(wxDC& dc, const wxRect& rect, int item, int flags) const override; +#endif + +private: + void fill_width_height(); +}; + + +// --------------------------------- +// *** PlaterPresetComboBox *** +// --------------------------------- + +class PlaterPresetComboBox : public PresetComboBox +{ +public: + PlaterPresetComboBox(wxWindow *parent, Preset::Type preset_type); + ~PlaterPresetComboBox(); + + ScalableButton* edit_btn { nullptr }; + + void set_extruder_idx(const int extr_idx) { m_extruder_idx = extr_idx; } + int get_extruder_idx() const { return m_extruder_idx; } + + bool switch_to_tab(); + void show_add_menu(); + void show_edit_menu(); + + void update() override; + void msw_rescale() override; + +private: + int m_extruder_idx = -1; +}; + + +// --------------------------------- +// *** TabPresetComboBox *** +// --------------------------------- + +class TabPresetComboBox : public PresetComboBox +{ + bool show_incompatible {false}; + bool m_enable_all {false}; + +public: + TabPresetComboBox(wxWindow *parent, Preset::Type preset_type); + ~TabPresetComboBox() {} + void set_show_incompatible_presets(bool show_incompatible_presets) { + show_incompatible = show_incompatible_presets; + } + + void update() override; + void update_dirty(); + void msw_rescale() override; + + void set_enable_all(bool enable=true) { m_enable_all = enable; } + + PresetCollection* presets() const { return m_collection; } + Preset::Type type() const { return m_type; } +}; + + +//------------------------------------------------ +// SavePresetDialog +//------------------------------------------------ + +class SavePresetDialog : public DPIDialog +{ + enum ActionType + { + ChangePreset, + AddPreset, + Switch, + UndefAction + }; + + struct Item + { + enum ValidationType + { + Valid, + NoValid, + Warning + }; + + Item(Preset::Type type, const std::string& suffix, wxBoxSizer* sizer, SavePresetDialog* parent); + + void update_valid_bmp(); + void accept(); + + bool is_valid() const { return m_valid_type != NoValid; } + Preset::Type type() const { return m_type; } + std::string preset_name() const { return m_preset_name; } + + private: + Preset::Type m_type; + ValidationType m_valid_type; + std::string m_preset_name; + + SavePresetDialog* m_parent {nullptr}; + wxStaticBitmap* m_valid_bmp {nullptr}; + wxComboBox* m_combo {nullptr}; + wxStaticText* m_valid_label {nullptr}; + + PresetCollection* m_presets {nullptr}; + + void update(); + }; + + std::vector m_items; + + wxBoxSizer* m_presets_sizer {nullptr}; + wxStaticText* m_label {nullptr}; + wxRadioBox* m_action_radio_box {nullptr}; + wxBoxSizer* m_radio_sizer {nullptr}; + ActionType m_action {UndefAction}; + + std::string m_ph_printer_name; + std::string m_old_preset_name; + +public: + + SavePresetDialog(Preset::Type type, std::string suffix = ""); + SavePresetDialog(std::vector types, std::string suffix = ""); + ~SavePresetDialog(); + + void AddItem(Preset::Type type, const std::string& suffix); + + std::string get_name(); + std::string get_name(Preset::Type type); + + bool enable_ok_btn() const; + void add_info_for_edit_ph_printer(wxBoxSizer *sizer); + void update_info_for_edit_ph_printer(const std::string &preset_name); + void layout(); + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + void on_sys_color_changed() override {} + +private: + void build(std::vector types, std::string suffix = ""); + void update_physical_printers(const std::string& preset_name); + void accept(); +}; + +} // namespace GUI +} // namespace Slic3r + +#endif diff --git a/src/slic3r/GUI/PresetHints.cpp b/src/slic3r/GUI/PresetHints.cpp index 24afeb526e..c40c4c6acb 100644 --- a/src/slic3r/GUI/PresetHints.cpp +++ b/src/slic3r/GUI/PresetHints.cpp @@ -4,7 +4,6 @@ #include "libslic3r/Slicing.hpp" #include "libslic3r/libslic3r.h" -#include "PresetBundle.hpp" #include "PresetHints.hpp" #include diff --git a/src/slic3r/GUI/PresetHints.hpp b/src/slic3r/GUI/PresetHints.hpp index be049c2c87..a61310f408 100644 --- a/src/slic3r/GUI/PresetHints.hpp +++ b/src/slic3r/GUI/PresetHints.hpp @@ -3,7 +3,7 @@ #include -#include "PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" namespace Slic3r { diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index bae60e47f9..216af5df49 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -15,11 +15,11 @@ #include "GUI.hpp" #include "GUI_App.hpp" -#include "AppConfig.hpp" #include "MsgDialog.hpp" #include "I18N.hpp" #include "../Utils/PrintHost.hpp" #include "wxExtensions.hpp" +#include "libslic3r/AppConfig.hpp" namespace fs = boost::filesystem; diff --git a/src/slic3r/GUI/RemovableDriveManager.cpp b/src/slic3r/GUI/RemovableDriveManager.cpp index d67ac4a22f..d865fe3476 100644 --- a/src/slic3r/GUI/RemovableDriveManager.cpp +++ b/src/slic3r/GUI/RemovableDriveManager.cpp @@ -393,6 +393,7 @@ bool RemovableDriveManager::set_and_verify_last_save_path(const std::string &pat #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS m_last_save_path = this->get_removable_drive_from_path(path); + m_exporting_finished = false; return ! m_last_save_path.empty(); } @@ -407,6 +408,7 @@ RemovableDriveManager::RemovableDrivesStatus RemovableDriveManager::status() } if (! out.has_eject) m_last_save_path.clear(); + out.has_eject = out.has_eject && m_exporting_finished; return out; } diff --git a/src/slic3r/GUI/RemovableDriveManager.hpp b/src/slic3r/GUI/RemovableDriveManager.hpp index e1a8d6faf1..26ee12e40c 100644 --- a/src/slic3r/GUI/RemovableDriveManager.hpp +++ b/src/slic3r/GUI/RemovableDriveManager.hpp @@ -83,7 +83,7 @@ public: // Public to be accessible from RemovableDriveManagerMM::on_device_unmount OSX notification handler. // It would be better to make this method private and friend to RemovableDriveManagerMM, but RemovableDriveManagerMM is an ObjectiveC class. void update(); - + void set_exporting_finished(bool b) { m_exporting_finished = b; } #ifdef _WIN32 // Called by Win32 Volume arrived / detached callback. void volumes_changed(); @@ -121,7 +121,9 @@ private: std::vector::const_iterator find_last_save_path_drive_data() const; // Set with set_and_verify_last_save_path() to a removable drive path to be ejected. std::string m_last_save_path; - + // Verifies that exporting was finished so drive can be ejected. + // Set false by set_and_verify_last_save_path() that is called just before exporting. + bool m_exporting_finished; #if __APPLE__ void register_window_osx(); void unregister_window_osx(); diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 012af342a8..da9c8fe25d 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -9,10 +9,10 @@ #include "wx/dataview.h" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/PresetBundle.hpp" #include "GUI_App.hpp" #include "Plater.hpp" #include "Tab.hpp" -#include "PresetBundle.hpp" #define FTS_FUZZY_MATCH_IMPLEMENTATION #include "fts_fuzzy_match.h" @@ -28,12 +28,6 @@ using GUI::into_u8; namespace Search { -// Does our wxWidgets version support markup? -// https://github.com/prusa3d/PrusaSlicer/issues/4282#issuecomment-634676371 -#if wxUSE_MARKUP && wxCHECK_VERSION(3, 1, 1) - #define SEARCH_SUPPORTS_MARKUP -#endif - static char marker_by_type(Preset::Type type, PrinterTechnology pt) { switch(type) { @@ -264,7 +258,7 @@ bool OptionsSearcher::search(const std::string& search, bool force/* = false*/) std::string label_u8 = into_u8(label); std::string label_plain = label_u8; -#ifdef SEARCH_SUPPORTS_MARKUP +#ifdef SUPPORTS_MARKUP boost::replace_all(label_plain, std::string(1, char(ImGui::ColorMarkerStart)), ""); boost::replace_all(label_plain, std::string(1, char(ImGui::ColorMarkerEnd)), ""); #else @@ -327,6 +321,14 @@ const Option& OptionsSearcher::get_option(size_t pos_in_filter) const return options[found[pos_in_filter].option_idx]; } +const Option& OptionsSearcher::get_option(const std::string& opt_key) const +{ + auto it = std::lower_bound(options.begin(), options.end(), Option({ boost::nowide::widen(opt_key) })); + assert(it != options.end()); + + return options[it - options.begin()]; +} + void OptionsSearcher::add_key(const std::string& opt_key, const wxString& group, const wxString& category) { groups_and_categories[opt_key] = GroupAndCategory{group, category}; @@ -442,7 +444,7 @@ SearchDialog::SearchDialog(OptionsSearcher* searcher) wxDataViewTextRenderer* const markupRenderer = new wxDataViewTextRenderer(); -#ifdef SEARCH_SUPPORTS_MARKUP +#ifdef SUPPORTS_MARKUP markupRenderer->EnableMarkup(); #endif diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index 9701e68088..a57e0d015d 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -14,8 +14,8 @@ #include #include "GUI_Utils.hpp" -#include "Preset.hpp" #include "wxExtensions.hpp" +#include "libslic3r/Preset.hpp" namespace Slic3r { @@ -37,8 +37,8 @@ struct GroupAndCategory { }; struct Option { - bool operator<(const Option& other) const { return other.label > this->label; } - bool operator>(const Option& other) const { return other.label < this->label; } +// bool operator<(const Option& other) const { return other.label > this->label; } + bool operator<(const Option& other) const { return other.opt_key > this->opt_key; } // Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters, // though for some languages (Chinese?) it may not work correctly. @@ -116,12 +116,18 @@ public: const FoundOption& operator[](const size_t pos) const noexcept { return found[pos]; } const Option& get_option(size_t pos_in_filter) const; + const Option& get_option(const std::string& opt_key) const; const std::vector& found_options() { return found; } const GroupAndCategory& get_group_and_category (const std::string& opt_key) { return groups_and_categories[opt_key]; } std::string& search_string() { return search_line; } void set_printer_technology(PrinterTechnology pt) { printer_technology = pt; } + + void sort_options_by_opt_key() { + std::sort(options.begin(), options.end(), [](const Option& o1, const Option& o2) { + return o1.opt_key < o2.opt_key; }); + } }; diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 69550748d8..bef0fd5d75 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -16,8 +16,11 @@ #include #include +#if ENABLE_GCODE_VIEWER +#include +#endif // ENABLE_GCODE_VIEWER -static const float UNIFORM_SCALE_COLOR[3] = { 1.0f, 0.38f, 0.0f }; +static const float UNIFORM_SCALE_COLOR[4] = { 0.923f, 0.504f, 0.264f, 1.0f }; namespace Slic3r { namespace GUI { @@ -110,8 +113,10 @@ Selection::Selection() , m_valid(false) , m_scale_factor(1.0f) { +#if !ENABLE_GCODE_VIEWER m_arrow.reset(new GLArrow); m_curved_arrow.reset(new GLCurvedArrow(16)); +#endif // !ENABLE_GCODE_VIEWER this->set_bounding_boxes_dirty(); #if ENABLE_RENDER_SELECTION_CENTER @@ -138,6 +143,10 @@ void Selection::set_volumes(GLVolumePtrs* volumes) // Init shall be called from the OpenGL render function, so that the OpenGL context is initialized! bool Selection::init() { +#if ENABLE_GCODE_VIEWER + m_arrow.init_from(straight_arrow(10.0f, 5.0f, 5.0f, 10.0f, 1.0f)); + m_curved_arrow.init_from(circular_arrow(16, 10.0f, 5.0f, 10.0f, 5.0f, 1.0f)); +#else if (!m_arrow->init()) return false; @@ -147,6 +156,7 @@ bool Selection::init() return false; m_curved_arrow->set_scale(5.0 * Vec3d::Ones()); +#endif //ENABLE_GCODE_VIEWER return true; } @@ -1261,40 +1271,40 @@ void Selection::render_center(bool gizmo_is_dragging) const } #endif // ENABLE_RENDER_SELECTION_CENTER -void Selection::render_sidebar_hints(const std::string& sidebar_field, const Shader& shader) const +void Selection::render_sidebar_hints(const std::string& sidebar_field) const { if (sidebar_field.empty()) return; + GLShaderProgram* shader = nullptr; + if (!boost::starts_with(sidebar_field, "layer")) { - shader.start_using(); + shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + shader->start_using(); glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - glsafe(::glEnable(GL_LIGHTING)); } glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glPushMatrix()); - if (!boost::starts_with(sidebar_field, "layer")) - { + if (!boost::starts_with(sidebar_field, "layer")) { const Vec3d& center = get_bounding_box().center(); - if (is_single_full_instance() && !wxGetApp().obj_manipul()->get_world_coordinates()) - { + if (is_single_full_instance() && !wxGetApp().obj_manipul()->get_world_coordinates()) { glsafe(::glTranslated(center(0), center(1), center(2))); - if (!boost::starts_with(sidebar_field, "position")) - { + if (!boost::starts_with(sidebar_field, "position")) { Transform3d orient_matrix = Transform3d::Identity(); if (boost::starts_with(sidebar_field, "scale")) orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - else if (boost::starts_with(sidebar_field, "rotation")) - { + 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); - else if (boost::ends_with(sidebar_field, "y")) - { + else if (boost::ends_with(sidebar_field, "y")) { const Vec3d& rotation = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation(); if (rotation(0) == 0.0) orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); @@ -1305,21 +1315,16 @@ void Selection::render_sidebar_hints(const std::string& sidebar_field, const Sha glsafe(::glMultMatrixd(orient_matrix.data())); } - } - else if (is_single_volume() || is_single_modifier()) - { + } else if (is_single_volume() || is_single_modifier()) { glsafe(::glTranslated(center(0), center(1), center(2))); Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); 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); glsafe(::glMultMatrixd(orient_matrix.data())); - } - else - { + } else { glsafe(::glTranslated(center(0), center(1), center(2))); - if (requires_local_axes()) - { + if (requires_local_axes()) { Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); glsafe(::glMultMatrixd(orient_matrix.data())); } @@ -1330,20 +1335,15 @@ void Selection::render_sidebar_hints(const std::string& sidebar_field, const Sha render_sidebar_position_hints(sidebar_field); else if (boost::starts_with(sidebar_field, "rotation")) render_sidebar_rotation_hints(sidebar_field); - else if (boost::starts_with(sidebar_field, "scale")) + else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) render_sidebar_scale_hints(sidebar_field); - else if (boost::starts_with(sidebar_field, "size")) - render_sidebar_size_hints(sidebar_field); else if (boost::starts_with(sidebar_field, "layer")) render_sidebar_layers_hints(sidebar_field); glsafe(::glPopMatrix()); if (!boost::starts_with(sidebar_field, "layer")) - { - glsafe(::glDisable(GL_LIGHTING)); - shader.stop_using(); - } + shader->stop_using(); } bool Selection::requires_local_axes() const @@ -1595,20 +1595,21 @@ void Selection::update_type() } else { + unsigned int sla_volumes_count = 0; + // Note: sla_volumes_count is a count of the selected sla_volumes per object instead of per instance, like a model_volumes_count is + for (unsigned int i : m_list) { + if ((*m_volumes)[i]->volume_idx() < 0) + ++sla_volumes_count; + } + if (m_cache.content.size() == 1) // single object { const ModelObject* model_object = m_model->objects[m_cache.content.begin()->first]; unsigned int model_volumes_count = (unsigned int)model_object->volumes.size(); - unsigned int sla_volumes_count = 0; - for (unsigned int i : m_list) - { - if ((*m_volumes)[i]->volume_idx() < 0) - ++sla_volumes_count; - } - unsigned int volumes_count = model_volumes_count + sla_volumes_count; + unsigned int instances_count = (unsigned int)model_object->instances.size(); unsigned int selected_instances_count = (unsigned int)m_cache.content.begin()->second.size(); - if (volumes_count * instances_count == (unsigned int)m_list.size()) + if (model_volumes_count * instances_count + sla_volumes_count == (unsigned int)m_list.size()) { m_type = SingleFullObject; // ensures the correct mode is selected @@ -1616,7 +1617,7 @@ void Selection::update_type() } else if (selected_instances_count == 1) { - if (volumes_count == (unsigned int)m_list.size()) + if (model_volumes_count + sla_volumes_count == (unsigned int)m_list.size()) { m_type = SingleFullInstance; // ensures the correct mode is selected @@ -1639,7 +1640,7 @@ void Selection::update_type() requires_disable = true; } } - else if ((selected_instances_count > 1) && (selected_instances_count * volumes_count == (unsigned int)m_list.size())) + else if ((selected_instances_count > 1) && (selected_instances_count * model_volumes_count + sla_volumes_count == (unsigned int)m_list.size())) { m_type = MultipleFullInstance; // ensures the correct mode is selected @@ -1656,7 +1657,7 @@ void Selection::update_type() unsigned int instances_count = (unsigned int)model_object->instances.size(); sels_cntr += volumes_count * instances_count; } - if (sels_cntr == (unsigned int)m_list.size()) + if (sels_cntr + sla_volumes_count == (unsigned int)m_list.size()) { m_type = MultipleFullObject; // ensures the correct mode is selected @@ -1943,6 +1944,29 @@ void Selection::render_bounding_box(const BoundingBoxf3& box, float* color) cons glsafe(::glEnd()); } +#if ENABLE_GCODE_VIEWER +void Selection::render_sidebar_position_hints(const std::string& sidebar_field) const +{ + auto set_color = [](Axis axis) { + GLShaderProgram* shader = wxGetApp().get_current_shader(); + if (shader != nullptr) + shader->set_uniform("uniform_color", AXES_COLOR[axis], 4); + }; + + if (boost::ends_with(sidebar_field, "x")) { + set_color(X); + glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); + m_arrow.render(); + } else if (boost::ends_with(sidebar_field, "y")) { + set_color(Y); + m_arrow.render(); + } else if (boost::ends_with(sidebar_field, "z")) { + set_color(Z); + glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); + m_arrow.render(); + } +} +#else void Selection::render_sidebar_position_hints(const std::string& sidebar_field) const { if (boost::ends_with(sidebar_field, "x")) @@ -1958,8 +1982,38 @@ void Selection::render_sidebar_position_hints(const std::string& sidebar_field) render_sidebar_position_hint(Z); } } +#endif // ENABLE_GCODE_VIEWER +#if ENABLE_GCODE_VIEWER void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field) const +{ + auto set_color = [](Axis axis) { + GLShaderProgram* shader = wxGetApp().get_current_shader(); + if (shader != nullptr) + shader->set_uniform("uniform_color", AXES_COLOR[axis], 4); + }; + + auto render_sidebar_rotation_hint = [this]() { + m_curved_arrow.render(); + glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); + m_curved_arrow.render(); + }; + + if (boost::ends_with(sidebar_field, "x")) { + set_color(X); + glsafe(::glRotated(90.0, 0.0, 1.0, 0.0)); + render_sidebar_rotation_hint(); + } else if (boost::ends_with(sidebar_field, "y")) { + set_color(Y); + glsafe(::glRotated(-90.0, 1.0, 0.0, 0.0)); + render_sidebar_rotation_hint(); + } else if (boost::ends_with(sidebar_field, "z")) { + set_color(Z); + render_sidebar_rotation_hint(); + } +} +#else +void Selection::render_sidebar_rotation_hints(const std::string & sidebar_field) const { if (boost::ends_with(sidebar_field, "x")) { @@ -1974,11 +2028,33 @@ void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field) else if (boost::ends_with(sidebar_field, "z")) render_sidebar_rotation_hint(Z); } +#endif // ENABLE_GCODE_VIEWER void Selection::render_sidebar_scale_hints(const std::string& sidebar_field) const { bool uniform_scale = requires_uniform_scale() || wxGetApp().obj_manipul()->get_uniform_scaling(); + auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis) { + GLShaderProgram* shader = wxGetApp().get_current_shader(); + if (shader != nullptr) + shader->set_uniform("uniform_color", uniform_scale ? UNIFORM_SCALE_COLOR : AXES_COLOR[axis], 4); + + glsafe(::glTranslated(0.0, 5.0, 0.0)); +#if ENABLE_GCODE_VIEWER + m_arrow.render(); +#else + m_arrow->render(); +#endif // ENABLE_GCODE_VIEWER + + glsafe(::glTranslated(0.0, -10.0, 0.0)); + glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); +#if ENABLE_GCODE_VIEWER + m_arrow.render(); +#else + m_arrow->render(); +#endif // ENABLE_GCODE_VIEWER + }; + if (boost::ends_with(sidebar_field, "x") || uniform_scale) { glsafe(::glPushMatrix()); @@ -2003,11 +2079,6 @@ void Selection::render_sidebar_scale_hints(const std::string& sidebar_field) con } } -void Selection::render_sidebar_size_hints(const std::string& sidebar_field) const -{ - render_sidebar_scale_hints(sidebar_field); -} - void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) const { static const double Margin = 10.0; @@ -2080,6 +2151,7 @@ void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) co glsafe(::glDisable(GL_BLEND)); } +#if !ENABLE_GCODE_VIEWER void Selection::render_sidebar_position_hint(Axis axis) const { m_arrow->set_color(AXES_COLOR[axis], 3); @@ -2106,10 +2178,7 @@ void Selection::render_sidebar_scale_hint(Axis axis) const glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); m_arrow->render(); } - -void Selection::render_sidebar_size_hint(Axis axis, double length) const -{ -} +#endif // !ENABLE_GCODE_VIEWER #ifndef NDEBUG static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index 7a929926c4..2d87b9873d 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -3,7 +3,9 @@ #include #include "libslic3r/Geometry.hpp" - +#if ENABLE_GCODE_VIEWER +#include "GLModel.hpp" +#endif // ENABLE_GCODE_VIEWER #if ENABLE_RENDER_SELECTION_CENTER class GLUquadric; @@ -19,6 +21,7 @@ class GLVolume; class GLArrow; class GLCurvedArrow; class DynamicPrintConfig; +class GLShaderProgram; using GLVolumePtrs = std::vector; using ModelObjectPtrs = std::vector; @@ -218,10 +221,15 @@ private: GLUquadricObj* m_quadric; #endif // ENABLE_RENDER_SELECTION_CENTER +#if ENABLE_GCODE_VIEWER + GLModel m_arrow; + GLModel m_curved_arrow; +#else // Arrows are saved through pointers to avoid including 3DScene.hpp. // It also allows mutability. std::unique_ptr m_arrow; std::unique_ptr m_curved_arrow; +#endif // ENABLE_GCODE_VIEWER mutable float m_scale_factor; @@ -336,7 +344,7 @@ public: #if ENABLE_RENDER_SELECTION_CENTER void render_center(bool gizmo_is_dragging) const; #endif // ENABLE_RENDER_SELECTION_CENTER - void render_sidebar_hints(const std::string& sidebar_field, const Shader& shader) const; + void render_sidebar_hints(const std::string& sidebar_field) const; bool requires_local_axes() const; @@ -377,12 +385,12 @@ private: void render_sidebar_position_hints(const std::string& sidebar_field) const; void render_sidebar_rotation_hints(const std::string& sidebar_field) const; void render_sidebar_scale_hints(const std::string& sidebar_field) const; - void render_sidebar_size_hints(const std::string& sidebar_field) const; void render_sidebar_layers_hints(const std::string& sidebar_field) const; +#if !ENABLE_GCODE_VIEWER void render_sidebar_position_hint(Axis axis) const; void render_sidebar_rotation_hint(Axis axis) const; void render_sidebar_scale_hint(Axis axis) const; - void render_sidebar_size_hint(Axis axis, double length) const; +#endif // !ENABLE_GCODE_VIEWER public: enum SyncRotationType { diff --git a/src/slic3r/GUI/SysInfoDialog.cpp b/src/slic3r/GUI/SysInfoDialog.cpp index 3bd0fcf9f7..34905fa6d4 100644 --- a/src/slic3r/GUI/SysInfoDialog.cpp +++ b/src/slic3r/GUI/SysInfoDialog.cpp @@ -34,9 +34,17 @@ std::string get_main_info(bool format_as_html) std::string line_end = format_as_html ? "
" : "\n"; if (!format_as_html) +#if ENABLE_GCODE_VIEWER + out << b_start << (wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME) << b_end << line_end; +#else out << b_start << SLIC3R_APP_NAME << b_end << line_end; +#endif // ENABLE_GCODE_VIEWER out << b_start << "Version: " << b_end << SLIC3R_VERSION << line_end; +#if ENABLE_GCODE_VIEWER + out << b_start << "Build: " << b_end << (wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID) << line_end; +#else out << b_start << "Build: " << b_end << SLIC3R_BUILD_ID << line_end; +#endif // ENABLE_GCODE_VIEWER out << line_end; out << b_start << "Operating System: " << b_end << wxPlatformInfo::Get().GetOperatingSystemFamilyName() << line_end; out << b_start << "System Architecture: " << b_end << wxPlatformInfo::Get().GetArchName() << line_end; @@ -78,7 +86,11 @@ std::string get_mem_info(bool format_as_html) } SysInfoDialog::SysInfoDialog() - : DPIDialog(NULL, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(L("System Information")), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) +#if ENABLE_GCODE_VIEWER + : DPIDialog(NULL, wxID_ANY, (wxGetApp().is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME)) + " - " + _L("System Information"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +#else + : DPIDialog(NULL, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _L("System Information"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER) +#endif // ENABLE_GCODE_VIEWER { wxColour bgr_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); SetBackgroundColour(bgr_clr); @@ -91,7 +103,11 @@ SysInfoDialog::SysInfoDialog() main_sizer->Add(hsizer, 1, wxEXPAND | wxALL, 10); // logo +#if ENABLE_GCODE_VIEWER + m_logo_bmp = ScalableBitmap(this, wxGetApp().is_editor() ? "PrusaSlicer_192px.png" : "PrusaSlicer-gcodeviewer_192px.png", 192); +#else m_logo_bmp = ScalableBitmap(this, "PrusaSlicer_192px.png", 192); +#endif // ENABLE_GCODE_VIEWER m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp()); hsizer->Add(m_logo, 0, wxALIGN_CENTER_VERTICAL); @@ -100,7 +116,11 @@ SysInfoDialog::SysInfoDialog() // title { +#if ENABLE_GCODE_VIEWER + wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize); +#else wxStaticText* title = new wxStaticText(this, wxID_ANY, SLIC3R_APP_NAME, wxDefaultPosition, wxDefaultSize); +#endif // ENABLE_GCODE_VIEWER wxFont title_font = wxGetApp().bold_font(); title_font.SetFamily(wxFONTFAMILY_ROMAN); title_font.SetPointSize(22); @@ -109,7 +129,7 @@ SysInfoDialog::SysInfoDialog() } // main_info_text - wxFont font = wxGetApp().normal_font(); + wxFont font = get_default_font_for_dpi(get_dpi_for_window(this));// wxGetApp().normal_font(); const auto text_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); auto text_clr_str = wxString::Format(wxT("#%02X%02X%02X"), text_clr.Red(), text_clr.Green(), text_clr.Blue()); auto bgr_clr_str = wxString::Format(wxT("#%02X%02X%02X"), bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()); @@ -154,7 +174,7 @@ SysInfoDialog::SysInfoDialog() } wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK); - m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _(L("Copy to Clipboard")), wxDefaultPosition, wxDefaultSize); + m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _L("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize); buttons->Insert(0, m_btn_copy_to_clipboard, 0, wxLEFT, 5); m_btn_copy_to_clipboard->Bind(wxEVT_BUTTON, &SysInfoDialog::onCopyToClipboard, this); @@ -175,7 +195,7 @@ void SysInfoDialog::on_dpi_changed(const wxRect &suggested_rect) m_logo_bmp.msw_rescale(); m_logo->SetBitmap(m_logo_bmp.bmp()); - wxFont font = GetFont(); + wxFont font = get_default_font_for_dpi(get_dpi_for_window(this));// GetFont(); const int fs = font.GetPointSize() - 1; int font_size[] = { static_cast(fs*1.5), static_cast(fs*1.4), static_cast(fs*1.3), fs, fs, fs, fs }; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 84bc5a5726..29c9e33022 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1,8 +1,8 @@ // #include "libslic3r/GCodeSender.hpp" #include "slic3r/Utils/Serial.hpp" #include "Tab.hpp" -#include "PresetBundle.hpp" #include "PresetHints.hpp" +#include "libslic3r/PresetBundle.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" @@ -27,14 +27,16 @@ #include #include "wxExtensions.hpp" +#include "PresetComboBoxes.hpp" #include #include "GUI_App.hpp" #include "GUI_ObjectList.hpp" -#include "ConfigWizard.hpp" #include "Plater.hpp" #include "MainFrame.hpp" #include "format.hpp" +#include "PhysicalPrinterDialog.hpp" +#include "UnsavedChangesDialog.hpp" namespace Slic3r { namespace GUI { @@ -100,7 +102,7 @@ Tab::Tab(wxNotebook* parent, const wxString& title, Preset::Type type) : wxGetApp().tabs_list.push_back(this); - m_em_unit = wxGetApp().em_unit(); + m_em_unit = em_unit(m_parent); //wxGetApp().em_unit(); m_config_manipulation = get_config_manipulation(); @@ -160,10 +162,17 @@ void Tab::create_preset_tab() #endif //__WXOSX__ // preset chooser - m_presets_choice = new PresetBitmapComboBox(panel, wxSize(35 * m_em_unit, -1)); + m_presets_choice = new TabPresetComboBox(panel, m_type); + m_presets_choice->set_selection_changed_function([this](int selection) { + if (!m_presets_choice->selection_is_changed_according_to_physical_printers()) + { + if (m_type == Preset::TYPE_PRINTER && !m_presets_choice->is_selected_physical_printer()) + m_preset_bundle->physical_printers.unselect_printer(); - // search combox -// m_search = new Search::SearchCtrl(panel); + // select preset + select_preset(m_presets_choice->GetString(selection).ToUTF8().data()); + } + }); auto color = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); @@ -173,6 +182,8 @@ void Tab::create_preset_tab() add_scaled_button(panel, &m_btn_save_preset, "save"); add_scaled_button(panel, &m_btn_delete_preset, "cross"); + if (m_type == Preset::Type::TYPE_PRINTER) + add_scaled_button(panel, &m_btn_edit_ph_printer, "cog"); m_show_incompatible_presets = false; add_scaled_bitmap(this, m_bmp_show_incompatible_presets, "flag_red"); @@ -184,6 +195,8 @@ void Tab::create_preset_tab() m_btn_save_preset->SetToolTip(from_u8((boost::format(_utf8(L("Save current %s"))) % m_title).str())); m_btn_delete_preset->SetToolTip(_(L("Delete this preset"))); m_btn_delete_preset->Disable(); + if (m_btn_edit_ph_printer) + m_btn_edit_ph_printer->Disable(); add_scaled_button(panel, &m_question_btn, "question"); m_question_btn->SetToolTip(_(L("Hover the cursor over buttons to find more information \n" @@ -238,6 +251,10 @@ void Tab::create_preset_tab() m_hsizer->Add(m_btn_save_preset, 0, wxALIGN_CENTER_VERTICAL); m_hsizer->AddSpacer(int(4 * scale_factor)); m_hsizer->Add(m_btn_delete_preset, 0, wxALIGN_CENTER_VERTICAL); + if (m_btn_edit_ph_printer) { + m_hsizer->AddSpacer(int(4 * scale_factor)); + m_hsizer->Add(m_btn_edit_ph_printer, 0, wxALIGN_CENTER_VERTICAL); + } m_hsizer->AddSpacer(int(/*16*/8 * scale_factor)); m_hsizer->Add(m_btn_hide_incompatible_presets, 0, wxALIGN_CENTER_VERTICAL); m_hsizer->AddSpacer(int(8 * scale_factor)); @@ -278,41 +295,19 @@ void Tab::create_preset_tab() m_treectrl->Bind(wxEVT_TREE_SEL_CHANGED, &Tab::OnTreeSelChange, this); m_treectrl->Bind(wxEVT_KEY_DOWN, &Tab::OnKeyDown, this); - m_presets_choice->Bind(wxEVT_COMBOBOX, ([this](wxCommandEvent e) { - //! Because of The MSW and GTK version of wxBitmapComboBox derived from wxComboBox, - //! but the OSX version derived from wxOwnerDrawnCombo, instead of: - //! select_preset(m_presets_choice->GetStringSelection().ToUTF8().data()); - //! we doing next: - // int selected_item = m_presets_choice->GetSelection(); - - // see https://github.com/prusa3d/PrusaSlicer/issues/3889 - // Under OSX: in case of use of a same names written in different case (like "ENDER" and "Ender") - // m_presets_choice->GetSelection() will return first item, because search in PopupListCtrl is case-insensitive. - // So, use GetSelection() from event parameter - int selected_item = e.GetSelection(); - if (m_selected_preset_item == size_t(selected_item) && !m_presets->current_is_dirty()) - return; - if (selected_item >= 0) { - std::string selected_string = m_presets_choice->GetString(selected_item).ToUTF8().data(); - if (selected_string.find(PresetCollection::separator_head()) == 0 - /*selected_string == "------- System presets -------" || - selected_string == "------- User presets -------"*/) { - m_presets_choice->SetSelection(m_selected_preset_item); - if (wxString::FromUTF8(selected_string.c_str()) == PresetCollection::separator(L("Add a new printer"))) - wxTheApp->CallAfter([]() { wxGetApp().run_wizard(ConfigWizard::RR_USER); }); - return; - } - m_selected_preset_item = selected_item; - select_preset(selected_string); - } - })); - m_btn_save_preset->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e) { save_preset(); })); m_btn_delete_preset->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e) { delete_preset(); })); m_btn_hide_incompatible_presets->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e) { toggle_show_hide_incompatible(); })); + if (m_btn_edit_ph_printer) + m_btn_edit_ph_printer->Bind(wxEVT_BUTTON, ([this](wxCommandEvent e) { + PhysicalPrinterDialog dlg(m_presets_choice->GetString(m_presets_choice->GetSelection())); + if (dlg.ShowModal() == wxID_OK) + update_tab_ui(); + })); + // Fill cache for mode bitmaps m_mode_bitmap_cache.reserve(3); m_mode_bitmap_cache.push_back(ScalableBitmap(this, "mode_simple" , mode_icon_px_size())); @@ -331,7 +326,7 @@ void Tab::add_scaled_button(wxWindow* parent, const wxString& label/* = wxEmptyString*/, long style /*= wxBU_EXACTFIT | wxNO_BORDER*/) { - *btn = new ScalableButton(parent, wxID_ANY, icon_name, label, wxDefaultSize, wxDefaultPosition, style); + *btn = new ScalableButton(parent, wxID_ANY, icon_name, label, wxDefaultSize, wxDefaultPosition, style, true); m_scaled_buttons.push_back(*btn); } @@ -418,8 +413,9 @@ void Tab::update_labels_colour() else color = &m_modified_label_clr; } - if (opt.first == "bed_shape" || opt.first == "compatible_prints" || opt.first == "compatible_printers") { - wxStaticText* label = (m_colored_Labels.find(opt.first) == m_colored_Labels.end()) ? nullptr : m_colored_Labels.at(opt.first); + if (opt.first == "bed_shape" || opt.first == "filament_ramming_parameters" || + opt.first == "compatible_prints" || opt.first == "compatible_printers" ) { + wxStaticText* label = m_colored_Labels.find(opt.first) == m_colored_Labels.end() ? nullptr : m_colored_Labels.at(opt.first); if (label) { label->SetForegroundColour(*color); label->Refresh(true); @@ -509,7 +505,8 @@ void Tab::update_changed_ui() icon = &m_bmp_white_bullet; tt = &m_tt_white_bullet; } - if (opt.first == "bed_shape" || opt.first == "compatible_prints" || opt.first == "compatible_printers") { + if (opt.first == "bed_shape" || opt.first == "filament_ramming_parameters" || + opt.first == "compatible_prints" || opt.first == "compatible_printers") { wxStaticText* label = (m_colored_Labels.find(opt.first) == m_colored_Labels.end()) ? nullptr : m_colored_Labels.at(opt.first); if (label) { label->SetForegroundColour(*color); @@ -661,6 +658,9 @@ void Tab::update_changed_tree_ui() get_sys_and_mod_flags(opt_key, sys_page, modified_page); } } + if (m_type == Preset::TYPE_FILAMENT && page->title() == "Advanced") { + get_sys_and_mod_flags("filament_ramming_parameters", sys_page, modified_page); + } if (page->title() == "Dependencies") { if (m_type == Slic3r::Preset::TYPE_PRINTER) { sys_page = m_presets->get_selected_preset_parent() != nullptr; @@ -739,7 +739,10 @@ void Tab::on_roll_back_value(const bool to_sys /*= true*/) to_sys ? group->back_to_sys_value("bed_shape") : group->back_to_initial_value("bed_shape"); load_key_value("bed_shape", true/*some value*/, true); } - + } + if (group->title == "Toolchange parameters with single extruder MM printers") { + if ((m_options_list["filament_ramming_parameters"] & os) == 0) + to_sys ? group->back_to_sys_value("filament_ramming_parameters") : group->back_to_initial_value("filament_ramming_parameters"); } if (group->title == "Profile dependencies") { // "compatible_printers" option doesn't exists in Printer Settimgs Tab @@ -778,14 +781,14 @@ void Tab::on_roll_back_value(const bool to_sys /*= true*/) // comparing the selected preset config with $self->{config}. void Tab::update_dirty() { - m_presets->update_dirty_ui(m_presets_choice); + m_presets_choice->update_dirty(); on_presets_changed(); update_changed_ui(); } void Tab::update_tab_ui() { - m_selected_preset_item = m_presets->update_tab_ui(m_presets_choice, m_show_incompatible_presets, m_em_unit); + m_presets_choice->update(); } // Load a provied DynamicConfig into the tab, modifying the active preset. @@ -847,20 +850,20 @@ void Tab::update_visibility() void Tab::msw_rescale() { - m_em_unit = wxGetApp().em_unit(); + m_em_unit = em_unit(m_parent); m_mode_sizer->msw_rescale(); + m_presets_choice->msw_rescale(); - m_presets_choice->SetSize(35 * m_em_unit, -1); m_treectrl->SetMinSize(wxSize(20 * m_em_unit, -1)); - update_tab_ui(); - // rescale buttons and cached bitmaps for (const auto btn : m_scaled_buttons) btn->msw_rescale(); for (const auto bmp : m_scaled_bitmaps) bmp->msw_rescale(); + for (const auto ikon : m_blinking_ikons) + ikon.second->msw_rescale(); for (ScalableBitmap& bmp : m_mode_bitmap_cache) bmp.msw_rescale(); @@ -963,7 +966,7 @@ void Tab::load_key_value(const std::string& opt_key, const boost::any& value, bo // Don't select another profile if this profile happens to become incompatible. m_preset_bundle->update_compatible(PresetSelectCompatibleType::Never); } - m_presets->update_dirty_ui(m_presets_choice); + m_presets_choice->update_dirty(); on_presets_changed(); update(); } @@ -1100,6 +1103,21 @@ void Tab::apply_searcher() wxGetApp().sidebar().get_searcher().apply(m_config, m_type, m_mode); } +void Tab::cache_config_diff(const std::vector& selected_options) +{ + m_cache_config.apply_only(m_presets->get_edited_preset().config, selected_options); +} + +void Tab::apply_config_from_cache() +{ + if (!m_cache_config.empty()) { + m_presets->get_edited_preset().config.apply(m_cache_config); + m_cache_config.clear(); + + update_dirty(); + } +} + // Call a callback to update the selection of presets on the plater: // To update the content of the selection boxes, @@ -1119,9 +1137,12 @@ void Tab::on_presets_changed() // Printer selected at the Printer tab, update "compatible" marks at the print and filament selectors. for (auto t: m_dependent_tabs) { + Tab* tab = wxGetApp().get_tab(t); // If the printer tells us that the print or filament/sla_material preset has been switched or invalidated, // refresh the print or filament/sla_material tab page. - wxGetApp().get_tab(t)->load_current_preset(); + // But if there are options, moved from the previously selected preset, update them to edited preset + tab->apply_config_from_cache(); + tab->load_current_preset(); } // clear m_dependent_tabs after first update from select_preset() // to avoid needless preset loading from update() function @@ -1742,22 +1763,21 @@ void TabFilament::build() optgroup->append_single_option_line("filament_cooling_initial_speed"); optgroup->append_single_option_line("filament_cooling_final_speed"); - line = optgroup->create_single_option_line("filament_ramming_parameters");// { _(L("Ramming")), "" }; - line.widget = [this](wxWindow* parent) { + create_line_with_widget(optgroup.get(), "filament_ramming_parameters", [this](wxWindow* parent) { auto ramming_dialog_btn = new wxButton(parent, wxID_ANY, _(L("Ramming settings"))+dots, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); ramming_dialog_btn->SetFont(Slic3r::GUI::wxGetApp().normal_font()); auto sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(ramming_dialog_btn); - ramming_dialog_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent& e) - { + ramming_dialog_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { RammingDialog dlg(this,(m_config->option("filament_ramming_parameters"))->get_at(0)); - if (dlg.ShowModal() == wxID_OK) - (m_config->option("filament_ramming_parameters"))->get_at(0) = dlg.get_parameters(); - })); + if (dlg.ShowModal() == wxID_OK) { + load_key_value("filament_ramming_parameters", dlg.get_parameters()); + update_changed_ui(); + } + }); return sizer; - }; - optgroup->append_line(line); + }); add_filament_overrides_page(); @@ -2148,11 +2168,10 @@ void TabPrinter::build_fff() line.append_widget(serial_test); optgroup->append_line(line); } -#endif optgroup = page->new_optgroup(L("Print Host upload")); build_printhost(optgroup.get()); - +#endif optgroup = page->new_optgroup(L("Firmware")); optgroup->append_single_option_line("gcode_flavor"); optgroup->append_single_option_line("silent_mode"); @@ -2310,8 +2329,10 @@ void TabPrinter::build_sla() optgroup->append_single_option_line("min_initial_exposure_time"); optgroup->append_single_option_line("max_initial_exposure_time"); + /* optgroup = page->new_optgroup(L("Print Host upload")); build_printhost(optgroup.get()); + */ const int notes_field_height = 25; // 250 @@ -2699,11 +2720,13 @@ void TabPrinter::update_fff() m_serial_test_btn->Disable(); } + /* { std::unique_ptr host(PrintHost::get_print_host(m_config)); m_print_host_test_btn->Enable(!m_config->opt_string("print_host").empty() && host->can_test()); m_printhost_browse_btn->Enable(host->has_auto_discovery()); } + */ bool have_multiple_extruders = m_extruders_count > 1; get_field("toolchange_gcode")->toggle(have_multiple_extruders); @@ -2805,7 +2828,7 @@ void Tab::load_current_preset() { const Preset& preset = m_presets->get_edited_preset(); - (preset.is_default || preset.is_system) ? m_btn_delete_preset->Disable() : m_btn_delete_preset->Enable(true); + update_btns_enabling(); update(); if (m_type == Slic3r::Preset::TYPE_PRINTER) { @@ -2861,7 +2884,7 @@ void Tab::load_current_preset() } on_presets_changed(); if (printer_technology == ptFFF) { - static_cast(this)->m_initial_extruders_count = static_cast(this)->m_extruders_count; + static_cast(this)->m_initial_extruders_count = static_cast(m_presets->get_selected_preset().config.option("nozzle_diameter"))->values.size(); //static_cast(this)->m_extruders_count; const Preset* parent_preset = m_presets->get_selected_preset_parent(); static_cast(this)->m_sys_extruders_count = parent_preset == nullptr ? 0 : static_cast(parent_preset->config.option("nozzle_diameter"))->values.size(); @@ -2942,10 +2965,31 @@ void Tab::update_page_tree_visibility() } +void Tab::update_btns_enabling() +{ + // we can't delete last preset from the physical printer + if (m_type == Preset::TYPE_PRINTER && m_preset_bundle->physical_printers.has_selection()) + m_btn_delete_preset->Enable(m_preset_bundle->physical_printers.get_selected_printer().preset_names.size() > 1); + else { + const Preset& preset = m_presets->get_edited_preset(); + m_btn_delete_preset->Enable(!preset.is_default && !preset.is_system); + } + + // we can edit physical printer only if it's selected in the list + if (m_btn_edit_ph_printer) + m_btn_edit_ph_printer->Enable(m_preset_bundle->physical_printers.has_selection()); +} + +void Tab::update_preset_choice() +{ + m_presets_choice->update(); + update_btns_enabling(); +} + // Called by the UI combo box when the user switches profiles, and also to delete the current profile. // Select a preset by a name.If !defined(name), then the default preset is selected. // If the current profile is modified, user is asked to save the changes. -void Tab::select_preset(std::string preset_name, bool delete_current) +void Tab::select_preset(std::string preset_name, bool delete_current /*=false*/, const std::string& last_selected_ph_printer_name/* =""*/) { if (preset_name.empty()) { if (delete_current) { @@ -2971,7 +3015,7 @@ void Tab::select_preset(std::string preset_name, bool delete_current) bool canceled = false; bool technology_changed = false; m_dependent_tabs.clear(); - if (current_dirty && ! may_discard_current_dirty_preset()) { + if (current_dirty && ! may_discard_current_dirty_preset(nullptr, preset_name)) { canceled = true; } else if (print_tab) { // Before switching the print profile to a new one, verify, whether the currently active filament or SLA material @@ -3053,7 +3097,16 @@ void Tab::select_preset(std::string preset_name, bool delete_current) } if (canceled) { + if (m_type == Preset::TYPE_PRINTER) { + if (!last_selected_ph_printer_name.empty() && + m_presets->get_edited_preset().name == PhysicalPrinter::get_preset_name(last_selected_ph_printer_name)) { + // If preset selection was canceled and previously was selected physical printer, we should select it back + m_preset_bundle->physical_printers.select_printer(last_selected_ph_printer_name); + } + } + update_tab_ui(); + // Trigger the on_presets_changed event so that we also restore the previous value in the plater selector, // if this action was initiated from the plater. on_presets_changed(); @@ -3095,6 +3148,14 @@ void Tab::select_preset(std::string preset_name, bool delete_current) else if (printer_technology == ptSLA && m_dependent_tabs.front() != Preset::Type::TYPE_SLA_PRINT) m_dependent_tabs = { Preset::Type::TYPE_SLA_PRINT, Preset::Type::TYPE_SLA_MATERIAL }; } + + // check and apply extruders count for printer preset + if (m_type == Preset::TYPE_PRINTER) + static_cast(this)->apply_extruder_cnt_from_cache(); + + // check if there is something in the cache to move to the new selected preset + apply_config_from_cache(); + load_current_preset(); } } @@ -3104,41 +3165,65 @@ void Tab::select_preset(std::string preset_name, bool delete_current) bool Tab::may_discard_current_dirty_preset(PresetCollection* presets /*= nullptr*/, const std::string& new_printer_name /*= ""*/) { if (presets == nullptr) presets = m_presets; - // Display a dialog showing the dirty options in a human readable form. - const Preset& old_preset = presets->get_edited_preset(); - std::string type_name = presets->name(); - wxString tab = " "; - wxString name = old_preset.is_default ? - from_u8((boost::format(_utf8(L("Default preset (%s)"))) % _utf8(type_name)).str()) : - from_u8((boost::format(_utf8(L("Preset (%s)"))) % _utf8(type_name)).str()) + "\n" + tab + old_preset.name; - // Collect descriptions of the dirty options. - wxString changes; - for (const std::string &opt_key : presets->current_dirty_options()) { - const ConfigOptionDef &opt = m_config->def()->options.at(opt_key); - /*std::string*/wxString name = ""; - if (! opt.category.empty()) - name += _(opt.category) + " > "; - name += !opt.full_label.empty() ? - _(opt.full_label) : - _(opt.label); - changes += tab + /*from_u8*/(name) + "\n"; + UnsavedChangesDialog dlg(m_type, presets, new_printer_name); + if (dlg.ShowModal() == wxID_CANCEL) + return false; + + if (dlg.save_preset()) // save selected changes + { + std::vector unselected_options = dlg.get_unselected_options(presets->type()); + const Preset& preset = presets->get_edited_preset(); + std::string name = preset.name; + + // for system/default/external presets we should take an edited name + if (preset.is_system || preset.is_default || preset.is_external) { + SavePresetDialog save_dlg(preset.type); + if (save_dlg.ShowModal() != wxID_OK) + return false; + name = save_dlg.get_name(); + } + + if (m_type == presets->type()) // save changes for the current preset from this tab + { + // revert unselected options to the old values + presets->get_edited_preset().config.apply_only(presets->get_selected_preset().config, unselected_options); + save_preset(name); + } + else + { + m_preset_bundle->save_changes_for_preset(name, presets->type(), unselected_options); + + /* If filament preset is saved for multi-material printer preset, + * there are cases when filament comboboxs are updated for old (non-modified) colors, + * but in full_config a filament_colors option aren't.*/ + if (presets->type() == Preset::TYPE_FILAMENT && wxGetApp().extruders_edited_cnt() > 1) + wxGetApp().plater()->force_filament_colors_update(); + } } - // Show a confirmation dialog with the list of dirty options. - wxString message = name + "\n\n"; - if (new_printer_name.empty()) - message += _(L("has the following unsaved changes:")); - else { - message += (m_type == Slic3r::Preset::TYPE_PRINTER) ? - _(L("is not compatible with printer")) : - _(L("is not compatible with print profile")); - message += wxString("\n") + tab + from_u8(new_printer_name) + "\n\n"; - message += _(L("and it has the following unsaved changes:")); + else if (dlg.move_preset()) // move selected changes + { + std::vector selected_options = dlg.get_selected_options(); + if (m_type == presets->type()) // move changes for the current preset from this tab + { + if (m_type == Preset::TYPE_PRINTER) { + auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); + if (it != selected_options.end()) { + // erase "extruders_count" option from the list + selected_options.erase(it); + // cache the extruders count + static_cast(this)->cache_extruder_cnt(); + } + } + + // copy selected options to the cache from edited preset + cache_config_diff(selected_options); + } + else + wxGetApp().get_tab(presets->type())->cache_config_diff(selected_options); } - wxMessageDialog confirm(parent(), - message + "\n" + changes + "\n\n" + _(L("Discard changes and continue anyway?")), - _(L("Unsaved Changes")), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION); - return confirm.ShowModal() == wxID_YES; + + return true; } // If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). @@ -3231,59 +3316,11 @@ void Tab::save_preset(std::string name /*= ""*/, bool detach) // focus currently.is there anything better than this ? //! m_treectrl->OnSetFocus(); - std::string suffix = detach ? _utf8(L("Detached")) : _CTX_utf8(L_CONTEXT("Copy", "PresetName"), "PresetName"); - if (name.empty()) { - const Preset &preset = m_presets->get_selected_preset(); - auto default_name = preset.is_default ? "Untitled" : -// preset.is_system ? (boost::format(_CTX_utf8(L_CONTEXT("%1% - Copy", "PresetName"), "PresetName")) % preset.name).str() : - preset.is_system ? (boost::format(("%1% - %2%")) % preset.name % suffix).str() : - preset.name; - - bool have_extention = boost::iends_with(default_name, ".ini"); - if (have_extention) { - size_t len = default_name.length()-4; - default_name.resize(len); - } - //[map $_->name, grep !$_->default && !$_->external, @{$self->{presets}}], - std::vector values; - for (size_t i = 0; i < m_presets->size(); ++i) { - const Preset &preset = m_presets->preset(i); - if (preset.is_default || preset.is_system || preset.is_external) - continue; - values.push_back(preset.name); - } - - SavePresetWindow dlg(parent()); - dlg.build(title(), default_name, values); + SavePresetDialog dlg(m_type, detach ? _u8L("Detached") : ""); if (dlg.ShowModal() != wxID_OK) return; name = dlg.get_name(); - if (name == "") { - show_error(this, _(L("The supplied name is empty. It can't be saved."))); - return; - } - const Preset *existing = m_presets->find_preset(name, false); - if (existing && (existing->is_default || existing->is_system)) { - show_error(this, _(L("Cannot overwrite a system profile."))); - return; - } - if (existing && (existing->is_external)) { - show_error(this, _(L("Cannot overwrite an external profile."))); - return; - } - if (existing && name != preset.name) - { - wxString msg_text = GUI::from_u8((boost::format(_utf8(L("Preset with name \"%1%\" already exists."))) % name).str()); - msg_text += "\n" + _(L("Replace?")); - wxMessageDialog dialog(nullptr, msg_text, _(L("Warning")), wxICON_WARNING | wxYES | wxNO); - - if (dialog.ShowModal() == wxID_NO) - return; - - // Remove the preset from the list. - m_presets->delete_preset(name); - } } // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini @@ -3345,13 +3382,70 @@ void Tab::delete_preset() // Don't let the user delete the ' - default - ' configuration. std::string action = current_preset.is_external ? _utf8(L("remove")) : _utf8(L("delete")); // TRN remove/delete - const wxString msg = from_u8((boost::format(_utf8(L("Are you sure you want to %1% the selected preset?"))) % action).str()); + + PhysicalPrinterCollection& physical_printers = m_preset_bundle->physical_printers; + wxString msg; + if (m_presets_choice->is_selected_physical_printer()) + msg = from_u8((boost::format(_u8L("Are you sure you want to delete \"%1%\" preset from the physical printer \"%2%\"?")) + % current_preset.name % physical_printers.get_selected_printer_name()).str()); + else + { + if (m_type == Preset::TYPE_PRINTER && !physical_printers.empty()) + { + // Check preset for delete in physical printers + // Ask a customer about next action, if there is a printer with just one preset and this preset is equal to delete + std::vector ph_printers = physical_printers.get_printers_with_preset(current_preset.name); + std::vector ph_printers_only = physical_printers.get_printers_with_only_preset(current_preset.name); + + if (!ph_printers.empty()) { + msg += _L("Next physical printer(s) has/have selected preset") + ":"; + for (const std::string& printer : ph_printers) + msg += "\n \"" + from_u8(printer) + "\","; + msg.RemoveLast(); + msg += "\n" + _L("Note, that selected preset will be deleted from this/those printer(s) too.")+ "\n\n"; + } + + if (!ph_printers_only.empty()) { + msg += _L("Next physical printer(s) has/have one and only selected preset") + ":"; + for (const std::string& printer : ph_printers_only) + msg += "\n \"" + from_u8(printer) + "\","; + msg.RemoveLast(); + msg += "\n" + _L("Note, that this/those printer(s) will be deleted after deleting of the selected preset.") + "\n\n"; + } + } + + msg += from_u8((boost::format(_u8L("Are you sure you want to %1% the selected preset?")) % action).str()); + } + action = current_preset.is_external ? _utf8(L("Remove")) : _utf8(L("Delete")); // TRN Remove/Delete wxString title = from_u8((boost::format(_utf8(L("%1% Preset"))) % action).str()); //action + _(L(" Preset")); if (current_preset.is_default || wxID_YES != wxMessageDialog(parent(), msg, title, wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION).ShowModal()) return; + + // if we just delete preset from the physical printer + if (m_presets_choice->is_selected_physical_printer()) { + PhysicalPrinter& printer = physical_printers.get_selected_printer(); + + if (printer.preset_names.size() == 1) { + wxMessageDialog dialog(nullptr, _L("It's a last for this physical printer. We can't delete it"), _L("Information"), wxICON_INFORMATION | wxOK); + dialog.ShowModal(); + return; + } + // just delete this preset from the current physical printer + printer.delete_preset(m_presets->get_edited_preset().name); + // select first from the possible presets for this printer + physical_printers.select_printer(printer); + + this->select_preset(physical_printers.get_selected_printer_preset_name()); + return; + } + + // delete selected preset from printers and printer, if it's needed + if (m_type == Preset::TYPE_PRINTER && !physical_printers.empty()) + physical_printers.delete_preset_from_printers(current_preset.name); + // Select will handle of the preset dependencies, of saving & closing the depending profiles, and // finally of deleting the preset. this->select_preset("", true); @@ -3360,6 +3454,7 @@ void Tab::delete_preset() void Tab::toggle_show_hide_incompatible() { m_show_incompatible_presets = !m_show_incompatible_presets; + m_presets_choice->set_show_incompatible_presets(m_show_incompatible_presets); update_show_hide_incompatible_button(); update_tab_ui(); } @@ -3533,6 +3628,25 @@ wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent) return sizer; } +void TabPrinter::cache_extruder_cnt() +{ + if (m_presets->get_edited_preset().printer_technology() == ptSLA) + return; + + m_cache_extruder_count = m_extruders_count; +} + +void TabPrinter::apply_extruder_cnt_from_cache() +{ + if (m_presets->get_edited_preset().printer_technology() == ptSLA) + return; + + if (m_cache_extruder_count > 0) { + m_presets->get_edited_preset().set_num_extruders(m_cache_extruder_count); + m_cache_extruder_count = 0; + } +} + void Tab::compatible_widget_reload(PresetDependencies &deps) { bool has_any = ! m_config->option(deps.key_list)->values.empty(); @@ -3919,6 +4033,7 @@ void TabSLAPrint::build() optgroup = page->new_optgroup(L("Support pillar")); optgroup->append_single_option_line("support_pillar_diameter"); + optgroup->append_single_option_line("support_small_pillar_diameter_percent"); optgroup->append_single_option_line("support_max_bridges_on_pillar"); optgroup->append_single_option_line("support_pillar_connection_mode"); @@ -3930,9 +4045,16 @@ void TabSLAPrint::build() optgroup->append_single_option_line("support_base_safety_distance"); // Mirrored parameter from Pad page for toggling elevation on the same page - optgroup->append_single_option_line("pad_around_object"); +// optgroup->append_single_option_line("pad_around_object"); optgroup->append_single_option_line("support_object_elevation"); + Line line{ "", "" }; + line.full_width = 1; + line.widget = [this](wxWindow* parent) { + return description_line_widget(parent, &m_support_object_elevation_description_line); + }; + optgroup->append_line(line); + optgroup = page->new_optgroup(L("Connection of the support sticks and junctions")); optgroup->append_single_option_line("support_critical_angle"); optgroup->append_single_option_line("support_max_bridge_length"); @@ -4006,6 +4128,14 @@ void TabSLAPrint::update() m_update_cnt++; m_config_manipulation.update_print_sla_config(m_config, true); + + bool elev = !m_config->opt_bool("pad_enable") || !m_config->opt_bool("pad_around_object"); + m_support_object_elevation_description_line->SetText(elev ? "" : + from_u8((boost::format(_u8L("\"%1%\" is disabled because \"%2%\" is on in \"%3%\" category.\n" + "To enable \"%1%\", please switch off \"%2%\"")) + % _L("Object elevation") % _L("Pad around object") % _L("Pad")).str())); + Layout(); + m_update_cnt--; if (m_update_cnt == 0) { diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 5805809bfb..f0b2e97b3d 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -33,12 +33,14 @@ #include "Event.hpp" #include "wxExtensions.hpp" #include "ConfigManipulation.hpp" -#include "Preset.hpp" #include "OptionsGroup.hpp" +#include "libslic3r/Preset.hpp" namespace Slic3r { namespace GUI { +class TabPresetComboBox; + // Single Tab page containing a{ vsizer } of{ optgroups } // package Slic3r::GUI::Tab::Page; using ConfigOptionsGroupShp = std::shared_ptr; @@ -113,10 +115,11 @@ protected: Preset::Type m_type; std::string m_name; const wxString m_title; - PresetBitmapComboBox* m_presets_choice; + TabPresetComboBox* m_presets_choice; ScalableButton* m_search_btn; ScalableButton* m_btn_save_preset; ScalableButton* m_btn_delete_preset; + ScalableButton* m_btn_edit_ph_printer {nullptr}; ScalableButton* m_btn_hide_incompatible_presets; wxBoxSizer* m_hsizer; wxBoxSizer* m_left_sizer; @@ -206,8 +209,6 @@ protected: bool m_is_nonsys_values{ true }; bool m_postpone_update_ui {false}; - size_t m_selected_preset_item{ 0 }; - void set_type(); int m_em_unit; @@ -230,6 +231,8 @@ protected: } m_highlighter; + DynamicPrintConfig m_cache_config; + public: PresetBundle* m_preset_bundle; bool m_show_btn_incompatible_presets = false; @@ -275,8 +278,10 @@ public: void load_current_preset(); void rebuild_page_tree(); void update_page_tree_visibility(); - // Select a new preset, possibly delete the current one. - void select_preset(std::string preset_name = "", bool delete_current = false); + void update_btns_enabling(); + void update_preset_choice(); + // Select a new preset, possibly delete the current one. + void select_preset(std::string preset_name = "", bool delete_current = false, const std::string& last_selected_ph_printer_name = ""); bool may_discard_current_dirty_preset(PresetCollection* presets = nullptr, const std::string& new_printer_name = ""); bool may_switch_to_SLA_preset(); @@ -320,13 +325,14 @@ public: DynamicPrintConfig* get_config() { return m_config; } PresetCollection* get_presets() { return m_presets; } - size_t get_selected_preset_item() { return m_selected_preset_item; } void on_value_change(const std::string& opt_key, const boost::any& value); void update_wiping_button_visibility(); void activate_option(const std::string& opt_key, const wxString& category); void apply_searcher(); + void cache_config_diff(const std::vector& selected_options); + void apply_config_from_cache(); protected: void create_line_with_widget(ConfigOptionsGroup* optgroup, const std::string& opt_key, widget_t widget); @@ -408,6 +414,7 @@ public: size_t m_extruders_count_old = 0; size_t m_initial_extruders_count; size_t m_sys_extruders_count; + size_t m_cache_extruder_count = 0; PrinterTechnology m_printer_technology = ptFFF; @@ -434,6 +441,8 @@ public: bool supports_printer_technology(const PrinterTechnology /* tech */) override { return true; } wxSizer* create_bed_shape_widget(wxWindow* parent); + void cache_extruder_cnt(); + void apply_extruder_cnt_from_cache(); }; class TabSLAMaterial : public Tab @@ -458,6 +467,9 @@ public: // Tab(parent, _(L("Print Settings")), L("sla_print")) {} Tab(parent, _(L("Print Settings")), Slic3r::Preset::TYPE_SLA_PRINT) {} ~TabSLAPrint() {} + + ogStaticText* m_support_object_elevation_description_line = nullptr; + void build() override; void reload_config() override; void update() override; diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp new file mode 100644 index 0000000000..5a0d23a209 --- /dev/null +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -0,0 +1,1066 @@ +#include "UnsavedChangesDialog.hpp" + +#include +#include +#include +#include +#include + +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "Tab.hpp" +#include "ExtraRenderers.hpp" +#include "wxExtensions.hpp" + +//#define FTS_FUZZY_MATCH_IMPLEMENTATION +//#include "fts_fuzzy_match.h" + +#include "BitmapCache.hpp" + +using boost::optional; + +namespace Slic3r { + +namespace GUI { + +// ---------------------------------------------------------------------------- +// ModelNode: a node inside UnsavedChangesModel +// ---------------------------------------------------------------------------- + +static const std::map type_icon_names = { + {Preset::TYPE_PRINT, "cog" }, + {Preset::TYPE_SLA_PRINT, "cog" }, + {Preset::TYPE_FILAMENT, "spool" }, + {Preset::TYPE_SLA_MATERIAL, "resin" }, + {Preset::TYPE_PRINTER, "printer" }, +}; + +static std::string black = "#000000"; +static std::string grey = "#808080"; +static std::string orange = "#ed6b21"; + +static void color_string(wxString& str, const std::string& color) +{ +#if defined(SUPPORTS_MARKUP) && !defined(__APPLE__) + str = from_u8((boost::format("%2%") % color % into_u8(str)).str()); +#endif +} + +static void make_string_bold(wxString& str) +{ +#if defined(SUPPORTS_MARKUP) && !defined(__APPLE__) + str = from_u8((boost::format("%1%") % into_u8(str)).str()); +#endif +} + +// preset(root) node +ModelNode::ModelNode(Preset::Type preset_type, const wxString& text, wxWindow* parent_win) : + m_parent_win(parent_win), + m_parent(nullptr), + m_preset_type(preset_type), + m_icon_name(type_icon_names.at(preset_type)), + m_text(text) +{ + UpdateIcons(); +} + +// group node +ModelNode::ModelNode(ModelNode* parent, const wxString& text, const std::string& icon_name) : + m_parent_win(parent->m_parent_win), + m_parent(parent), + m_icon_name(icon_name), + m_text(text) +{ + UpdateIcons(); +} + +// category node +ModelNode::ModelNode(ModelNode* parent, const wxString& text) : + m_parent_win(parent->m_parent_win), + m_parent(parent), + m_text(text) +{ +} + +#ifdef __linux__ +wxIcon ModelNode::get_bitmap(const wxString& color) +#else +wxBitmap ModelNode::get_bitmap(const wxString& color) +#endif // __linux__ +{ + /* It's supposed that standard size of an icon is 48px*16px for 100% scaled display. + * So set sizes for solid_colored icons used for filament preset + * and scale them in respect to em_unit value + */ + const double em = em_unit(m_parent_win); + const int icon_width = lround(6.4 * em); + const int icon_height = lround(1.6 * em); + + BitmapCache bmp_cache; + unsigned char rgb[3]; + BitmapCache::parse_color(into_u8(color), rgb); + // there is no need to scale created solid bitmap +#ifndef __linux__ + return bmp_cache.mksolid(icon_width, icon_height, rgb, true); +#else + wxIcon icon; + icon.CopyFromBitmap(bmp_cache.mksolid(icon_width, icon_height, rgb, true)); + return icon; +#endif // __linux__ +} + +// option node +ModelNode::ModelNode(ModelNode* parent, const wxString& text, const wxString& old_value, const wxString& new_value) : + m_parent(parent), + m_old_color(old_value.StartsWith("#") ? old_value : ""), + m_new_color(new_value.StartsWith("#") ? new_value : ""), + m_container(false), + m_text(text), + m_old_value(old_value), + m_new_value(new_value) +{ + // check if old/new_value is color + if (m_old_color.IsEmpty()) { + if (!m_new_color.IsEmpty()) + m_old_value = _L("Undef"); + } + else { + m_old_color_bmp = get_bitmap(m_old_color); + m_old_value.Clear(); + } + + if (m_new_color.IsEmpty()) { + if (!m_old_color.IsEmpty()) + m_new_value = _L("Undef"); + } + else { + m_new_color_bmp = get_bitmap(m_new_color); + m_new_value.Clear(); + } + + // "color" strings + color_string(m_old_value, black); + color_string(m_new_value, orange); +} + +void ModelNode::UpdateEnabling() +{ + auto change_text_color = [](wxString& str, const std::string& clr_from, const std::string& clr_to) + { +#if defined(SUPPORTS_MARKUP) && !defined(__APPLE__) + std::string old_val = into_u8(str); + boost::replace_all(old_val, clr_from, clr_to); + str = from_u8(old_val); +#endif + }; + + if (!m_toggle) { + change_text_color(m_text, black, grey); + change_text_color(m_old_value, black, grey); + change_text_color(m_new_value, orange,grey); + } + else { + change_text_color(m_text, grey, black); + change_text_color(m_old_value, grey, black); + change_text_color(m_new_value, grey, orange); + } + // update icons for the colors + UpdateIcons(); +} + +void ModelNode::UpdateIcons() +{ + // update icons for the colors, if any exists + if (!m_old_color.IsEmpty()) + m_old_color_bmp = get_bitmap(m_toggle ? m_old_color : grey); + if (!m_new_color.IsEmpty()) + m_new_color_bmp = get_bitmap(m_toggle ? m_new_color : grey); + + // update main icon, if any exists + if (m_icon_name.empty()) + return; + +#ifdef __linux__ + m_icon.CopyFromBitmap(create_scaled_bitmap(m_icon_name, m_parent_win, 16, !m_toggle)); +#else + m_icon = create_scaled_bitmap(m_icon_name, m_parent_win, 16, !m_toggle); +#endif //__linux__ +} + + +// ---------------------------------------------------------------------------- +// UnsavedChangesModel +// ---------------------------------------------------------------------------- + +UnsavedChangesModel::UnsavedChangesModel(wxWindow* parent) : + m_parent_win(parent) +{ +} + +UnsavedChangesModel::~UnsavedChangesModel() +{ + for (ModelNode* preset_node : m_preset_nodes) + delete preset_node; +} + +wxDataViewItem UnsavedChangesModel::AddPreset(Preset::Type type, wxString preset_name) +{ + // "color" strings + color_string(preset_name, black); + make_string_bold(preset_name); + + auto preset = new ModelNode(type, preset_name, m_parent_win); + m_preset_nodes.emplace_back(preset); + + wxDataViewItem child((void*)preset); + wxDataViewItem parent(nullptr); + + ItemAdded(parent, child); + return child; +} + +ModelNode* UnsavedChangesModel::AddOption(ModelNode* group_node, wxString option_name, wxString old_value, wxString new_value) +{ + ModelNode* option = new ModelNode(group_node, option_name, old_value, new_value); + group_node->Append(option); + wxDataViewItem group_item = wxDataViewItem((void*)group_node); + ItemAdded(group_item, wxDataViewItem((void*)option)); + + m_ctrl->Expand(group_item); + return option; +} + +ModelNode* UnsavedChangesModel::AddOptionWithGroup(ModelNode* category_node, wxString group_name, wxString option_name, wxString old_value, wxString new_value) +{ + ModelNode* group_node = new ModelNode(category_node, group_name); + category_node->Append(group_node); + ItemAdded(wxDataViewItem((void*)category_node), wxDataViewItem((void*)group_node)); + + return AddOption(group_node, option_name, old_value, new_value); +} + +ModelNode* UnsavedChangesModel::AddOptionWithGroupAndCategory(ModelNode* preset_node, wxString category_name, wxString group_name, wxString option_name, wxString old_value, wxString new_value) +{ + ModelNode* category_node = new ModelNode(preset_node, category_name, "cog"); + preset_node->Append(category_node); + ItemAdded(wxDataViewItem((void*)preset_node), wxDataViewItem((void*)category_node)); + + return AddOptionWithGroup(category_node, group_name, option_name, old_value, new_value); +} + +wxDataViewItem UnsavedChangesModel::AddOption(Preset::Type type, wxString category_name, wxString group_name, wxString option_name, + wxString old_value, wxString new_value) +{ + // "color" strings + color_string(category_name, black); + color_string(group_name, black); + color_string(option_name, black); + + // "make" strings bold + make_string_bold(category_name); + make_string_bold(group_name); + + // add items + for (ModelNode* preset : m_preset_nodes) + if (preset->type() == type) + { + for (ModelNode* category : preset->GetChildren()) + if (category->text() == category_name) + { + for (ModelNode* group : category->GetChildren()) + if (group->text() == group_name) + return wxDataViewItem((void*)AddOption(group, option_name, old_value, new_value)); + + return wxDataViewItem((void*)AddOptionWithGroup(category, group_name, option_name, old_value, new_value)); + } + + return wxDataViewItem((void*)AddOptionWithGroupAndCategory(preset, category_name, group_name, option_name, old_value, new_value)); + } + + return wxDataViewItem(nullptr); +} + +static void update_children(ModelNode* parent) +{ + if (parent->IsContainer()) { + bool toggle = parent->IsToggled(); + for (ModelNode* child : parent->GetChildren()) { + child->Toggle(toggle); + child->UpdateEnabling(); + update_children(child); + } + } +} + +static void update_parents(ModelNode* node) +{ + ModelNode* parent = node->GetParent(); + if (parent) { + bool toggle = false; + for (ModelNode* child : parent->GetChildren()) { + if (child->IsToggled()) { + toggle = true; + break; + } + } + parent->Toggle(toggle); + parent->UpdateEnabling(); + update_parents(parent); + } +} + +void UnsavedChangesModel::UpdateItemEnabling(wxDataViewItem item) +{ + assert(item.IsOk()); + ModelNode* node = (ModelNode*)item.GetID(); + node->UpdateEnabling(); + + update_children(node); + update_parents(node); +} + +bool UnsavedChangesModel::IsEnabledItem(const wxDataViewItem& item) +{ + assert(item.IsOk()); + ModelNode* node = (ModelNode*)item.GetID(); + return node->IsToggled(); +} + +void UnsavedChangesModel::GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const +{ + assert(item.IsOk()); + + ModelNode* node = (ModelNode*)item.GetID(); + switch (col) + { + case colToggle: + variant = node->m_toggle; + break; +#ifdef __linux__ + case colIconText: + variant << wxDataViewIconText(node->m_text, node->m_icon); + break; + case colOldValue: + variant << wxDataViewIconText(node->m_old_value, node->m_old_color_bmp); + break; + case colNewValue: + variant << wxDataViewIconText(node->m_new_value, node->m_new_color_bmp); + break; +#else + case colIconText: + variant << DataViewBitmapText(node->m_text, node->m_icon); + break; + case colOldValue: + variant << DataViewBitmapText(node->m_old_value, node->m_old_color_bmp); + break; + case colNewValue: + variant << DataViewBitmapText(node->m_new_value, node->m_new_color_bmp); + break; +#endif //__linux__ + + default: + wxLogError("UnsavedChangesModel::GetValue: wrong column %d", col); + } +} + +bool UnsavedChangesModel::SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) +{ + assert(item.IsOk()); + + ModelNode* node = (ModelNode*)item.GetID(); + switch (col) + { + case colToggle: + node->m_toggle = variant.GetBool(); + return true; +#ifdef __linux__ + case colIconText: { + wxDataViewIconText data; + data << variant; + node->m_icon = data.GetIcon(); + node->m_text = data.GetText(); + return true; } + case colOldValue: { + wxDataViewIconText data; + data << variant; + node->m_old_color_bmp = data.GetIcon(); + node->m_old_value = data.GetText(); + return true; } + case colNewValue: { + wxDataViewIconText data; + data << variant; + node->m_new_color_bmp = data.GetIcon(); + node->m_new_value = data.GetText(); + return true; } +#else + case colIconText: { + DataViewBitmapText data; + data << variant; + node->m_icon = data.GetBitmap(); + node->m_text = data.GetText(); + return true; } + case colOldValue: { + DataViewBitmapText data; + data << variant; + node->m_old_color_bmp = data.GetBitmap(); + node->m_old_value = data.GetText(); + return true; } + case colNewValue: { + DataViewBitmapText data; + data << variant; + node->m_new_color_bmp = data.GetBitmap(); + node->m_new_value = data.GetText(); + return true; } +#endif //__linux__ + default: + wxLogError("UnsavedChangesModel::SetValue: wrong column"); + } + return false; +} + +bool UnsavedChangesModel::IsEnabled(const wxDataViewItem& item, unsigned int col) const +{ + assert(item.IsOk()); + if (col == colToggle) + return true; + + // disable unchecked nodes + return ((ModelNode*)item.GetID())->IsToggled(); +} + +wxDataViewItem UnsavedChangesModel::GetParent(const wxDataViewItem& item) const +{ + // the invisible root node has no parent + if (!item.IsOk()) + return wxDataViewItem(nullptr); + + ModelNode* node = (ModelNode*)item.GetID(); + + // "MyMusic" also has no parent + if (node->IsRoot()) + return wxDataViewItem(nullptr); + + return wxDataViewItem((void*)node->GetParent()); +} + +bool UnsavedChangesModel::IsContainer(const wxDataViewItem& item) const +{ + // the invisble root node can have children + if (!item.IsOk()) + return true; + + ModelNode* node = (ModelNode*)item.GetID(); + return node->IsContainer(); +} + +unsigned int UnsavedChangesModel::GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const +{ + ModelNode* node = (ModelNode*)parent.GetID(); + if (!node) { + for (auto preset_node : m_preset_nodes) + array.Add(wxDataViewItem((void*)preset_node)); + return m_preset_nodes.size(); + } + + if (node->GetChildCount() == 0) + return 0; + + unsigned int count = node->GetChildren().GetCount(); + for (unsigned int pos = 0; pos < count; pos++) { + ModelNode* child = node->GetChildren().Item(pos); + array.Add(wxDataViewItem((void*)child)); + } + + return count; +} + + +wxString UnsavedChangesModel::GetColumnType(unsigned int col) const +{ + switch (col) + { + case colToggle: + return "bool"; + case colIconText: + case colOldValue: + case colNewValue: + default: + return "DataViewBitmapText";//"string"; + } +} + +static void rescale_children(ModelNode* parent) +{ + if (parent->IsContainer()) { + for (ModelNode* child : parent->GetChildren()) { + child->UpdateIcons(); + rescale_children(child); + } + } +} + +void UnsavedChangesModel::Rescale() +{ + for (ModelNode* node : m_preset_nodes) { + node->UpdateIcons(); + rescale_children(node); + } +} + + +//------------------------------------------ +// UnsavedChangesDialog +//------------------------------------------ + +UnsavedChangesDialog::UnsavedChangesDialog(const wxString& header) + : DPIDialog(nullptr, wxID_ANY, _L("Unsaved Changes"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + build(Preset::TYPE_INVALID, nullptr, "", header); +} + +UnsavedChangesDialog::UnsavedChangesDialog(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset) + : DPIDialog(nullptr, wxID_ANY, _L("Unsaved Changes"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + build(type, dependent_presets, new_selected_preset); +} + +void UnsavedChangesDialog::build(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header) +{ + wxColour bgr_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + SetBackgroundColour(bgr_clr); + +#if ENABLE_WX_3_1_3_DPI_CHANGED_EVENT && defined(__WXMSW__) + // ys_FIXME! temporary workaround for correct font scaling + // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, + // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT + this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); +#endif // ENABLE_WX_3_1_3_DPI_CHANGED_EVENT + + int border = 10; + int em = em_unit(); + + m_action_line = new wxStaticText(this, wxID_ANY, ""); + m_action_line->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); + + m_tree = new wxDataViewCtrl(this, wxID_ANY, wxDefaultPosition, wxSize(em * 80, em * 30), wxBORDER_SIMPLE | wxDV_VARIABLE_LINE_HEIGHT | wxDV_ROW_LINES); + m_tree_model = new UnsavedChangesModel(this); + m_tree->AssociateModel(m_tree_model); + m_tree_model->SetAssociatedControl(m_tree); + + m_tree->AppendToggleColumn(L"\u2714", UnsavedChangesModel::colToggle, wxDATAVIEW_CELL_ACTIVATABLE, 6 * em);//2610,11,12 //2714 + + auto append_bmp_text_column = [this](const wxString& label, unsigned model_column, int width, bool set_expander = false) + { +#ifdef __linux__ + wxDataViewIconTextRenderer* rd = new wxDataViewIconTextRenderer(); +#ifdef SUPPORTS_MARKUP + rd->EnableMarkup(true); +#endif + wxDataViewColumn* column = new wxDataViewColumn(label, rd, model_column, width, wxALIGN_TOP, wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_CELL_INERT); +#else + wxDataViewColumn* column = new wxDataViewColumn(label, new BitmapTextRenderer(true), model_column, width, wxALIGN_TOP, wxDATAVIEW_COL_RESIZABLE); +#endif //__linux__ + m_tree->AppendColumn(column); + if (set_expander) + m_tree->SetExpanderColumn(column); + }; + + append_bmp_text_column("", UnsavedChangesModel::colIconText, 30 * em); + append_bmp_text_column(_L("Old Value"), UnsavedChangesModel::colOldValue, 20 * em); + append_bmp_text_column(_L("New Value"), UnsavedChangesModel::colNewValue, 20 * em); + + m_tree->Bind(wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, &UnsavedChangesDialog::item_value_changed, this); + m_tree->Bind(wxEVT_DATAVIEW_ITEM_CONTEXT_MENU, &UnsavedChangesDialog::context_menu, this); + + // Add Buttons + wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxCANCEL); + + auto add_btn = [this, buttons](ScalableButton** btn, int& btn_id, const std::string& icon_name, Action close_act, int idx, bool process_enable = true) + { + *btn = new ScalableButton(this, btn_id = NewControlId(), icon_name, "", wxDefaultSize, wxDefaultPosition, wxBORDER_DEFAULT, true); + buttons->Insert(idx, *btn, 0, wxLEFT, 5); + + (*btn)->Bind(wxEVT_BUTTON, [this, close_act](wxEvent&) { close(close_act); }); + if (process_enable) + (*btn)->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(!m_empty_selection); }); + (*btn)->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) { show_info_line(Action::Undef); e.Skip(); }); + }; + + int btn_idx = 0; + add_btn(&m_save_btn, m_save_btn_id, "save", Action::Save, btn_idx++); + if (dependent_presets && (type != dependent_presets->type() ? true : + dependent_presets->get_edited_preset().printer_technology() == dependent_presets->find_preset(new_selected_preset)->printer_technology())) + add_btn(&m_move_btn, m_move_btn_id, "paste_menu", Action::Move, btn_idx++); + add_btn(&m_continue_btn, m_continue_btn_id, "cross", Action::Continue, btn_idx, false); + + m_info_line = new wxStaticText(this, wxID_ANY, ""); + m_info_line->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); + m_info_line->Hide(); + + wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); + + topSizer->Add(m_action_line,0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(m_tree, 1, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(m_info_line, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, 2*border); + topSizer->Add(buttons, 0, wxEXPAND | wxALL, border); + + update(type, dependent_presets, new_selected_preset, header); + + SetSizer(topSizer); + topSizer->SetSizeHints(this); + + show_info_line(Action::Undef); +} + +void UnsavedChangesDialog::item_value_changed(wxDataViewEvent& event) +{ + if (event.GetColumn() != UnsavedChangesModel::colToggle) + return; + + wxDataViewItem item = event.GetItem(); + + m_tree_model->UpdateItemEnabling(item); + m_tree->Refresh(); + + // update an enabling of the "save/move" buttons + m_empty_selection = get_selected_options().empty(); +} + +void UnsavedChangesDialog::context_menu(wxDataViewEvent& event) +{ + if (!m_has_long_strings) + return; + + wxDataViewItem item = event.GetItem(); + if (!item) + { + wxPoint mouse_pos = wxGetMousePosition() - m_tree->GetScreenPosition(); + wxDataViewColumn* col = nullptr; + m_tree->HitTest(mouse_pos, item, col); + + if (!item) + item = m_tree->GetSelection(); + + if (!item) + return; + } + + auto it = m_items_map.find(item); + if (it == m_items_map.end() || !it->second.is_long) + return; + FullCompareDialog(it->second.opt_name, it->second.old_val, it->second.new_val).ShowModal(); +} + +void UnsavedChangesDialog::show_info_line(Action action, std::string preset_name) +{ + if (m_motion_action == action) + return; + if (action == Action::Undef && !m_has_long_strings) + m_info_line->Hide(); + else { + wxString text; + if (action == Action::Undef) + text = _L("Some fields are too long to fit. Right click on it to show full text."); + else if (action == Action::Continue) + text = _L("All changed options will be reverted."); + else { + std::string act_string = action == Action::Save ? _u8L("save") : _u8L("move"); + if (preset_name.empty()) + text = from_u8((boost::format("Press to %1% selected options.") % act_string).str()); + else + text = from_u8((boost::format("Press to %1% selected options to the preset \"%2%\".") % act_string % preset_name).str()); + text += "\n" + _L("Unselected options will be reverted."); + } + m_info_line->SetLabel(text); + m_info_line->Show(); + } + + m_motion_action = action; + + Layout(); + Refresh(); +} + +void UnsavedChangesDialog::close(Action action) +{ + m_exit_action = action; + this->EndModal(wxID_CLOSE); +} + +template +wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false) +{ + const ConfigOptionDef& def = config.def()->options.at(opt_key); + const std::vector& names = def.enum_labels;//ConfigOptionEnum::get_enum_names(); + T val = config.option>(opt_key)->value; + + // Each infill doesn't use all list of infill declared in PrintConfig.hpp. + // So we should "convert" val to the correct one + if (is_infill) { + for (auto key_val : *def.enum_keys_map) + if ((int)key_val.second == (int)val) { + auto it = std::find(def.enum_values.begin(), def.enum_values.end(), key_val.first); + if (it == def.enum_values.end()) + return ""; + return from_u8(_utf8(names[it - def.enum_values.begin()])); + } + return _L("Undef"); + } + return from_u8(_utf8(names[static_cast(val)])); +} + +static int get_id_from_opt_key(std::string opt_key) +{ + int pos = opt_key.find("#"); + if (pos > 0) { + boost::erase_head(opt_key, pos + 1); + return atoi(opt_key.c_str()); + } + return 0; +} + +static std::string get_pure_opt_key(std::string opt_key) +{ + int pos = opt_key.find("#"); + if (pos > 0) + boost::erase_tail(opt_key, opt_key.size() - pos); + return opt_key; +} + +static wxString get_string_value(std::string opt_key, const DynamicPrintConfig& config) +{ + int opt_idx = get_id_from_opt_key(opt_key); + opt_key = get_pure_opt_key(opt_key); + + if (config.option(opt_key)->is_nil()) + return _L("N/A"); + + wxString out; + + const ConfigOptionDef* opt = config.def()->get(opt_key); + bool is_nullable = opt->nullable; + + switch (opt->type) { + case coInt: + return from_u8((boost::format("%1%") % config.opt_int(opt_key)).str()); + case coInts: { + int val = is_nullable ? + config.opt(opt_key)->get_at(opt_idx) : + config.opt(opt_key)->get_at(opt_idx); + return from_u8((boost::format("%1%") % val).str()); + } + case coBool: + return config.opt_bool(opt_key) ? "true" : "false"; + case coBools: { + bool val = is_nullable ? + config.opt(opt_key)->get_at(opt_idx) : + config.opt(opt_key)->get_at(opt_idx); + return val ? "true" : "false"; + } + case coPercent: + return from_u8((boost::format("%1%%%") % int(config.optptr(opt_key)->getFloat())).str()); + case coPercents: { + double val = is_nullable ? + config.opt(opt_key)->get_at(opt_idx) : + config.opt(opt_key)->get_at(opt_idx); + return from_u8((boost::format("%1%%%") % int(val)).str()); + } + case coFloat: + return double_to_string(config.opt_float(opt_key)); + case coFloats: { + double val = is_nullable ? + config.opt(opt_key)->get_at(opt_idx) : + config.opt(opt_key)->get_at(opt_idx); + return double_to_string(val); + } + case coString: + return from_u8(config.opt_string(opt_key)); + case coStrings: { + const ConfigOptionStrings* strings = config.opt(opt_key); + if (strings) { + if (opt_key == "compatible_printers" || opt_key == "compatible_prints") { + if (strings->empty()) + return _L("All"); + for (size_t id = 0; id < strings->size(); id++) + out += from_u8(strings->get_at(id)) + "\n"; + out.RemoveLast(1); + return out; + } + if (!strings->empty()) + return from_u8(strings->get_at(opt_idx)); + } + break; + } + case coFloatOrPercent: { + const ConfigOptionFloatOrPercent* opt = config.opt(opt_key); + if (opt) + out = double_to_string(opt->value) + (opt->percent ? "%" : ""); + return out; + } + case coEnum: { + if (opt_key == "top_fill_pattern" || + opt_key == "bottom_fill_pattern" || + opt_key == "fill_pattern") + return get_string_from_enum(opt_key, config, true); + if (opt_key == "gcode_flavor") + return get_string_from_enum(opt_key, config); + if (opt_key == "ironing_type") + return get_string_from_enum(opt_key, config); + if (opt_key == "support_material_pattern") + return get_string_from_enum(opt_key, config); + if (opt_key == "seam_position") + return get_string_from_enum(opt_key, config); + if (opt_key == "display_orientation") + return get_string_from_enum(opt_key, config); + if (opt_key == "support_pillar_connection_mode") + return get_string_from_enum(opt_key, config); + break; + } + case coPoints: { + if (opt_key == "bed_shape") { + BedShape shape(*config.option(opt_key)); + return shape.get_full_name_with_params(); + } + Vec2d val = config.opt(opt_key)->get_at(opt_idx); + return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str()); + } + default: + break; + } + return out; +} + +wxString UnsavedChangesDialog::get_short_string(wxString full_string) +{ + int max_len = 30; + if (full_string.IsEmpty() || full_string.StartsWith("#") || + (full_string.Find("\n") == wxNOT_FOUND && full_string.Length() < max_len)) + return full_string; + + m_has_long_strings = true; + + int n_pos = full_string.Find("\n"); + if (n_pos != wxNOT_FOUND && n_pos < max_len) + max_len = n_pos; + + full_string.Truncate(max_len); + return full_string + dots; +} + +void UnsavedChangesDialog::update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header) +{ + PresetCollection* presets = dependent_presets; + + // activate buttons and labels + m_save_btn ->Bind(wxEVT_ENTER_WINDOW, [this, presets] (wxMouseEvent& e) { show_info_line(Action::Save, presets ? presets->get_selected_preset().name : ""); e.Skip(); }); + if (m_move_btn) { + bool is_empty_name = type != dependent_presets->type(); + m_move_btn ->Bind(wxEVT_ENTER_WINDOW, [this, new_selected_preset, is_empty_name] (wxMouseEvent& e) { show_info_line(Action::Move, is_empty_name ? "" : new_selected_preset); e.Skip(); }); + } + m_continue_btn ->Bind(wxEVT_ENTER_WINDOW, [this] (wxMouseEvent& e) { show_info_line(Action::Continue); e.Skip(); }); + + m_continue_btn->SetLabel(_L("Continue without changes")); + + if (type == Preset::TYPE_INVALID) { + m_action_line ->SetLabel(header + "\n" + _L("Next presets have the following unsaved changes:")); + m_save_btn ->SetLabel(_L("Save selected")); + } + else { + wxString action_msg; + if (type == dependent_presets->type()) { + action_msg = _L("has the following unsaved changes:"); + if (m_move_btn) + m_move_btn->SetLabel(from_u8((boost::format(_u8L("Move selected to preset: %1%")) % ("\"" + new_selected_preset + "\"")).str())); + } + else { + action_msg = type == Preset::TYPE_PRINTER ? + _L("is not compatible with printer") : + _L("is not compatible with print profile"); + action_msg += " \"" + from_u8(new_selected_preset) + "\"\n"; + action_msg += _L("and it has the following unsaved changes:"); + + if (m_move_btn) + m_move_btn->SetLabel(_L("Move selected to the first compatible preset")); + } + m_action_line->SetLabel(from_u8((boost::format(_utf8(L("Preset \"%1%\" %2%"))) % _utf8(presets->get_edited_preset().name) % action_msg).str())); + m_save_btn->SetLabel(from_u8((boost::format(_u8L("Save selected to preset: %1%")) % ("\"" + presets->get_selected_preset().name + "\"")).str())); + } + + update_tree(type, presets); +} + +void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* presets_) +{ + Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher(); + searcher.sort_options_by_opt_key(); + + // list of the presets with unsaved changes + std::vector presets_list; + if (type == Preset::TYPE_INVALID) + { + PrinterTechnology printer_technology = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology(); + + for (Tab* tab : wxGetApp().tabs_list) + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + presets_list.emplace_back(tab->get_presets()); + } + else + presets_list.emplace_back(presets_); + + // Display a dialog showing the dirty options in a human readable form. + for (PresetCollection* presets : presets_list) + { + const DynamicPrintConfig& old_config = presets->get_selected_preset().config; + const DynamicPrintConfig& new_config = presets->get_edited_preset().config; + type = presets->type(); + + m_tree_model->AddPreset(type, from_u8(presets->get_edited_preset().name)); + + // Collect dirty options. + const bool deep_compare = (type == Preset::TYPE_PRINTER || type == Preset::TYPE_SLA_MATERIAL); + auto dirty_options = presets->current_dirty_options(deep_compare); + auto dirty_options_ = presets->current_dirty_options(); + + // process changes of extruders count + if (type == Preset::TYPE_PRINTER && + old_config.opt("extruder_colour")->values.size() != new_config.opt("extruder_colour")->values.size()) { + wxString local_label = _L("Extruders count"); + wxString old_val = from_u8((boost::format("%1%") % old_config.opt("extruder_colour")->values.size()).str()); + wxString new_val = from_u8((boost::format("%1%") % new_config.opt("extruder_colour")->values.size()).str()); + + ItemData item_data = { "extruders_count", local_label, old_val, new_val, type }; + m_items_map.emplace(m_tree_model->AddOption(type, _L("General"), _L("Capabilities"), local_label, old_val, new_val), item_data); + + } + + for (const std::string& opt_key : /*presets->current_dirty_options()*/dirty_options) { + const Search::Option& option = searcher.get_option(opt_key); + + ItemData item_data = { opt_key, option.label_local, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config), type }; + + wxString old_val = get_short_string(item_data.old_val); + wxString new_val = get_short_string(item_data.new_val); + if (old_val != item_data.old_val || new_val != item_data.new_val) + item_data.is_long = true; + + m_items_map.emplace(m_tree_model->AddOption(type, option.category_local, option.group_local, option.label_local, old_val, new_val), item_data); + } + } +} + +std::vector UnsavedChangesDialog::get_unselected_options(Preset::Type type) +{ + std::vector ret; + + for (auto item : m_items_map) { + if (item.second.opt_key == "extruders_count") + continue; + if (item.second.type == type && !m_tree_model->IsEnabledItem(item.first)) + ret.emplace_back(get_pure_opt_key(item.second.opt_key)); + } + + return ret; +} + +std::vector UnsavedChangesDialog::get_selected_options() +{ + std::vector ret; + + for (auto item : m_items_map) + if (m_tree_model->IsEnabledItem(item.first)) + ret.emplace_back(get_pure_opt_key(item.second.opt_key)); + + return ret; +} + +void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + int em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_CANCEL, m_save_btn_id, m_move_btn_id, m_continue_btn_id }); + for (auto btn : { m_save_btn, m_move_btn, m_continue_btn } ) + btn->msw_rescale(); + + const wxSize& size = wxSize(80 * em, 30 * em); + SetMinSize(size); + + m_tree->GetColumn(UnsavedChangesModel::colToggle )->SetWidth(6 * em); + m_tree->GetColumn(UnsavedChangesModel::colIconText)->SetWidth(30 * em); + m_tree->GetColumn(UnsavedChangesModel::colOldValue)->SetWidth(20 * em); + m_tree->GetColumn(UnsavedChangesModel::colNewValue)->SetWidth(20 * em); + + m_tree_model->Rescale(); + m_tree->Refresh(); + + Fit(); + Refresh(); +} + +void UnsavedChangesDialog::on_sys_color_changed() +{ + for (auto btn : { m_save_btn, m_move_btn, m_continue_btn } ) + btn->msw_rescale(); + // msw_rescale updates just icons, so use it + m_tree_model->Rescale(); + m_tree->Refresh(); + + Refresh(); +} + + +//------------------------------------------ +// FullCompareDialog +//------------------------------------------ + +FullCompareDialog::FullCompareDialog(const wxString& option_name, const wxString& old_value, const wxString& new_value) + : wxDialog(nullptr, wxID_ANY, option_name, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + wxColour bgr_clr = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + SetBackgroundColour(bgr_clr); + + int border = 10; + + wxStaticBoxSizer* sizer = new wxStaticBoxSizer(wxVERTICAL, this); + + wxFlexGridSizer* grid_sizer = new wxFlexGridSizer(2, 2, 1, 0); + grid_sizer->SetFlexibleDirection(wxBOTH); + grid_sizer->AddGrowableCol(0,1); + grid_sizer->AddGrowableCol(1,1); + grid_sizer->AddGrowableRow(1,1); + + auto add_header = [grid_sizer, border, this](wxString label) { + wxStaticText* text = new wxStaticText(this, wxID_ANY, label); + text->SetFont(this->GetFont().Bold()); + grid_sizer->Add(text, 0, wxALL, border); + }; + + auto add_value = [grid_sizer, border, this](wxString label, bool is_colored = false) { + wxTextCtrl* text = new wxTextCtrl(this, wxID_ANY, label, wxDefaultPosition, wxSize(300, -1), wxTE_MULTILINE | wxTE_READONLY | wxBORDER_NONE | wxTE_RICH); + text->SetStyle(0, label.Len(), wxTextAttr(is_colored ? wxColour(orange) : wxNullColour, wxNullColour, this->GetFont())); + grid_sizer->Add(text, 1, wxALL | wxEXPAND, border); + }; + + add_header(_L("Old value")); + add_header(_L("New value")); + add_value(old_value); + add_value(new_value, true); + + sizer->Add(grid_sizer, 1, wxEXPAND); + + wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK); + + wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); + + topSizer->Add(sizer, 1, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(buttons, 0, wxEXPAND | wxALL, border); + + SetSizer(topSizer); + topSizer->SetSizeHints(this); +} + + +} + +} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp new file mode 100644 index 0000000000..f78a1fec0e --- /dev/null +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -0,0 +1,272 @@ +#ifndef slic3r_UnsavedChangesDialog_hpp_ +#define slic3r_UnsavedChangesDialog_hpp_ + +#include +#include +#include + +#include "GUI_Utils.hpp" +#include "wxExtensions.hpp" +#include "libslic3r/Preset.hpp" + +class ScalableButton; +class wxStaticText; + +namespace Slic3r { +namespace GUI{ + +// ---------------------------------------------------------------------------- +// ModelNode: a node inside UnsavedChangesModel +// ---------------------------------------------------------------------------- + +class ModelNode; +WX_DEFINE_ARRAY_PTR(ModelNode*, ModelNodePtrArray); + +// On all of 3 different platforms Bitmap+Text icon column looks different +// because of Markup text is missed or not implemented. +// As a temporary workaround, we will use: +// MSW - DataViewBitmapText (our custom renderer wxBitmap + wxString, supported Markup text) +// OSX - -//-, but Markup text is not implemented right now +// GTK - wxDataViewIconText (wxWidgets for GTK renderer wxIcon + wxString, supported Markup text) +class ModelNode +{ + wxWindow* m_parent_win{ nullptr }; + + ModelNode* m_parent; + ModelNodePtrArray m_children; + wxBitmap m_empty_bmp; + Preset::Type m_preset_type {Preset::TYPE_INVALID}; + + std::string m_icon_name; + // saved values for colors if they exist + wxString m_old_color; + wxString m_new_color; + + // TODO/FIXME: + // the GTK version of wxDVC (in particular wxDataViewCtrlInternal::ItemAdded) + // needs to know in advance if a node is or _will be_ a container. + // Thus implementing: + // bool IsContainer() const + // { return m_children.GetCount()>0; } + // doesn't work with wxGTK when UnsavedChangesModel::AddToClassical is called + // AND the classical node was removed (a new node temporary without children + // would be added to the control) + bool m_container {true}; + +#ifdef __linux__ + wxIcon get_bitmap(const wxString& color); +#else + wxBitmap get_bitmap(const wxString& color); +#endif //__linux__ + +public: + + bool m_toggle {true}; +#ifdef __linux__ + wxIcon m_icon; + wxIcon m_old_color_bmp; + wxIcon m_new_color_bmp; +#else + wxBitmap m_icon; + wxBitmap m_old_color_bmp; + wxBitmap m_new_color_bmp; +#endif //__linux__ + wxString m_text; + wxString m_old_value; + wxString m_new_value; + + // preset(root) node + ModelNode(Preset::Type preset_type, const wxString& text, wxWindow* parent_win); + + // category node + ModelNode(ModelNode* parent, const wxString& text, const std::string& icon_name); + + // group node + ModelNode(ModelNode* parent, const wxString& text); + + // option node + ModelNode(ModelNode* parent, const wxString& text, const wxString& old_value, const wxString& new_value); + + ~ModelNode() { + // free all our children nodes + size_t count = m_children.GetCount(); + for (size_t i = 0; i < count; i++) { + ModelNode* child = m_children[i]; + delete child; + } + } + + bool IsContainer() const { return m_container; } + bool IsToggled() const { return m_toggle; } + void Toggle(bool toggle = true) { m_toggle = toggle; } + bool IsRoot() const { return m_parent == nullptr; } + Preset::Type type() const { return m_preset_type; } + const wxString& text() const { return m_text; } + + ModelNode* GetParent() { return m_parent; } + ModelNodePtrArray& GetChildren() { return m_children; } + ModelNode* GetNthChild(unsigned int n) { return m_children.Item(n); } + unsigned int GetChildCount() const { return m_children.GetCount(); } + + void Insert(ModelNode* child, unsigned int n) { m_children.Insert(child, n); } + void Append(ModelNode* child) { m_children.Add(child); } + + void UpdateEnabling(); + void UpdateIcons(); +}; + + +// ---------------------------------------------------------------------------- +// UnsavedChangesModel +// ---------------------------------------------------------------------------- + +class UnsavedChangesModel : public wxDataViewModel +{ + wxWindow* m_parent_win { nullptr }; + std::vector m_preset_nodes; + + wxDataViewCtrl* m_ctrl{ nullptr }; + + ModelNode *AddOption(ModelNode *group_node, + wxString option_name, + wxString old_value, + wxString new_value); + ModelNode *AddOptionWithGroup(ModelNode *category_node, + wxString group_name, + wxString option_name, + wxString old_value, + wxString new_value); + ModelNode *AddOptionWithGroupAndCategory(ModelNode *preset_node, + wxString category_name, + wxString group_name, + wxString option_name, + wxString old_value, + wxString new_value); + +public: + enum { + colToggle, + colIconText, + colOldValue, + colNewValue, + colMax + }; + + UnsavedChangesModel(wxWindow* parent); + ~UnsavedChangesModel(); + + void SetAssociatedControl(wxDataViewCtrl* ctrl) { m_ctrl = ctrl; } + + wxDataViewItem AddPreset(Preset::Type type, wxString preset_name); + wxDataViewItem AddOption(Preset::Type type, wxString category_name, wxString group_name, wxString option_name, + wxString old_value, wxString new_value); + + void UpdateItemEnabling(wxDataViewItem item); + bool IsEnabledItem(const wxDataViewItem& item); + + unsigned int GetColumnCount() const override { return colMax; } + wxString GetColumnType(unsigned int col) const override; + void Rescale(); + + wxDataViewItem GetParent(const wxDataViewItem& item) const override; + unsigned int GetChildren(const wxDataViewItem& parent, wxDataViewItemArray& array) const override; + + void GetValue(wxVariant& variant, const wxDataViewItem& item, unsigned int col) const override; + bool SetValue(const wxVariant& variant, const wxDataViewItem& item, unsigned int col) override; + + bool IsEnabled(const wxDataViewItem& item, unsigned int col) const override; + bool IsContainer(const wxDataViewItem& item) const override; + // Is the container just a header or an item with all columns + // In our case it is an item with all columns + bool HasContainerColumns(const wxDataViewItem& WXUNUSED(item)) const override { return true; } +}; + + +//------------------------------------------ +// UnsavedChangesDialog +//------------------------------------------ +class UnsavedChangesDialog : public DPIDialog +{ + wxDataViewCtrl* m_tree { nullptr }; + UnsavedChangesModel* m_tree_model { nullptr }; + + ScalableButton* m_save_btn { nullptr }; + ScalableButton* m_move_btn { nullptr }; + ScalableButton* m_continue_btn { nullptr }; + wxStaticText* m_action_line { nullptr }; + wxStaticText* m_info_line { nullptr }; + + bool m_empty_selection { false }; + bool m_has_long_strings { false }; + int m_save_btn_id { wxID_ANY }; + int m_move_btn_id { wxID_ANY }; + int m_continue_btn_id { wxID_ANY }; + + enum class Action { + Undef, + Save, + Move, + Continue + }; + + // selected action after Dialog closing + Action m_exit_action {Action::Undef}; + + // Action during mouse motion + Action m_motion_action {Action::Undef}; + + struct ItemData + { + std::string opt_key; + wxString opt_name; + wxString old_val; + wxString new_val; + Preset::Type type; + bool is_long {false}; + }; + // tree items related to the options + std::map m_items_map; + +public: + UnsavedChangesDialog(const wxString& header); + UnsavedChangesDialog(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset); + ~UnsavedChangesDialog() {} + + wxString get_short_string(wxString full_string); + + void build(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header = ""); + void update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header); + void update_tree(Preset::Type type, PresetCollection *presets); + void item_value_changed(wxDataViewEvent &event); + void context_menu(wxDataViewEvent &event); + void show_info_line(Action action, std::string preset_name = ""); + void close(Action action); + + bool save_preset() const { return m_exit_action == Action::Save; } + bool move_preset() const { return m_exit_action == Action::Move; } + bool just_continue() const { return m_exit_action == Action::Continue; } + + std::vector get_unselected_options(Preset::Type type); + std::vector get_selected_options(); + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + void on_sys_color_changed() override; +}; + + +//------------------------------------------ +// FullCompareDialog +//------------------------------------------ +class FullCompareDialog : public wxDialog +{ +public: + FullCompareDialog(const wxString& option_name, const wxString& old_value, const wxString& new_value); + ~FullCompareDialog() {} +}; + + +} +} + +#endif //slic3r_UnsavedChangesDialog_hpp_ diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index ad9f0a121e..6f79db5916 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -174,7 +174,6 @@ wxMenuItem* append_menu_check_item(wxMenu* menu, int id, const wxString& string, const unsigned int wxCheckListBoxComboPopup::DefaultWidth = 200; const unsigned int wxCheckListBoxComboPopup::DefaultHeight = 200; -const unsigned int wxCheckListBoxComboPopup::DefaultItemHeight = 18; bool wxCheckListBoxComboPopup::Create(wxWindow* parent) { @@ -198,17 +197,22 @@ wxString wxCheckListBoxComboPopup::GetStringValue() const wxSize wxCheckListBoxComboPopup::GetAdjustedSize(int minWidth, int prefHeight, int maxHeight) { - // matches owner wxComboCtrl's width - // and sets height dinamically in dependence of contained items count + // set width dinamically in dependence of items text + // and set height dinamically in dependence of items count wxComboCtrl* cmb = GetComboCtrl(); - if (cmb != nullptr) - { + if (cmb != nullptr) { wxSize size = GetComboCtrl()->GetSize(); unsigned int count = GetCount(); - if (count > 0) - size.SetHeight(count * DefaultItemHeight); + if (count > 0) { + int max_width = size.x; + for (unsigned int i = 0; i < count; ++i) { + max_width = std::max(max_width, 60 + GetTextExtent(GetString(i)).x); + } + size.SetWidth(max_width); + size.SetHeight(count * cmb->GetCharHeight()); + } else size.SetHeight(DefaultHeight); @@ -300,94 +304,6 @@ void wxCheckListBoxComboPopup::OnListBoxSelection(wxCommandEvent& evt) } -namespace Slic3r { -namespace GUI { - -// *** PresetBitmapComboBox *** - -/* For PresetBitmapComboBox we use bitmaps that are created from images that are already scaled appropriately for Retina - * (Contrary to the intuition, the `scale` argument for Bitmap's constructor doesn't mean - * "please scale this to such and such" but rather - * "the wxImage is already sized for backing scale such and such". ) - * Unfortunately, the constructor changes the size of wxBitmap too. - * Thus We need to use unscaled size value for bitmaps that we use - * to avoid scaled size of control items. - * For this purpose control drawing methods and - * control size calculation methods (virtual) are overridden. - **/ - -PresetBitmapComboBox::PresetBitmapComboBox(wxWindow* parent, const wxSize& size) : - wxBitmapComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, 0, nullptr, wxCB_READONLY) -{} - -#ifdef __APPLE__ -bool PresetBitmapComboBox::OnAddBitmap(const wxBitmap& bitmap) -{ - if (bitmap.IsOk()) - { - // we should use scaled! size values of bitmap - int width = (int)bitmap.GetScaledWidth(); - int height = (int)bitmap.GetScaledHeight(); - - if (m_usedImgSize.x < 0) - { - // If size not yet determined, get it from this image. - m_usedImgSize.x = width; - m_usedImgSize.y = height; - - // Adjust control size to vertically fit the bitmap - wxWindow* ctrl = GetControl(); - ctrl->InvalidateBestSize(); - wxSize newSz = ctrl->GetBestSize(); - wxSize sz = ctrl->GetSize(); - if (newSz.y > sz.y) - ctrl->SetSize(sz.x, newSz.y); - else - DetermineIndent(); - } - - wxCHECK_MSG(width == m_usedImgSize.x && height == m_usedImgSize.y, - false, - "you can only add images of same size"); - - return true; - } - - return false; -} - -void PresetBitmapComboBox::OnDrawItem(wxDC& dc, - const wxRect& rect, - int item, - int flags) const -{ - const wxBitmap& bmp = *(wxBitmap*)m_bitmaps[item]; - if (bmp.IsOk()) - { - // we should use scaled! size values of bitmap - wxCoord w = bmp.GetScaledWidth(); - wxCoord h = bmp.GetScaledHeight(); - - const int imgSpacingLeft = 4; - - // Draw the image centered - dc.DrawBitmap(bmp, - rect.x + (m_usedImgSize.x - w) / 2 + imgSpacingLeft, - rect.y + (rect.height - h) / 2, - true); - } - - wxString text = GetString(item); - if (!text.empty()) - dc.DrawText(text, - rect.x + m_imgAreaWidth + 1, - rect.y + (rect.height - dc.GetCharHeight()) / 2); -} -#endif -} -} - - // *** wxDataViewTreeCtrlComboPopup *** const unsigned int wxDataViewTreeCtrlComboPopup::DefaultWidth = 270; @@ -520,7 +436,7 @@ wxBitmap create_scaled_bitmap( const std::string& bmp_name_in, if (bmp == nullptr) { // Neither SVG nor PNG has been found, raise error - throw std::runtime_error("Could not load bitmap: " + bmp_name); + throw Slic3r::RuntimeError("Could not load bitmap: " + bmp_name); } return *bmp; @@ -819,11 +735,12 @@ void MenuWithSeparators::SetSecondSeparator() // ---------------------------------------------------------------------------- ScalableBitmap::ScalableBitmap( wxWindow *parent, const std::string& icon_name/* = ""*/, - const int px_cnt/* = 16*/): + const int px_cnt/* = 16*/, + const bool grayscale/* = false*/): m_parent(parent), m_icon_name(icon_name), m_px_cnt(px_cnt) { - m_bmp = create_scaled_bitmap(icon_name, parent, px_cnt); + m_bmp = create_scaled_bitmap(icon_name, parent, px_cnt, grayscale); } wxSize ScalableBitmap::GetBmpSize() const @@ -856,7 +773,7 @@ int ScalableBitmap::GetBmpHeight() const void ScalableBitmap::msw_rescale() { - m_bmp = create_scaled_bitmap(m_icon_name, m_parent, m_px_cnt); + m_bmp = create_scaled_bitmap(m_icon_name, m_parent, m_px_cnt, m_grayscale); } // ---------------------------------------------------------------------------- @@ -869,9 +786,11 @@ ScalableButton::ScalableButton( wxWindow * parent, const wxString& label /* = wxEmptyString*/, const wxSize& size /* = wxDefaultSize*/, const wxPoint& pos /* = wxDefaultPosition*/, - long style /*= wxBU_EXACTFIT | wxNO_BORDER*/) : + long style /*= wxBU_EXACTFIT | wxNO_BORDER*/, + bool use_default_disabled_bitmap/* = false*/) : + m_parent(parent), m_current_icon_name(icon_name), - m_parent(parent) + m_use_default_disabled_bitmap (use_default_disabled_bitmap) { Create(parent, id, label, pos, size, style); #ifdef __WXMSW__ @@ -880,6 +799,8 @@ ScalableButton::ScalableButton( wxWindow * parent, #endif // __WXMSW__ SetBitmap(create_scaled_bitmap(icon_name, parent)); + if (m_use_default_disabled_bitmap) + SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); if (size != wxDefaultSize) { @@ -929,11 +850,19 @@ int ScalableButton::GetBitmapHeight() #endif } +void ScalableButton::UseDefaultBitmapDisabled() +{ + m_use_default_disabled_bitmap = true; + SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); +} + void ScalableButton::msw_rescale() { SetBitmap(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt)); if (!m_disabled_icon_name.empty()) SetBitmapDisabled(create_scaled_bitmap(m_disabled_icon_name, m_parent, m_px_cnt)); + else if (m_use_default_disabled_bitmap) + SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); if (m_width > 0 || m_height>0) { diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 569257e1b4..4f04bc5b20 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -63,7 +63,6 @@ class wxCheckListBoxComboPopup : public wxCheckListBox, public wxComboPopup { static const unsigned int DefaultWidth; static const unsigned int DefaultHeight; - static const unsigned int DefaultItemHeight; wxString m_text; @@ -95,37 +94,6 @@ public: void OnListBoxSelection(wxCommandEvent& evt); }; -namespace Slic3r { -namespace GUI { -// *** PresetBitmapComboBox *** - -// BitmapComboBox used to presets list on Sidebar and Tabs -class PresetBitmapComboBox: public wxBitmapComboBox -{ -public: - PresetBitmapComboBox(wxWindow* parent, const wxSize& size = wxDefaultSize); - ~PresetBitmapComboBox() {} - -#ifdef __APPLE__ -protected: - /* For PresetBitmapComboBox we use bitmaps that are created from images that are already scaled appropriately for Retina - * (Contrary to the intuition, the `scale` argument for Bitmap's constructor doesn't mean - * "please scale this to such and such" but rather - * "the wxImage is already sized for backing scale such and such". ) - * Unfortunately, the constructor changes the size of wxBitmap too. - * Thus We need to use unscaled size value for bitmaps that we use - * to avoid scaled size of control items. - * For this purpose control drawing methods and - * control size calculation methods (virtual) are overridden. - **/ - virtual bool OnAddBitmap(const wxBitmap& bitmap) override; - virtual void OnDrawItem(wxDC& dc, const wxRect& rect, int item, int flags) const override; -#endif -}; - -} -} - // *** wxDataViewTreeCtrlComboBox *** @@ -161,7 +129,8 @@ public: ScalableBitmap() {}; ScalableBitmap( wxWindow *parent, const std::string& icon_name = "", - const int px_cnt = 16); + const int px_cnt = 16, + const bool grayscale = false); ~ScalableBitmap() {} @@ -182,6 +151,7 @@ private: wxBitmap m_bmp = wxBitmap(); std::string m_icon_name = ""; int m_px_cnt {16}; + bool m_grayscale {false}; }; @@ -239,7 +209,8 @@ public: const wxString& label = wxEmptyString, const wxSize& size = wxDefaultSize, const wxPoint& pos = wxDefaultPosition, - long style = wxBU_EXACTFIT | wxNO_BORDER); + long style = wxBU_EXACTFIT | wxNO_BORDER, + bool use_default_disabled_bitmap = false); ScalableButton( wxWindow * parent, @@ -253,6 +224,7 @@ public: void SetBitmap_(const ScalableBitmap& bmp); void SetBitmapDisabled_(const ScalableBitmap &bmp); int GetBitmapHeight(); + void UseDefaultBitmapDisabled(); void msw_rescale(); @@ -263,6 +235,8 @@ private: int m_width {-1}; // should be multiplied to em_unit int m_height{-1}; // should be multiplied to em_unit + bool m_use_default_disabled_bitmap {false}; + // bitmap dimensions int m_px_cnt{ 16 }; }; @@ -364,7 +338,7 @@ class BlinkingBitmap : public wxStaticBitmap { public: BlinkingBitmap() {}; - BlinkingBitmap(wxWindow* parent, const std::string& icon_name = "redo_toolbar"); + BlinkingBitmap(wxWindow* parent, const std::string& icon_name = "search_blink"); ~BlinkingBitmap() {} diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index 8de4991d85..bcab6daaf8 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -30,10 +30,10 @@ #include "libslic3r/Model.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/PresetBundle.hpp" #include "libslic3r/Format/3mf.hpp" #include "../GUI/GUI.hpp" #include "../GUI/I18N.hpp" -#include "../GUI/PresetBundle.hpp" #include #include @@ -209,10 +209,10 @@ typedef std::functionGetResults(model.GetAddressOf()); else - throw std::runtime_error(L("Failed loading the input model.")); + throw Slic3r::RuntimeError(L("Failed loading the input model.")); Microsoft::WRL::ComPtr> meshes; hr = model->get_Meshes(meshes.GetAddressOf()); @@ -245,7 +245,7 @@ void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path hr = model->RepairAsync(repairAsync.GetAddressOf()); status = winrt_async_await(repairAsync, throw_on_cancel); if (status != AsyncStatus::Completed) - throw std::runtime_error(L("Mesh repair failed.")); + throw Slic3r::RuntimeError(L("Mesh repair failed.")); repairAsync->GetResults(); on_progress(L("Loading repaired model"), 60); @@ -260,14 +260,14 @@ void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path hr = printing3d3mfpackage->SaveModelToPackageAsync(model.Get(), saveToPackageAsync.GetAddressOf()); status = winrt_async_await(saveToPackageAsync, throw_on_cancel); if (status != AsyncStatus::Completed) - throw std::runtime_error(L("Saving mesh into the 3MF container failed.")); + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); hr = saveToPackageAsync->GetResults(); Microsoft::WRL::ComPtr> generatorStreamAsync; hr = printing3d3mfpackage->SaveAsync(generatorStreamAsync.GetAddressOf()); status = winrt_async_await(generatorStreamAsync, throw_on_cancel); if (status != AsyncStatus::Completed) - throw std::runtime_error(L("Saving mesh into the 3MF container failed.")); + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); Microsoft::WRL::ComPtr generatorStream; hr = generatorStreamAsync->GetResults(generatorStream.GetAddressOf()); @@ -299,7 +299,7 @@ void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path hr = inputStream->ReadAsync(buffer.Get(), 65536 * 2048, ABI::Windows::Storage::Streams::InputStreamOptions_ReadAhead, asyncRead.GetAddressOf()); status = winrt_async_await(asyncRead, throw_on_cancel); if (status != AsyncStatus::Completed) - throw std::runtime_error(L("Saving mesh into the 3MF container failed.")); + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); hr = buffer->get_Length(&length); if (length == 0) break; @@ -365,7 +365,7 @@ void fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx) model_object->add_instance(); if (!Slic3r::store_3mf(path_src.string().c_str(), &model, nullptr, false)) { boost::filesystem::remove(path_src); - throw std::runtime_error(L("Export of a temporary 3mf file failed")); + throw Slic3r::RuntimeError(L("Export of a temporary 3mf file failed")); } model.clear_objects(); model.clear_materials(); @@ -380,15 +380,15 @@ void fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx) bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), &config, &model, false); boost::filesystem::remove(path_dst); if (! loaded) - throw std::runtime_error(L("Import of the repaired 3mf file failed")); + throw Slic3r::RuntimeError(L("Import of the repaired 3mf file failed")); if (model.objects.size() == 0) - throw std::runtime_error(L("Repaired 3MF file does not contain any object")); + throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any object")); if (model.objects.size() > 1) - throw std::runtime_error(L("Repaired 3MF file contains more than one object")); + throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one object")); if (model.objects.front()->volumes.size() == 0) - throw std::runtime_error(L("Repaired 3MF file does not contain any volume")); + throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any volume")); if (model.objects.front()->volumes.size() > 1) - throw std::runtime_error(L("Repaired 3MF file contains more than one volume")); + throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one volume")); meshes_repaired.emplace_back(std::move(model.objects.front()->volumes.front()->mesh())); } for (size_t i = 0; i < volumes.size(); ++ i) { diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index a16aac5b5b..8c79a478a1 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -156,7 +156,7 @@ Http::priv::priv(const std::string &url) Http::tls_global_init(); if (curl == nullptr) { - throw std::runtime_error(std::string("Could not construct Curl object")); + throw Slic3r::RuntimeError(std::string("Could not construct Curl object")); } set_timeout_connect(DEFAULT_TIMEOUT_CONNECT); @@ -415,6 +415,16 @@ Http& Http::remove_header(std::string name) return *this; } +// Authorization by HTTP digest, based on RFC2617. +Http& Http::auth_digest(const std::string &user, const std::string &password) +{ + curl_easy_setopt(p->curl, CURLOPT_USERNAME, user.c_str()); + curl_easy_setopt(p->curl, CURLOPT_PASSWORD, password.c_str()); + curl_easy_setopt(p->curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + + return *this; +} + Http& Http::ca_file(const std::string &name) { if (p && priv::ca_file_supported(p->curl)) { diff --git a/src/slic3r/Utils/Http.hpp b/src/slic3r/Utils/Http.hpp index f162362796..ae3660fbfe 100644 --- a/src/slic3r/Utils/Http.hpp +++ b/src/slic3r/Utils/Http.hpp @@ -64,6 +64,8 @@ public: Http& header(std::string name, const std::string &value); // Removes a header field. Http& remove_header(std::string name); + // Authorization by HTTP digest, based on RFC2617. + Http& auth_digest(const std::string &user, const std::string &password); // Sets a CA certificate file for usage with HTTPS. This is only supported on some backends, // specifically, this is supported with OpenSSL and NOT supported with Windows and OS X native certificate store. // See also ca_file_supported(). diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index ee87f822fa..fad45f8225 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -11,7 +11,6 @@ #include -#include "libslic3r/PrintConfig.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI.hpp" #include "Http.hpp" @@ -170,6 +169,13 @@ std::string OctoPrint::make_url(const std::string &path) const } } +SL1Host::SL1Host(DynamicPrintConfig *config) : + OctoPrint(config), + authorization_type(dynamic_cast*>(config->option("printhost_authorization_type"))->value), + username(config->opt_string("printhost_user")), + password(config->opt_string("printhost_password")) +{ +} // SL1Host const char* SL1Host::get_name() const { return "SL1Host"; } @@ -191,4 +197,20 @@ bool SL1Host::validate_version_text(const boost::optional &version_ return version_text ? boost::starts_with(*version_text, "Prusa SLA") : false; } +void SL1Host::set_auth(Http &http) const +{ + switch (authorization_type) { + case atKeyPassword: + http.header("X-Api-Key", get_apikey()); + break; + case atUserPassword: + http.auth_digest(username, password); + break; + } + + if (! get_cafile().empty()) { + http.ca_file(get_cafile()); + } +} + } diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp index 965019d859..ed1c61bd60 100644 --- a/src/slic3r/Utils/OctoPrint.hpp +++ b/src/slic3r/Utils/OctoPrint.hpp @@ -6,6 +6,7 @@ #include #include "PrintHost.hpp" +#include "libslic3r/PrintConfig.hpp" namespace Slic3r { @@ -29,6 +30,8 @@ public: bool can_test() const override { return true; } bool can_start_print() const override { return true; } std::string get_host() const override { return host; } + const std::string& get_apikey() const { return apikey; } + const std::string& get_cafile() const { return cafile; } protected: virtual bool validate_version_text(const boost::optional &version_text) const; @@ -38,14 +41,14 @@ private: std::string apikey; std::string cafile; - void set_auth(Http &http) const; + virtual void set_auth(Http &http) const; std::string make_url(const std::string &path) const; }; class SL1Host: public OctoPrint { public: - SL1Host(DynamicPrintConfig *config) : OctoPrint(config) {} + SL1Host(DynamicPrintConfig *config); ~SL1Host() override = default; const char* get_name() const override; @@ -56,6 +59,15 @@ public: protected: bool validate_version_text(const boost::optional &version_text) const override; + +private: + void set_auth(Http &http) const override; + + // Host authorization type. + AuthorizationType authorization_type; + // username and password for HTTP Digest Authentization (RFC RFC2617) + std::string username; + std::string password; }; } diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index c32613c468..14b9fb0c4a 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -19,14 +19,15 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/format.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" -#include "slic3r/GUI/PresetBundle.hpp" #include "slic3r/GUI/UpdateDialogs.hpp" #include "slic3r/GUI/ConfigWizard.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/Utils/Http.hpp" #include "slic3r/Config/Version.hpp" #include "slic3r/Config/Snapshot.hpp" @@ -154,6 +155,9 @@ struct PresetUpdater::priv bool cancel; std::thread thread; + bool has_waiting_updates { false }; + Updates waiting_updates; + priv(); void set_download_prefs(AppConfig *app_config); @@ -165,6 +169,7 @@ struct PresetUpdater::priv void check_install_indices() const; Updates get_config_updates(const Semver& old_slic3r_version) const; void perform_updates(Updates &&updates, bool snapshot = true) const; + void set_waiting_updates(Updates u); }; PresetUpdater::priv::priv() @@ -326,7 +331,15 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors) continue; } Slic3r::rename_file(idx_path_temp, idx_path); - index = std::move(new_index); + //if we rename path we need to change it in Index object too or create the object again + //index = std::move(new_index); + try { + index.load(idx_path); + } + catch (const std::exception& /* err */) { + BOOST_LOG_TRIVIAL(error) << format("Could not load downloaded index %1% for vendor %2%: invalid index?", idx_path, vendor.name); + continue; + } if (cancel) return; } @@ -632,6 +645,12 @@ void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons } } +void PresetUpdater::priv::set_waiting_updates(Updates u) +{ + waiting_updates = u; + has_waiting_updates = true; +} + PresetUpdater::PresetUpdater() : p(new priv()) {} @@ -690,9 +709,9 @@ void PresetUpdater::slic3r_update_notify() } } -PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver &old_slic3r_version) const +PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, bool no_notification) const { - if (! p->enabled_config_update) { return R_NOOP; } + if (! p->enabled_config_update) { return R_NOOP; } auto updates = p->get_config_updates(old_slic3r_version); if (updates.incompats.size() > 0) { @@ -779,30 +798,38 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver &old_slic3 } // regular update - BOOST_LOG_TRIVIAL(info) << format("Update of %1% bundles available. Asking for confirmation ...", updates.updates.size()); + if (no_notification) { + BOOST_LOG_TRIVIAL(info) << format("Update of %1% bundles available. Asking for confirmation ...", p->waiting_updates.updates.size()); - std::vector updates_msg; - for (const auto &update : updates.updates) { - std::string changelog_url = update.version.config_version.prerelease() == nullptr ? update.changelog_url : std::string(); - updates_msg.emplace_back(update.vendor, update.version.config_version, update.version.comment, std::move(changelog_url)); - } + std::vector updates_msg; + for (const auto& update : updates.updates) { + std::string changelog_url = update.version.config_version.prerelease() == nullptr ? update.changelog_url : std::string(); + updates_msg.emplace_back(update.vendor, update.version.config_version, update.version.comment, std::move(changelog_url)); + } - GUI::MsgUpdateConfig dlg(updates_msg); + GUI::MsgUpdateConfig dlg(updates_msg); - const auto res = dlg.ShowModal(); - if (res == wxID_OK) { - BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; - p->perform_updates(std::move(updates)); + const auto res = dlg.ShowModal(); + if (res == wxID_OK) { + BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; + p->perform_updates(std::move(updates)); - // Reload global configuration - auto *app_config = GUI::wxGetApp().app_config; - GUI::wxGetApp().preset_bundle->load_presets(*app_config); - GUI::wxGetApp().load_current_presets(); - return R_UPDATE_INSTALLED; + // Reload global configuration + auto* app_config = GUI::wxGetApp().app_config; + GUI::wxGetApp().preset_bundle->load_presets(*app_config); + GUI::wxGetApp().load_current_presets(); + return R_UPDATE_INSTALLED; + } + else { + BOOST_LOG_TRIVIAL(info) << "User refused the update"; + return R_UPDATE_REJECT; + } } else { - BOOST_LOG_TRIVIAL(info) << "User refused the update"; - return R_UPDATE_REJECT; + p->set_waiting_updates(updates); + GUI::wxGetApp().plater()->get_notification_manager()->push_notification(GUI::NotificationType::PresetUpdateAviable, *(GUI::wxGetApp().plater()->get_current_canvas3D())); } + + // MsgUpdateConfig will show after the notificaation is clicked } else { BOOST_LOG_TRIVIAL(info) << "No configuration updates available."; } @@ -825,5 +852,37 @@ void PresetUpdater::install_bundles_rsrc(std::vector bundles, bool p->perform_updates(std::move(updates), snapshot); } +void PresetUpdater::on_update_notification_confirm() +{ + if (!p->has_waiting_updates) + return; + BOOST_LOG_TRIVIAL(info) << format("Update of %1% bundles available. Asking for confirmation ...", p->waiting_updates.updates.size()); + + std::vector updates_msg; + for (const auto& update : p->waiting_updates.updates) { + std::string changelog_url = update.version.config_version.prerelease() == nullptr ? update.changelog_url : std::string(); + updates_msg.emplace_back(update.vendor, update.version.config_version, update.version.comment, std::move(changelog_url)); + } + + GUI::MsgUpdateConfig dlg(updates_msg); + + const auto res = dlg.ShowModal(); + if (res == wxID_OK) { + BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update"; + p->perform_updates(std::move(p->waiting_updates)); + + // Reload global configuration + auto* app_config = GUI::wxGetApp().app_config; + GUI::wxGetApp().preset_bundle->load_presets(*app_config); + GUI::wxGetApp().load_current_presets(); + p->has_waiting_updates = false; + //return R_UPDATE_INSTALLED; + } + else { + BOOST_LOG_TRIVIAL(info) << "User refused the update"; + //return R_UPDATE_REJECT; + } + +} } diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index e186958284..0ca363c613 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -35,16 +35,20 @@ public: R_INCOMPAT_CONFIGURED, R_UPDATE_INSTALLED, R_UPDATE_REJECT, + R_UPDATE_NOTIFICATION }; // If updating is enabled, check if updates are available in cache, if so, ask about installation. // A false return value implies Slic3r should exit due to incompatibility of configuration. // Providing old slic3r version upgrade profiles on upgrade of an application even in case // that the config index installed from the Internet is equal to the index contained in the installation package. - UpdateResult config_update(const Semver &old_slic3r_version) const; + // no_notification = force modal textbox, otherwise some cases only shows notification + UpdateResult config_update(const Semver &old_slic3r_version, bool no_notification) const; // "Update" a list of bundles from resources (behaves like an online update). void install_bundles_rsrc(std::vector bundles, bool snapshot = true) const; + + void on_update_notification_confirm(); private: struct priv; std::unique_ptr p; diff --git a/src/slic3r/Utils/Process.cpp b/src/slic3r/Utils/Process.cpp new file mode 100644 index 0000000000..375c10a6a5 --- /dev/null +++ b/src/slic3r/Utils/Process.cpp @@ -0,0 +1,131 @@ +#include "Process.hpp" + +#include + +#include "../GUI/GUI.hpp" +// for file_wildcards() +#include "../GUI/GUI_App.hpp" +// localization +#include "../GUI/I18N.hpp" + +#include +#include + +#include +#include + +// For starting another PrusaSlicer instance on OSX. +// Fails to compile on Windows on the build server. +#ifdef __APPLE__ + #include + #include +#endif + +#include + +namespace Slic3r { +namespace GUI { + +enum class NewSlicerInstanceType { + Slicer, + GCodeViewer +}; + +// Start a new Slicer process instance either in a Slicer mode or in a G-code mode. +// Optionally load a 3MF, STL or a G-code on start. +static void start_new_slicer_or_gcodeviewer(const NewSlicerInstanceType instance_type, const wxString *path_to_open) +{ +#ifdef _WIN32 + wxString path; + wxFileName::SplitPath(wxStandardPaths::Get().GetExecutablePath(), &path, nullptr, nullptr, wxPATH_NATIVE); + path += "\\"; + path += (instance_type == NewSlicerInstanceType::Slicer) ? "prusa-slicer.exe" : "prusa-gcodeviewer.exe"; + std::vector args; + args.reserve(3); + args.emplace_back(path.wc_str()); + if (path_to_open != nullptr) + args.emplace_back(path_to_open->wc_str()); + args.emplace_back(nullptr); + BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << into_u8(path) << "\""; + // Don't call with wxEXEC_HIDE_CONSOLE, PrusaSlicer in GUI mode would just show the splash screen. It would not open the main window though, it would + // just hang in the background. + if (wxExecute(const_cast(args.data()), wxEXEC_ASYNC) <= 0) + BOOST_LOG_TRIVIAL(error) << "Failed to spawn a new slicer \"" << into_u8(path); +#else + // Own executable path. + boost::filesystem::path bin_path = into_path(wxStandardPaths::Get().GetExecutablePath()); + #if defined(__APPLE__) + { + bin_path = bin_path.parent_path() / ((instance_type == NewSlicerInstanceType::Slicer) ? "PrusaSlicer" : "PrusaGCodeViewer"); + // On Apple the wxExecute fails, thus we use boost::process instead. + BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << bin_path.string() << "\""; + try { + std::vector args; + if (path_to_open) + args.emplace_back(into_u8(*path_to_open)); + boost::process::spawn(bin_path, args); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to spawn a new slicer \"" << bin_path.string() << "\": " << ex.what(); + } + } + #else // Linux or Unix + { + std::vector args; + args.reserve(3); + #ifdef __linux + static const char *gcodeviewer_param = "--gcodeviewer"; + { + // If executed by an AppImage, start the AppImage, not the main process. + // see https://docs.appimage.org/packaging-guide/environment-variables.html#id2 + const char *appimage_binary = std::getenv("APPIMAGE"); + if (appimage_binary) { + args.emplace_back(appimage_binary); + if (instance_type == NewSlicerInstanceType::GCodeViewer) + args.emplace_back(gcodeviewer_param); + } + } + #endif // __linux + std::string my_path; + if (args.empty()) { + // Binary path was not set to the AppImage in the Linux specific block above, call the application directly. + my_path = (bin_path.parent_path() / ((instance_type == NewSlicerInstanceType::Slicer) ? "prusa-slicer" : "prusa-gcodeviewer")).string(); + args.emplace_back(my_path.c_str()); + } + std::string to_open; + if (path_to_open) { + to_open = into_u8(*path_to_open); + args.emplace_back(to_open.c_str()); + } + args.emplace_back(nullptr); + BOOST_LOG_TRIVIAL(info) << "Trying to spawn a new slicer \"" << args[0] << "\""; + if (wxExecute(const_cast(args.data()), wxEXEC_ASYNC | wxEXEC_MAKE_GROUP_LEADER) <= 0) + BOOST_LOG_TRIVIAL(error) << "Failed to spawn a new slicer \"" << args[0]; + } + #endif // Linux or Unix +#endif // Win32 +} + +void start_new_slicer(const wxString *path_to_open) +{ + start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::Slicer, path_to_open); +} + +void start_new_gcodeviewer(const wxString *path_to_open) +{ + start_new_slicer_or_gcodeviewer(NewSlicerInstanceType::GCodeViewer, path_to_open); +} + +void start_new_gcodeviewer_open_file(wxWindow *parent) +{ + wxFileDialog dialog(parent ? parent : wxGetApp().GetTopWindow(), + _L("Open G-code file:"), + from_u8(wxGetApp().app_config->get_last_dir()), wxString(), + file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() == wxID_OK) { + wxString path = dialog.GetPath(); + start_new_gcodeviewer(&path); + } +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/Utils/Process.hpp b/src/slic3r/Utils/Process.hpp new file mode 100644 index 0000000000..65f90222dd --- /dev/null +++ b/src/slic3r/Utils/Process.hpp @@ -0,0 +1,20 @@ +#ifndef GUI_PROCESS_HPP +#define GUI_PROCESS_HPP + +class wxWindow; +class wxString; + +namespace Slic3r { +namespace GUI { + +// Start a new slicer instance, optionally with a file to open. +void start_new_slicer(const wxString *path_to_open = nullptr); +// Start a new G-code viewer instance, optionally with a file to open. +void start_new_gcodeviewer(const wxString *path_to_open = nullptr); +// Open a file dialog, ask the user to select a new G-code to open, start a new G-code viewer. +void start_new_gcodeviewer_open_file(wxWindow *parent = nullptr); + +} // namespace GUI +} // namespace Slic3r + +#endif // GUI_PROCESS_HPP diff --git a/src/slic3r/Utils/Profile.hpp b/src/slic3r/Utils/Profile.hpp new file mode 100644 index 0000000000..5fb1e31167 --- /dev/null +++ b/src/slic3r/Utils/Profile.hpp @@ -0,0 +1,19 @@ +#ifndef slic3r_GUI_Profile_hpp_ +#define slic3r_GUI_Profile_hpp_ + +// Profiling support using the Shiny intrusive profiler +//#define SLIC3R_PROFILE_GUI +#if defined(SLIC3R_PROFILE) && defined(SLIC3R_PROFILE_GUI) + #include + #define SLIC3R_GUI_PROFILE_FUNC() PROFILE_FUNC() + #define SLIC3R_GUI_PROFILE_BLOCK(name) PROFILE_BLOCK(name) + #define SLIC3R_GUI_PROFILE_UPDATE() PROFILE_UPDATE() + #define SLIC3R_GUI_PROFILE_OUTPUT(x) PROFILE_OUTPUT(x) +#else + #define SLIC3R_GUI_PROFILE_FUNC() + #define SLIC3R_GUI_PROFILE_BLOCK(name) + #define SLIC3R_GUI_PROFILE_UPDATE() + #define SLIC3R_GUI_PROFILE_OUTPUT(x) +#endif + +#endif // slic3r_GUI_Profile_hpp_ diff --git a/src/slic3r/Utils/SLAImport.cpp b/src/slic3r/Utils/SLAImport.cpp deleted file mode 100644 index 442025a77d..0000000000 --- a/src/slic3r/Utils/SLAImport.cpp +++ /dev/null @@ -1,314 +0,0 @@ -#include "SLAImport.hpp" - -#include - -#include "libslic3r/SlicesToTriangleMesh.hpp" -#include "libslic3r/MarchingSquares.hpp" -#include "libslic3r/ClipperUtils.hpp" -#include "libslic3r/MTUtils.hpp" -#include "libslic3r/PrintConfig.hpp" -#include "libslic3r/SLA/RasterBase.hpp" -#include "libslic3r/miniz_extension.hpp" - -#include -#include -#include - -#include -#include - -namespace marchsq { - -// Specialize this struct to register a raster type for the Marching squares alg -template<> struct _RasterTraits { - using Rst = wxImage; - - // The type of pixel cell in the raster - using ValueType = uint8_t; - - // Value at a given position - static uint8_t get(const Rst &rst, size_t row, size_t col) - { - return rst.GetRed(col, row); - } - - // Number of rows and cols of the raster - static size_t rows(const Rst &rst) { return rst.GetHeight(); } - static size_t cols(const Rst &rst) { return rst.GetWidth(); } -}; - -} // namespace marchsq - -namespace Slic3r { - -namespace { - -struct ArchiveData { - boost::property_tree::ptree profile, config; - std::vector images; -}; - -static const constexpr char *CONFIG_FNAME = "config.ini"; -static const constexpr char *PROFILE_FNAME = "prusaslicer.ini"; - -boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry, - MZ_Archive & zip) -{ - std::string buf(size_t(entry.m_uncomp_size), '\0'); - - if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, - buf.data(), buf.size(), 0)) - throw std::runtime_error(zip.get_errorstr()); - - boost::property_tree::ptree tree; - std::stringstream ss(buf); - boost::property_tree::read_ini(ss, tree); - return tree; -} - -sla::EncodedRaster read_png(const mz_zip_archive_file_stat &entry, - MZ_Archive & zip, - const std::string & name) -{ - std::vector buf(entry.m_uncomp_size); - - if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, - buf.data(), buf.size(), 0)) - throw std::runtime_error(zip.get_errorstr()); - - return sla::EncodedRaster(std::move(buf), - name.empty() ? entry.m_filename : name); -} - -ArchiveData extract_sla_archive(const std::string &zipfname, - const std::string &exclude) -{ - ArchiveData arch; - - // Little RAII - struct Arch: public MZ_Archive { - Arch(const std::string &fname) { - if (!open_zip_reader(&arch, fname)) - throw std::runtime_error(get_errorstr()); - } - - ~Arch() { close_zip_reader(&arch); } - } zip (zipfname); - - mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch); - - for (mz_uint i = 0; i < num_entries; ++i) - { - mz_zip_archive_file_stat entry; - - if (mz_zip_reader_file_stat(&zip.arch, i, &entry)) - { - std::string name = entry.m_filename; - boost::algorithm::to_lower(name); - - if (boost::algorithm::contains(name, exclude)) continue; - - if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip); - if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip); - - if (boost::filesystem::path(name).extension().string() == ".png") { - auto it = std::lower_bound( - arch.images.begin(), arch.images.end(), sla::EncodedRaster({}, name), - [](const sla::EncodedRaster &r1, const sla::EncodedRaster &r2) { - return std::less()(r1.extension(), r2.extension()); - }); - - arch.images.insert(it, read_png(entry, zip, name)); - } - } - } - - return arch; -} - -ExPolygons rings_to_expolygons(const std::vector &rings, - double px_w, double px_h) -{ - ExPolygons polys; polys.reserve(rings.size()); - - for (const marchsq::Ring &ring : rings) { - Polygon poly; Points &pts = poly.points; - pts.reserve(ring.size()); - - for (const marchsq::Coord &crd : ring) - pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h)); - - polys.emplace_back(poly); - } - - // reverse the raster transformations - return union_ex(polys); -} - -template void foreach_vertex(ExPolygon &poly, Fn &&fn) -{ - for (auto &p : poly.contour.points) fn(p); - for (auto &h : poly.holes) - for (auto &p : h.points) fn(p); -} - -void invert_raster_trafo(ExPolygons & expolys, - const sla::RasterBase::Trafo &trafo, - coord_t width, - coord_t height) -{ - for (auto &expoly : expolys) { - if (trafo.mirror_y) - foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); }); - - if (trafo.mirror_x) - foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); }); - - expoly.translate(-trafo.center_x, -trafo.center_y); - - if (trafo.flipXY) - foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); }); - - if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) { - expoly.contour.reverse(); - for (auto &h : expoly.holes) h.reverse(); - } - } -} - -struct RasterParams { - sla::RasterBase::Trafo trafo; // Raster transformations - coord_t width, height; // scaled raster dimensions (not resolution) - double px_h, px_w; // pixel dimesions - marchsq::Coord win; // marching squares window size -}; - -RasterParams get_raster_params(const DynamicPrintConfig &cfg) -{ - auto *opt_disp_cols = cfg.option("display_pixels_x"); - auto *opt_disp_rows = cfg.option("display_pixels_y"); - auto *opt_disp_w = cfg.option("display_width"); - auto *opt_disp_h = cfg.option("display_height"); - auto *opt_mirror_x = cfg.option("display_mirror_x"); - auto *opt_mirror_y = cfg.option("display_mirror_y"); - auto *opt_orient = cfg.option>("display_orientation"); - - if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h || - !opt_mirror_x || !opt_mirror_y || !opt_orient) - throw std::runtime_error("Invalid SL1 file"); - - RasterParams rstp; - - rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1); - rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1); - - sla::RasterBase::Trafo trafo{opt_orient->value == sladoLandscape ? - sla::RasterBase::roLandscape : - sla::RasterBase::roPortrait, - {opt_mirror_x->value, opt_mirror_y->value}}; - - rstp.height = scaled(opt_disp_h->value); - rstp.width = scaled(opt_disp_w->value); - - return rstp; -} - -struct SliceParams { double layerh = 0., initial_layerh = 0.; }; - -SliceParams get_slice_params(const DynamicPrintConfig &cfg) -{ - auto *opt_layerh = cfg.option("layer_height"); - auto *opt_init_layerh = cfg.option("initial_layer_height"); - - if (!opt_layerh || !opt_init_layerh) - throw std::runtime_error("Invalid SL1 file"); - - return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()}; -} - -std::vector extract_slices_from_sla_archive( - ArchiveData & arch, - const RasterParams & rstp, - std::function progr) -{ - auto jobdir = arch.config.get("jobDir"); - for (auto &c : jobdir) c = std::tolower(c); - - std::vector slices(arch.images.size()); - - struct Status - { - double incr, val, prev; - bool stop = false; - tbb::spin_mutex mutex; - } st {100. / slices.size(), 0., 0.}; - - tbb::parallel_for(size_t(0), arch.images.size(), - [&arch, &slices, &st, &rstp, progr](size_t i) { - // Status indication guarded with the spinlock - { - std::lock_guard lck(st.mutex); - if (st.stop) return; - - st.val += st.incr; - double curr = std::round(st.val); - if (curr > st.prev) { - st.prev = curr; - st.stop = !progr(int(curr)); - } - } - - auto &buf = arch.images[i]; - wxMemoryInputStream stream{buf.data(), buf.size()}; - wxImage img{stream}; - - auto rings = marchsq::execute(img, 128, rstp.win); - ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h); - - // Invert the raster transformations indicated in - // the profile metadata - invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height); - - slices[i] = std::move(expolys); - }); - - if (st.stop) slices = {}; - - return slices; -} - -} // namespace - -void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) -{ - ArchiveData arch = extract_sla_archive(zipfname, "png"); - out.load(arch.profile); -} - -void import_sla_archive( - const std::string & zipfname, - Vec2i windowsize, - TriangleMesh & out, - DynamicPrintConfig & profile, - std::function progr) -{ - // Ensure minimum window size for marching squares - windowsize.x() = std::max(2, windowsize.x()); - windowsize.y() = std::max(2, windowsize.y()); - - ArchiveData arch = extract_sla_archive(zipfname, "thumbnail"); - profile.load(arch.profile); - - RasterParams rstp = get_raster_params(profile); - rstp.win = {windowsize.y(), windowsize.x()}; - - SliceParams slicp = get_slice_params(profile); - - std::vector slices = - extract_slices_from_sla_archive(arch, rstp, progr); - - if (!slices.empty()) - out = slices_to_triangle_mesh(slices, 0, slicp.layerh, slicp.initial_layerh); -} - -} // namespace Slic3r diff --git a/src/slic3r/Utils/SLAImport.hpp b/src/slic3r/Utils/SLAImport.hpp deleted file mode 100644 index 73995014f4..0000000000 --- a/src/slic3r/Utils/SLAImport.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef SLAIMPORT_HPP -#define SLAIMPORT_HPP - -#include - -#include -#include - -namespace Slic3r { - -class TriangleMesh; -class DynamicPrintConfig; - -void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out); - -void import_sla_archive( - const std::string & zipfname, - Vec2i windowsize, - TriangleMesh & out, - DynamicPrintConfig & profile, - std::function progr = [](int) { return true; }); - -inline void import_sla_archive( - const std::string & zipfname, - Vec2i windowsize, - TriangleMesh & out, - std::function progr = [](int) { return true; }) -{ - DynamicPrintConfig profile; - import_sla_archive(zipfname, windowsize, out, profile, progr); -} - -} - -#endif // SLAIMPORT_HPP diff --git a/src/slic3r/Utils/Serial.cpp b/src/slic3r/Utils/Serial.cpp index 737e76c0b5..959c60c373 100644 --- a/src/slic3r/Utils/Serial.cpp +++ b/src/slic3r/Utils/Serial.cpp @@ -298,7 +298,7 @@ void Serial::set_baud_rate(unsigned baud_rate) auto handle_errno = [](int retval) { if (retval != 0) { - throw std::runtime_error( + throw Slic3r::RuntimeError( (boost::format("Could not set baud rate: %1%") % strerror(errno)).str() ); } @@ -346,7 +346,7 @@ void Serial::set_baud_rate(unsigned baud_rate) handle_errno(::cfsetspeed(&ios, baud_rate)); handle_errno(::tcsetattr(handle, TCSAFLUSH, &ios)); #else - throw std::runtime_error("Custom baud rates are not currently supported on this OS"); + throw Slic3r::RuntimeError("Custom baud rates are not currently supported on this OS"); #endif } } @@ -358,7 +358,7 @@ void Serial::set_DTR(bool on) auto handle = native_handle(); #if defined(_WIN32) && !defined(__SYMBIAN32__) if (! EscapeCommFunction(handle, on ? SETDTR : CLRDTR)) { - throw std::runtime_error("Could not set serial port DTR"); + throw Slic3r::RuntimeError("Could not set serial port DTR"); } #else int status; @@ -369,7 +369,7 @@ void Serial::set_DTR(bool on) } } - throw std::runtime_error( + throw Slic3r::RuntimeError( (boost::format("Could not set serial port DTR: %1%") % strerror(errno)).str() ); #endif diff --git a/src/slic3r/Utils/Thread.hpp b/src/slic3r/Utils/Thread.hpp index e9c76d2aba..194971c9eb 100644 --- a/src/slic3r/Utils/Thread.hpp +++ b/src/slic3r/Utils/Thread.hpp @@ -1,5 +1,5 @@ -#ifndef THREAD_HPP -#define THREAD_HPP +#ifndef GUI_THREAD_HPP +#define GUI_THREAD_HPP #include #include @@ -25,4 +25,4 @@ template inline boost::thread create_thread(Fn &&fn) } -#endif // THREAD_HPP +#endif // GUI_THREAD_HPP diff --git a/src/slic3r/Utils/UndoRedo.cpp b/src/slic3r/Utils/UndoRedo.cpp index 9c8d7a8c68..10b8062f7b 100644 --- a/src/slic3r/Utils/UndoRedo.cpp +++ b/src/slic3r/Utils/UndoRedo.cpp @@ -847,7 +847,7 @@ void StackImpl::load_snapshot(size_t timestamp, Slic3r::Model& model, Slic3r::GU // Find the snapshot by time. It must exist. const auto it_snapshot = std::lower_bound(m_snapshots.begin(), m_snapshots.end(), Snapshot(timestamp)); if (it_snapshot == m_snapshots.end() || it_snapshot->timestamp != timestamp) - throw std::runtime_error((boost::format("Snapshot with timestamp %1% does not exist") % timestamp).str()); + throw Slic3r::RuntimeError((boost::format("Snapshot with timestamp %1% does not exist") % timestamp).str()); m_active_snapshot_time = timestamp; model.clear_objects(); diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_data.cpp index 70f82f4a50..8e5f6bafdc 100644 --- a/tests/fff_print/test_data.cpp +++ b/tests/fff_print/test_data.cpp @@ -137,7 +137,7 @@ TriangleMesh mesh(TestMesh m) { {0,1,2}, {2,1,3}, {4,0,5}, {4,1,0}, {6,4,7}, {7,4,5}, {4,8,1}, {0,2,5}, {5,2,9}, {2,10,9}, {10,3,11}, {2,3,10}, {9,10,12}, {13,9,12}, {3,1,8}, {11,3,8}, {10,11,8}, {4,10,8}, {6,12,10}, {4,6,10}, {7,13,12}, {6,7,12}, {7,5,9}, {13,7,9} }); break; default: - throw std::invalid_argument("Slic3r::Test::mesh(): called with invalid mesh ID"); + throw Slic3r::InvalidArgument("Slic3r::Test::mesh(): called with invalid mesh ID"); break; } @@ -244,8 +244,12 @@ std::string gcode(Print & print) boost::filesystem::path temp = boost::filesystem::unique_path(); print.set_status_silent(); print.process(); +#if ENABLE_GCODE_VIEWER + print.export_gcode(temp.string(), nullptr, nullptr); +#else print.export_gcode(temp.string(), nullptr); - std::ifstream t(temp.string()); +#endif // ENABLE_GCODE_VIEWER + std::ifstream t(temp.string()); std::string str((std::istreambuf_iterator(t)), std::istreambuf_iterator()); boost::nowide::remove(temp.string().c_str()); return str; diff --git a/tests/fff_print/test_model.cpp b/tests/fff_print/test_model.cpp index 6cb9266214..45a080f315 100644 --- a/tests/fff_print/test_model.cpp +++ b/tests/fff_print/test_model.cpp @@ -50,7 +50,11 @@ SCENARIO("Model construction", "[Model]") { print.apply(model, config); print.process(); boost::filesystem::path temp = boost::filesystem::unique_path(); +#if ENABLE_GCODE_VIEWER + print.export_gcode(temp.string(), nullptr, nullptr); +#else print.export_gcode(temp.string(), nullptr); +#endif // ENABLE_GCODE_VIEWER REQUIRE(boost::filesystem::exists(temp)); REQUIRE(boost::filesystem::is_regular_file(temp)); REQUIRE(boost::filesystem::file_size(temp) > 0); diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 5a1e8f18b7..501af0c6f3 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -17,6 +17,9 @@ add_executable(${_TEST_NAME}_tests test_marchingsquares.cpp test_timeutils.cpp test_voronoi.cpp + test_optimizers.cpp + test_png_io.cpp + test_timeutils.cpp ) if (TARGET OpenVDB::openvdb) diff --git a/tests/libslic3r/test_optimizers.cpp b/tests/libslic3r/test_optimizers.cpp new file mode 100644 index 0000000000..6e84f6a691 --- /dev/null +++ b/tests/libslic3r/test_optimizers.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include + +#include + +void check_opt_result(double score, double ref, double abs_err, double rel_err) +{ + double abs_diff = std::abs(score - ref); + double rel_diff = std::abs(abs_diff / std::abs(ref)); + + bool abs_reached = abs_diff < abs_err; + bool rel_reached = rel_diff < rel_err; + bool precision_reached = abs_reached || rel_reached; + REQUIRE(precision_reached); +} + +template void test_sin(Opt &&opt) +{ + using namespace Slic3r::opt; + + auto optfunc = [](const auto &in) { + auto [phi] = in; + + return std::sin(phi); + }; + + auto init = initvals({PI}); + auto optbounds = bounds({ {0., 2 * PI}}); + + Result result_min = opt.to_min().optimize(optfunc, init, optbounds); + Result result_max = opt.to_max().optimize(optfunc, init, optbounds); + + check_opt_result(result_min.score, -1., 1e-2, 1e-4); + check_opt_result(result_max.score, 1., 1e-2, 1e-4); +} + +template void test_sphere_func(Opt &&opt) +{ + using namespace Slic3r::opt; + + Result result = opt.to_min().optimize([](const auto &in) { + auto [x, y] = in; + + return x * x + y * y + 1.; + }, initvals({.6, -0.2}), bounds({{-1., 1.}, {-1., 1.}})); + + check_opt_result(result.score, 1., 1e-2, 1e-4); +} + +TEST_CASE("Test brute force optimzer for basic 1D and 2D functions", "[Opt]") { + using namespace Slic3r::opt; + + Optimizer opt; + + test_sin(opt); + test_sphere_func(opt); +} diff --git a/tests/libslic3r/test_png_io.cpp b/tests/libslic3r/test_png_io.cpp new file mode 100644 index 0000000000..51f94be326 --- /dev/null +++ b/tests/libslic3r/test_png_io.cpp @@ -0,0 +1,55 @@ +#define NOMINMAX +#include + +#include + +#include "libslic3r/PNGRead.hpp" +#include "libslic3r/SLA/AGGRaster.hpp" +#include "libslic3r/BoundingBox.hpp" + +using namespace Slic3r; + +static sla::RasterGrayscaleAA create_raster(const sla::RasterBase::Resolution &res) +{ + sla::RasterBase::PixelDim pixdim{1., 1.}; + + auto bb = BoundingBox({0, 0}, {scaled(1.), scaled(1.)}); + sla::RasterBase::Trafo trafo; + trafo.center_x = bb.center().x(); + trafo.center_y = bb.center().y(); + + return sla::RasterGrayscaleAA{res, pixdim, trafo, agg::gamma_threshold(.5)}; +} + +TEST_CASE("PNG read", "[PNG]") { + auto rst = create_raster({100, 100}); + + size_t rstsum = 0; + for (size_t r = 0; r < rst.resolution().height_px; ++r) + for (size_t c = 0; c < rst.resolution().width_px; ++c) + rstsum += rst.read_pixel(c, r); + + SECTION("Correct png buffer should be recognized as such.") { + auto enc_rst = rst.encode(sla::PNGRasterEncoder{}); + REQUIRE(Slic3r::png::is_png({enc_rst.data(), enc_rst.size()})); + } + + SECTION("Fake png buffer should be recognized as such.") { + std::vector fake(10, '\0'); + REQUIRE(!Slic3r::png::is_png({fake.data(), fake.size()})); + } + + SECTION("Decoded PNG buffer resolution should match the original") { + auto enc_rst = rst.encode(sla::PNGRasterEncoder{}); + + png::ImageGreyscale img; + png::decode_png({enc_rst.data(), enc_rst.size()}, img); + + REQUIRE(img.rows == rst.resolution().height_px); + REQUIRE(img.cols == rst.resolution().width_px); + + size_t sum = std::accumulate(img.buf.begin(), img.buf.end(), size_t(0)); + + REQUIRE(sum == rstsum); + } +} diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index 9d47f3ae4d..d071b49739 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -1,7 +1,8 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) -add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp - sla_print_tests.cpp - sla_test_utils.hpp sla_test_utils.cpp +add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp + sla_print_tests.cpp + sla_test_utils.hpp sla_test_utils.cpp sla_treebuilder_tests.cpp + sla_supptgen_tests.cpp sla_raycast_tests.cpp) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 82df2c1a6f..1575ee0e6b 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -4,6 +4,9 @@ #include "sla_test_utils.hpp" +#include +#include + namespace { const char *const BELOW_PAD_TEST_OBJECTS[] = { @@ -37,9 +40,9 @@ TEST_CASE("Support point generator should be deterministic if seeded", "[SLASupportGeneration], [SLAPointGen]") { TriangleMesh mesh = load_model("A_upsidedown.obj"); - sla::EigenMesh3D emesh{mesh}; + sla::IndexedMesh emesh{mesh}; - sla::SupportConfig supportcfg; + sla::SupportTreeConfig supportcfg; sla::SupportPointGenerator::Config autogencfg; autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; @@ -124,14 +127,14 @@ TEST_CASE("WingedPadAroundObjectIsValid", "[SLASupportGeneration]") { } TEST_CASE("ElevatedSupportGeometryIsValid", "[SLASupportGeneration]") { - sla::SupportConfig supportcfg; + sla::SupportTreeConfig supportcfg; supportcfg.object_elevation_mm = 5.; for (auto fname : SUPPORT_TEST_MODELS) test_supports(fname); } TEST_CASE("FloorSupportGeometryIsValid", "[SLASupportGeneration]") { - sla::SupportConfig supportcfg; + sla::SupportTreeConfig supportcfg; supportcfg.object_elevation_mm = 0; for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg); @@ -139,7 +142,7 @@ TEST_CASE("FloorSupportGeometryIsValid", "[SLASupportGeneration]") { TEST_CASE("ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration]") { - sla::SupportConfig supportcfg; + sla::SupportTreeConfig supportcfg; for (auto fname : SUPPORT_TEST_MODELS) test_support_model_collision(fname, supportcfg); @@ -147,7 +150,7 @@ TEST_CASE("ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration]") { TEST_CASE("FloorSupportsDoNotPierceModel", "[SLASupportGeneration]") { - sla::SupportConfig supportcfg; + sla::SupportTreeConfig supportcfg; supportcfg.object_elevation_mm = 0; for (auto fname : SUPPORT_TEST_MODELS) @@ -228,3 +231,23 @@ TEST_CASE("Triangle mesh conversions should be correct", "[SLAConversions]") cntr.from_obj(infile); } } + +TEST_CASE("halfcone test", "[halfcone]") { + sla::DiffBridge br{Vec3d{1., 1., 1.}, Vec3d{10., 10., 10.}, 0.25, 0.5}; + + TriangleMesh m = sla::to_triangle_mesh(sla::get_mesh(br, 45)); + + m.require_shared_vertices(); + m.WriteOBJFile("Halfcone.obj"); +} + +TEST_CASE("Test concurrency") +{ + std::vector vals = grid(0., 100., 10.); + + double ref = std::accumulate(vals.begin(), vals.end(), 0.); + + double s = sla::ccr_par::reduce(vals.begin(), vals.end(), 0., std::plus{}); + + REQUIRE(s == Approx(ref)); +} diff --git a/tests/sla_print/sla_raycast_tests.cpp b/tests/sla_print/sla_raycast_tests.cpp index c82e4569a8..b56909280b 100644 --- a/tests/sla_print/sla_raycast_tests.cpp +++ b/tests/sla_print/sla_raycast_tests.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include #include #include "sla_test_utils.hpp" @@ -65,7 +65,7 @@ TEST_CASE("Raycaster with loaded drillholes", "[sla_raycast]") cube.merge(*cube_inside); cube.require_shared_vertices(); - sla::EigenMesh3D emesh{cube}; + sla::IndexedMesh emesh{cube}; emesh.load_holes(holes); Vec3d s = center.cast(); diff --git a/tests/sla_print/sla_supptgen_tests.cpp b/tests/sla_print/sla_supptgen_tests.cpp new file mode 100644 index 0000000000..ee9013a44c --- /dev/null +++ b/tests/sla_print/sla_supptgen_tests.cpp @@ -0,0 +1,146 @@ +#include +#include + +#include +#include + +#include "sla_test_utils.hpp" + +namespace Slic3r { namespace sla { + +TEST_CASE("Overhanging point should be supported", "[SupGen]") { + + // Pyramid with 45 deg slope + TriangleMesh mesh = make_pyramid(10.f, 10.f); + mesh.rotate_y(PI); + mesh.require_shared_vertices(); + mesh.WriteOBJFile("Pyramid.obj"); + + sla::SupportPoints pts = calc_support_pts(mesh); + + // The overhang, which is the upside-down pyramid's edge + Vec3f overh{0., 0., -10.}; + + REQUIRE(!pts.empty()); + + float dist = (overh - pts.front().pos).norm(); + + for (const auto &pt : pts) + dist = std::min(dist, (overh - pt.pos).norm()); + + // Should require exactly one support point at the overhang + REQUIRE(pts.size() > 0); + REQUIRE(dist < 1.f); +} + +double min_point_distance(const sla::SupportPoints &pts) +{ + sla::PointIndex index; + + for (size_t i = 0; i < pts.size(); ++i) + index.insert(pts[i].pos.cast(), i); + + auto d = std::numeric_limits::max(); + index.foreach([&d, &index](const sla::PointIndexEl &el) { + auto res = index.nearest(el.first, 2); + for (const sla::PointIndexEl &r : res) + if (r.second != el.second) + d = std::min(d, (el.first - r.first).norm()); + }); + + return d; +} + +TEST_CASE("Overhanging horizontal surface should be supported", "[SupGen]") { + double width = 10., depth = 10., height = 1.; + + TriangleMesh mesh = make_cube(width, depth, height); + mesh.translate(0., 0., 5.); // lift up + mesh.require_shared_vertices(); + mesh.WriteOBJFile("Cuboid.obj"); + + sla::SupportPointGenerator::Config cfg; + sla::SupportPoints pts = calc_support_pts(mesh, cfg); + + double mm2 = width * depth; + + REQUIRE(!pts.empty()); + REQUIRE(pts.size() * cfg.support_force() > mm2 * cfg.tear_pressure()); + REQUIRE(min_point_distance(pts) >= cfg.minimal_distance); +} + +template auto&& center_around_bb(M &&mesh) +{ + auto bb = mesh.bounding_box(); + mesh.translate(-bb.center().template cast()); + + return std::forward(mesh); +} + +TEST_CASE("Overhanging edge should be supported", "[SupGen]") { + float width = 10.f, depth = 10.f, height = 5.f; + + TriangleMesh mesh = make_prism(width, depth, height); + mesh.rotate_y(PI); // rotate on its back + mesh.translate(0., 0., height); + mesh.require_shared_vertices(); + mesh.WriteOBJFile("Prism.obj"); + + sla::SupportPointGenerator::Config cfg; + sla::SupportPoints pts = calc_support_pts(mesh, cfg); + + Linef3 overh{ {0.f, -depth / 2.f, 0.f}, {0.f, depth / 2.f, 0.f}}; + + // Get all the points closer that 1 mm to the overhanging edge: + sla::SupportPoints overh_pts; overh_pts.reserve(pts.size()); + + std::copy_if(pts.begin(), pts.end(), std::back_inserter(overh_pts), + [&overh](const sla::SupportPoint &pt){ + return line_alg::distance_to(overh, Vec3d{pt.pos.cast()}) < 1.; + }); + + REQUIRE(overh_pts.size() * cfg.support_force() > overh.length() * cfg.tear_pressure()); + double ddiff = min_point_distance(pts) - cfg.minimal_distance; + REQUIRE(ddiff > - 0.1 * cfg.minimal_distance); +} + +TEST_CASE("Hollowed cube should be supported from the inside", "[SupGen][Hollowed]") { + TriangleMesh mesh = make_cube(20., 20., 20.); + + hollow_mesh(mesh, HollowingConfig{}); + + mesh.WriteOBJFile("cube_hollowed.obj"); + + auto bb = mesh.bounding_box(); + auto h = float(bb.max.z() - bb.min.z()); + Vec3f mv = bb.center().cast() - Vec3f{0.f, 0.f, 0.5f * h}; + mesh.translate(-mv); + mesh.require_shared_vertices(); + + sla::SupportPointGenerator::Config cfg; + sla::SupportPoints pts = calc_support_pts(mesh, cfg); + sla::remove_bottom_points(pts, mesh.bounding_box().min.z() + EPSILON); + + REQUIRE(!pts.empty()); +} + +TEST_CASE("Two parallel plates should be supported", "[SupGen][Hollowed]") +{ + double width = 20., depth = 20., height = 1.; + + TriangleMesh mesh = center_around_bb(make_cube(width + 5., depth + 5., height)); + TriangleMesh mesh_high = center_around_bb(make_cube(width, depth, height)); + mesh_high.translate(0., 0., 10.); // lift up + mesh.merge(mesh_high); + mesh.require_shared_vertices(); + + mesh.WriteOBJFile("parallel_plates.obj"); + + sla::SupportPointGenerator::Config cfg; + sla::SupportPoints pts = calc_support_pts(mesh, cfg); + sla::remove_bottom_points(pts, mesh.bounding_box().min.z() + EPSILON); + + REQUIRE(!pts.empty()); +} + +}} // namespace Slic3r::sla diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp index 1eaf796c00..a6a0f4304c 100644 --- a/tests/sla_print/sla_test_utils.cpp +++ b/tests/sla_print/sla_test_utils.cpp @@ -2,13 +2,13 @@ #include "libslic3r/SLA/AGGRaster.hpp" void test_support_model_collision(const std::string &obj_filename, - const sla::SupportConfig &input_supportcfg, + const sla::SupportTreeConfig &input_supportcfg, const sla::HollowingConfig &hollowingcfg, const sla::DrainHoles &drainholes) { SupportByproducts byproducts; - sla::SupportConfig supportcfg = input_supportcfg; + sla::SupportTreeConfig supportcfg = input_supportcfg; // Set head penetration to a small negative value which should ensure that // the supports will not touch the model body. @@ -38,7 +38,10 @@ void test_support_model_collision(const std::string &obj_filename, Polygons intersections = intersection(sup_slice, mod_slice); - notouch = notouch && intersections.empty(); + double pinhead_r = scaled(input_supportcfg.head_front_radius_mm); + + // TODO:: make it strict without a threshold of PI * pihead_radius ^ 2 + notouch = notouch && area(intersections) < PI * pinhead_r * pinhead_r; } /*if (!notouch) */export_failed_case(support_slices, byproducts); @@ -69,11 +72,12 @@ void export_failed_case(const std::vector &support_slices, const Sup m.merge(byproducts.input_mesh); m.repair(); m.require_shared_vertices(); - m.WriteOBJFile(byproducts.obj_fname.c_str()); + m.WriteOBJFile((Catch::getResultCapture().getCurrentTestName() + "_" + + byproducts.obj_fname).c_str()); } void test_supports(const std::string &obj_filename, - const sla::SupportConfig &supportcfg, + const sla::SupportTreeConfig &supportcfg, const sla::HollowingConfig &hollowingcfg, const sla::DrainHoles &drainholes, SupportByproducts &out) @@ -104,7 +108,7 @@ void test_supports(const std::string &obj_filename, // Create the special index-triangle mesh with spatial indexing which // is the input of the support point and support mesh generators - sla::EigenMesh3D emesh{mesh}; + sla::IndexedMesh emesh{mesh}; #ifdef SLIC3R_HOLE_RAYCASTER if (hollowingcfg.enabled) @@ -129,8 +133,7 @@ void test_supports(const std::string &obj_filename, // If there is no elevation, support points shall be removed from the // bottom of the object. if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { - sla::remove_bottom_points(support_points, zmin, - supportcfg.base_height_mm); + sla::remove_bottom_points(support_points, zmin + supportcfg.base_height_mm); } else { // Should be support points at least on the bottom of the model REQUIRE_FALSE(support_points.empty()); @@ -141,7 +144,8 @@ void test_supports(const std::string &obj_filename, // Generate the actual support tree sla::SupportTreeBuilder treebuilder; - treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg}); + sla::SupportableMesh sm{emesh, support_points, supportcfg}; + sla::SupportTreeBuildsteps::execute(treebuilder, sm); check_support_tree_integrity(treebuilder, supportcfg); @@ -157,8 +161,8 @@ void test_supports(const std::string &obj_filename, if (std::abs(supportcfg.object_elevation_mm) < EPSILON) allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm; - REQUIRE(obb.min.z() >= allowed_zmin); - REQUIRE(obb.max.z() <= zmax); + REQUIRE(obb.min.z() >= Approx(allowed_zmin)); + REQUIRE(obb.max.z() <= Approx(zmax)); // Move out the support tree into the byproducts, we can examine it further // in various tests. @@ -168,15 +172,15 @@ void test_supports(const std::string &obj_filename, } void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, - const sla::SupportConfig &cfg) + const sla::SupportTreeConfig &cfg) { double gnd = stree.ground_level; double H1 = cfg.max_solo_pillar_height_mm; double H2 = cfg.max_dual_pillar_height_mm; for (const sla::Head &head : stree.heads()) { - REQUIRE((!head.is_valid() || head.pillar_id != sla::ID_UNSET || - head.bridge_id != sla::ID_UNSET)); + REQUIRE((!head.is_valid() || head.pillar_id != sla::SupportTreeNode::ID_UNSET || + head.bridge_id != sla::SupportTreeNode::ID_UNSET)); } for (const sla::Pillar &pillar : stree.pillars()) { @@ -410,3 +414,71 @@ double predict_error(const ExPolygon &p, const sla::RasterBase::PixelDim &pd) return error; } + + +// Make a 3D pyramid +TriangleMesh make_pyramid(float base, float height) +{ + float a = base / 2.f; + + TriangleMesh mesh( + { + {-a, -a, 0}, {a, -a, 0}, {a, a, 0}, + {-a, a, 0}, {0.f, 0.f, height} + }, + { + {0, 1, 2}, + {0, 2, 3}, + {0, 1, 4}, + {1, 2, 4}, + {2, 3, 4}, + {3, 0, 4} + }); + + mesh.repair(); + + return mesh; +} + + TriangleMesh make_prism(double width, double length, double height) +{ + // We need two upward facing triangles + + double x = width / 2., y = length / 2.; + + TriangleMesh mesh( + { + {-x, -y, 0.}, {x, -y, 0.}, {0., -y, height}, + {-x, y, 0.}, {x, y, 0.}, {0., y, height}, + }, + { + {0, 1, 2}, // side 1 + {4, 3, 5}, // side 2 + {1, 4, 2}, {2, 4, 5}, // roof 1 + {0, 2, 5}, {0, 5, 3}, // roof 2 + {3, 4, 1}, {3, 1, 0} // bottom + }); + + return mesh; +} + +sla::SupportPoints calc_support_pts( + const TriangleMesh & mesh, + const sla::SupportPointGenerator::Config &cfg) +{ + // Prepare the slice grid and the slices + std::vector slices; + auto bb = cast(mesh.bounding_box()); + std::vector heights = grid(bb.min.z(), bb.max.z(), 0.1f); + slice_mesh(mesh, heights, slices, CLOSING_RADIUS, [] {}); + + // Prepare the support point calculator + sla::IndexedMesh emesh{mesh}; + sla::SupportPointGenerator spgen{emesh, cfg, []{}, [](int){}}; + + // Calculate the support points + spgen.seed(0); + spgen.execute(slices, heights); + + return spgen.output(); +} diff --git a/tests/sla_print/sla_test_utils.hpp b/tests/sla_print/sla_test_utils.hpp index 3652b1f81c..d10a85b259 100644 --- a/tests/sla_print/sla_test_utils.hpp +++ b/tests/sla_print/sla_test_utils.hpp @@ -67,16 +67,16 @@ struct SupportByproducts const constexpr float CLOSING_RADIUS = 0.005f; void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, - const sla::SupportConfig &cfg); + const sla::SupportTreeConfig &cfg); void test_supports(const std::string &obj_filename, - const sla::SupportConfig &supportcfg, + const sla::SupportTreeConfig &supportcfg, const sla::HollowingConfig &hollowingcfg, const sla::DrainHoles &drainholes, SupportByproducts &out); inline void test_supports(const std::string &obj_filename, - const sla::SupportConfig &supportcfg, + const sla::SupportTreeConfig &supportcfg, SupportByproducts &out) { sla::HollowingConfig hcfg; @@ -85,7 +85,7 @@ inline void test_supports(const std::string &obj_filename, } inline void test_supports(const std::string &obj_filename, - const sla::SupportConfig &supportcfg = {}) + const sla::SupportTreeConfig &supportcfg = {}) { SupportByproducts byproducts; test_supports(obj_filename, supportcfg, byproducts); @@ -97,13 +97,13 @@ void export_failed_case(const std::vector &support_slices, void test_support_model_collision( const std::string &obj_filename, - const sla::SupportConfig &input_supportcfg, + const sla::SupportTreeConfig &input_supportcfg, const sla::HollowingConfig &hollowingcfg, const sla::DrainHoles &drainholes); inline void test_support_model_collision( const std::string &obj_filename, - const sla::SupportConfig &input_supportcfg = {}) + const sla::SupportTreeConfig &input_supportcfg = {}) { sla::HollowingConfig hcfg; hcfg.enabled = false; @@ -185,4 +185,13 @@ long raster_pxsum(const sla::RasterGrayscaleAA &raster); double predict_error(const ExPolygon &p, const sla::RasterBase::PixelDim &pd); +// Make a 3D pyramid +TriangleMesh make_pyramid(float base, float height); + +TriangleMesh make_prism(double width, double length, double height); + +sla::SupportPoints calc_support_pts( + const TriangleMesh & mesh, + const sla::SupportPointGenerator::Config &cfg = {}); + #endif // SLA_TEST_UTILS_HPP diff --git a/tests/sla_print/sla_treebuilder_tests.cpp b/tests/sla_print/sla_treebuilder_tests.cpp new file mode 100644 index 0000000000..91c2ea6f8e --- /dev/null +++ b/tests/sla_print/sla_treebuilder_tests.cpp @@ -0,0 +1,99 @@ +//#include +//#include + +//#include "libslic3r/TriangleMesh.hpp" +//#include "libslic3r/SLA/SupportTreeBuildsteps.hpp" +//#include "libslic3r/SLA/SupportTreeMesher.hpp" + +//TEST_CASE("Test bridge_mesh_intersect on a cube's wall", "[SLABridgeMeshInters]") { +// using namespace Slic3r; + +// TriangleMesh cube = make_cube(10., 10., 10.); + +// sla::SupportConfig cfg = {}; // use default config +// sla::SupportPoints pts = {{10.f, 5.f, 5.f, float(cfg.head_front_radius_mm), false}}; +// sla::SupportableMesh sm{cube, pts, cfg}; + +// size_t steps = 45; +// SECTION("Bridge is straight horizontal and pointing away from the cube") { + +// sla::Bridge bridge(pts[0].pos.cast(), Vec3d{15., 5., 5.}, +// pts[0].head_front_radius); + +// auto hit = sla::query_hit(sm, bridge); + +// REQUIRE(std::isinf(hit.distance())); + +// cube.merge(sla::to_triangle_mesh(get_mesh(bridge, steps))); +// cube.require_shared_vertices(); +// cube.WriteOBJFile("cube1.obj"); +// } + +// SECTION("Bridge is tilted down in 45 degrees, pointing away from the cube") { +// sla::Bridge bridge(pts[0].pos.cast(), Vec3d{15., 5., 0.}, +// pts[0].head_front_radius); + +// auto hit = sla::query_hit(sm, bridge); + +// REQUIRE(std::isinf(hit.distance())); + +// cube.merge(sla::to_triangle_mesh(get_mesh(bridge, steps))); +// cube.require_shared_vertices(); +// cube.WriteOBJFile("cube2.obj"); +// } +//} + + +//TEST_CASE("Test bridge_mesh_intersect on a sphere", "[SLABridgeMeshInters]") { +// using namespace Slic3r; + +// TriangleMesh sphere = make_sphere(1.); + +// sla::SupportConfig cfg = {}; // use default config +// cfg.head_back_radius_mm = cfg.head_front_radius_mm; +// sla::SupportPoints pts = {{1.f, 0.f, 0.f, float(cfg.head_front_radius_mm), false}}; +// sla::SupportableMesh sm{sphere, pts, cfg}; + +// size_t steps = 45; +// SECTION("Bridge is straight horizontal and pointing away from the sphere") { + +// sla::Bridge bridge(pts[0].pos.cast(), Vec3d{2., 0., 0.}, +// pts[0].head_front_radius); + +// auto hit = sla::query_hit(sm, bridge); + +// sphere.merge(sla::to_triangle_mesh(get_mesh(bridge, steps))); +// sphere.require_shared_vertices(); +// sphere.WriteOBJFile("sphere1.obj"); + +// REQUIRE(std::isinf(hit.distance())); +// } + +// SECTION("Bridge is tilted down 45 deg and pointing away from the sphere") { + +// sla::Bridge bridge(pts[0].pos.cast(), Vec3d{2., 0., -2.}, +// pts[0].head_front_radius); + +// auto hit = sla::query_hit(sm, bridge); + +// sphere.merge(sla::to_triangle_mesh(get_mesh(bridge, steps))); +// sphere.require_shared_vertices(); +// sphere.WriteOBJFile("sphere2.obj"); + +// REQUIRE(std::isinf(hit.distance())); +// } + +// SECTION("Bridge is tilted down 90 deg and pointing away from the sphere") { + +// sla::Bridge bridge(pts[0].pos.cast(), Vec3d{1., 0., -2.}, +// pts[0].head_front_radius); + +// auto hit = sla::query_hit(sm, bridge); + +// sphere.merge(sla::to_triangle_mesh(get_mesh(bridge, steps))); +// sphere.require_shared_vertices(); +// sphere.WriteOBJFile("sphere3.obj"); + +// REQUIRE(std::isinf(hit.distance())); +// } +//} diff --git a/version.inc b/version.inc index 30b373bdfd..e5985fcd0d 100644 --- a/version.inc +++ b/version.inc @@ -7,3 +7,6 @@ set(SLIC3R_VERSION "2.3.0-alpha0") set(SLIC3R_BUILD_ID "PrusaSlicer-${SLIC3R_VERSION}+UNKNOWN") set(SLIC3R_RC_VERSION "2,3,0,0") set(SLIC3R_RC_VERSION_DOTS "2.3.0.0") + +set(GCODEVIEWER_APP_NAME "Prusa GCode Viewer") +set(GCODEVIEWER_BUILD_ID "Prusa GCode Viewer-${SLIC3R_VERSION}+UNKNOWN") diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp index c3cd7e6165..47961c6231 100644 --- a/xs/src/perlglue.cpp +++ b/xs/src/perlglue.cpp @@ -15,7 +15,7 @@ REGISTER_CLASS(Filler, "Filler"); REGISTER_CLASS(Flow, "Flow"); REGISTER_CLASS(CoolingBuffer, "GCode::CoolingBuffer"); REGISTER_CLASS(GCode, "GCode"); -REGISTER_CLASS(GCodePreviewData, "GCode::PreviewData"); +//REGISTER_CLASS(GCodePreviewData, "GCode::PreviewData"); // REGISTER_CLASS(GCodeSender, "GCode::Sender"); REGISTER_CLASS(Layer, "Layer"); REGISTER_CLASS(SupportLayer, "Layer::Support"); diff --git a/xs/xsp/GCode.xsp b/xs/xsp/GCode.xsp index 9e04edd4c6..1536c874b5 100644 --- a/xs/xsp/GCode.xsp +++ b/xs/xsp/GCode.xsp @@ -26,14 +26,14 @@ croak("%s\n", e.what()); } %}; - void do_export_w_preview(Print *print, const char *path, GCodePreviewData *preview_data) - %code%{ - try { - THIS->do_export(print, path, preview_data); - } catch (std::exception& e) { - croak("%s\n", e.what()); - } - %}; +// void do_export_w_preview(Print *print, const char *path, GCodePreviewData *preview_data) +// %code%{ +// try { +// THIS->do_export(print, path, preview_data); +// } catch (std::exception& e) { +// croak("%s\n", e.what()); +// } +// %}; Ref origin() %code{% RETVAL = &(THIS->origin()); %}; @@ -60,26 +60,26 @@ %code{% RETVAL = const_cast(static_cast(static_cast(&THIS->config()))); %}; }; -%name{Slic3r::GCode::PreviewData} class GCodePreviewData { - GCodePreviewData(); - ~GCodePreviewData(); - void reset(); - bool empty() const; - void set_type(int type) - %code%{ - if ((0 <= type) && (type < GCodePreviewData::Extrusion::Num_View_Types)) - THIS->extrusion.view_type = (GCodePreviewData::Extrusion::EViewType)type; - %}; - int type() %code%{ RETVAL = (int)THIS->extrusion.view_type; %}; - void set_extrusion_flags(int flags) - %code%{ THIS->extrusion.role_flags = (unsigned int)flags; %}; - void set_travel_visible(bool visible) - %code%{ THIS->travel.is_visible = visible; %}; - void set_retractions_visible(bool visible) - %code%{ THIS->retraction.is_visible = visible; %}; - void set_unretractions_visible(bool visible) - %code%{ THIS->unretraction.is_visible = visible; %}; - void set_shells_visible(bool visible) - %code%{ THIS->shell.is_visible = visible; %}; - void set_extrusion_paths_colors(std::vector colors); -}; +//%name{Slic3r::GCode::PreviewData} class GCodePreviewData { +// GCodePreviewData(); +// ~GCodePreviewData(); +// void reset(); +// bool empty() const; +// void set_type(int type) +// %code%{ +// if ((0 <= type) && (type < GCodePreviewData::Extrusion::Num_View_Types)) +// THIS->extrusion.view_type = (GCodePreviewData::Extrusion::EViewType)type; +// %}; +// int type() %code%{ RETVAL = (int)THIS->extrusion.view_type; %}; +// void set_extrusion_flags(int flags) +// %code%{ THIS->extrusion.role_flags = (unsigned int)flags; %}; +// void set_travel_visible(bool visible) +// %code%{ THIS->travel.is_visible = visible; %}; +// void set_retractions_visible(bool visible) +// %code%{ THIS->retraction.is_visible = visible; %}; +// void set_unretractions_visible(bool visible) +// %code%{ THIS->unretraction.is_visible = visible; %}; +// void set_shells_visible(bool visible) +// %code%{ THIS->shell.is_visible = visible; %}; +// void set_extrusion_paths_colors(std::vector colors); +//}; diff --git a/xs/xsp/Model.xsp b/xs/xsp/Model.xsp index 4fb35578db..844b7c95ec 100644 --- a/xs/xsp/Model.xsp +++ b/xs/xsp/Model.xsp @@ -12,7 +12,7 @@ #include "libslic3r/Format/OBJ.hpp" #include "libslic3r/Format/PRUS.hpp" #include "libslic3r/Format/STL.hpp" -#include "slic3r/GUI/PresetBundle.hpp" +#include "libslic3r/PresetBundle.hpp" %} %name{Slic3r::Model} class Model { diff --git a/xs/xsp/Print.xsp b/xs/xsp/Print.xsp index 0952513ca3..e4957c042f 100644 --- a/xs/xsp/Print.xsp +++ b/xs/xsp/Print.xsp @@ -76,10 +76,10 @@ _constant() %code%{ RETVAL = const_cast(&THIS->skirt()); %}; Ref brim() %code%{ RETVAL = const_cast(&THIS->brim()); %}; - std::string estimated_normal_print_time() - %code%{ RETVAL = THIS->print_statistics().estimated_normal_print_time; %}; - std::string estimated_silent_print_time() - %code%{ RETVAL = THIS->print_statistics().estimated_silent_print_time; %}; +// std::string estimated_normal_print_time() +// %code%{ RETVAL = THIS->print_statistics().estimated_normal_print_time; %}; +// std::string estimated_silent_print_time() +// %code%{ RETVAL = THIS->print_statistics().estimated_silent_print_time; %}; double total_used_filament() %code%{ RETVAL = THIS->print_statistics().total_used_filament; %}; double total_extruded_volume() diff --git a/xs/xsp/my.map b/xs/xsp/my.map index fd50d29751..7e51b237c0 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -191,9 +191,9 @@ GCode* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T Clone O_OBJECT_SLIC3R_T -GCodePreviewData* O_OBJECT_SLIC3R -Ref O_OBJECT_SLIC3R_T -Clone O_OBJECT_SLIC3R_T +//GCodePreviewData* O_OBJECT_SLIC3R +//Ref O_OBJECT_SLIC3R_T +//Clone O_OBJECT_SLIC3R_T MotionPlanner* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt index 7e277703b8..385b50f1aa 100644 --- a/xs/xsp/typemap.xspt +++ b/xs/xsp/typemap.xspt @@ -155,9 +155,9 @@ %typemap{Ref}{simple}; %typemap{Clone}{simple}; -%typemap{GCodePreviewData*}; -%typemap{Ref}{simple}; -%typemap{Clone}{simple}; +//%typemap{GCodePreviewData*}; +//%typemap{Ref}{simple}; +//%typemap{Clone}{simple}; %typemap{Points}; %typemap{Pointfs};