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/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/PrusaSlicerGCodeViewer_128px.png b/resources/icons/PrusaSlicerGCodeViewer_128px.png
new file mode 100644
index 0000000000..0bf85abbd6
Binary files /dev/null and b/resources/icons/PrusaSlicerGCodeViewer_128px.png differ
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
diff --git a/resources/icons/seam.svg b/resources/icons/seam.svg
new file mode 100644
index 0000000000..119fb6afcc
--- /dev/null
+++ b/resources/icons/seam.svg
@@ -0,0 +1,42 @@
+
+
+
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 @@
+
+
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 @@
+
+
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..0b0b3c0eef 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)
@@ -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
diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp
index c37afb805d..2962f0cdfe 100644
--- a/src/PrusaSlicer.cpp
+++ b/src/PrusaSlicer.cpp
@@ -101,6 +101,7 @@ 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 = false;
const std::vector &load_configs = m_config.option("load", true)->values;
@@ -469,7 +470,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 +522,9 @@ int CLI::run(int argc, char **argv)
<< " (" << print.total_extruded_volume()/1000 << "cm3)" << std::endl;
*/
}
+ } else if (opt_key == "gcodeviewer") {
+ start_gui = true;
+ start_as_gcodeviewer = true;
} else {
boost::nowide::cerr << "error: option not supported yet: " << opt_key << std::endl;
return 1;
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 feda857ae2..4a1d1faa0c 100644
--- a/src/imgui/imconfig.h
+++ b/src/imgui/imconfig.h
@@ -108,17 +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 CloseIconMarker = 0xB;
- const char CloseIconHoverMarker = 0xC;
- const char TimerDotMarker = 0xE;
- const char TimerDotEmptyMarker = 0xF;
- const char WarningMarker = 0x10;
- const char ErrorMarker = 0x11;
+ 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/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index 290b8953cc..3d241dd371 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -99,6 +99,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
@@ -161,6 +163,8 @@ add_library(libslic3r STATIC
PrintConfig.hpp
PrintObject.cpp
PrintRegion.cpp
+ PNGRead.hpp
+ PNGRead.cpp
Semver.cpp
ShortestPath.cpp
ShortestPath.hpp
@@ -308,6 +312,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/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/ExtrusionEntity.cpp b/src/libslic3r/ExtrusionEntity.cpp
index 69b3a6455d..b2c5e1350f 100644
--- a/src/libslic3r/ExtrusionEntity.cpp
+++ b/src/libslic3r/ExtrusionEntity.cpp
@@ -306,7 +306,11 @@ 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");
@@ -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/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp
index 59dc85a0ae..66dd0049cf 100644
--- a/src/libslic3r/Format/3mf.cpp
+++ b/src/libslic3r/Format/3mf.cpp
@@ -87,6 +87,7 @@ 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";
@@ -285,6 +286,7 @@ namespace Slic3r {
std::vector vertices;
std::vector triangles;
std::vector custom_supports;
+ std::vector custom_seam;
bool empty()
{
@@ -296,6 +298,7 @@ namespace Slic3r {
vertices.clear();
triangles.clear();
custom_supports.clear();
+ custom_seam.clear();
}
};
@@ -1114,6 +1117,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));
@@ -1544,6 +1556,7 @@ namespace Slic3r {
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;
}
@@ -1877,14 +1890,18 @@ namespace Slic3r {
volume->source.transform = Slic3r::Geometry::Transformation(volume_matrix_to_object);
volume->calculate_convex_hull();
- // recreate custom supports from previously loaded attribute
+ // 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)
{
@@ -2401,6 +2418,10 @@ namespace Slic3r {
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";
}
}
@@ -2591,7 +2612,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/SL1.cpp b/src/libslic3r/Format/SL1.cpp
index 5c402ef5bf..ff1af5d8b1 100644
--- a/src/libslic3r/Format/SL1.cpp
+++ b/src/libslic3r/Format/SL1.cpp
@@ -8,8 +8,316 @@
#include "libslic3r/Zipper.hpp"
#include "libslic3r/SLAPrint.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 "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 std::runtime_error(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 std::runtime_error(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 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(), 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 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);
+
+ 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 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));
+ }
+ }
+
+ 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 {
diff --git a/src/libslic3r/Format/SL1.hpp b/src/libslic3r/Format/SL1.hpp
index fbb6d61604..ab731ff841 100644
--- a/src/libslic3r/Format/SL1.hpp
+++ b/src/libslic3r/Format/SL1.hpp
@@ -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 7d80677184..135389eb3e 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -48,594 +48,599 @@ 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 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;
+ }
+
+ 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 == gcfRepRap ? 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 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;
}
- // 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 == gcfRepRap ? 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());
+ || (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 (! has_extrusions)
+ if (!has_extrusions)
throw std::runtime_error(_(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)
@@ -644,8 +649,8 @@ std::vector GCode::collect_layers_to_print(const PrintObjec
_(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.
@@ -660,7 +665,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;
@@ -675,15 +680,15 @@ 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;
@@ -692,14 +697,14 @@ std::vector>> GCode::collec
// 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]);
}
@@ -709,7 +714,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();
@@ -731,7 +751,9 @@ void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_
if (file == nullptr)
throw std::runtime_error(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();
@@ -764,6 +786,12 @@ void GCode::do_export(Print* print, const char* path, GCodePreviewData* preview_
throw std::runtime_error(msg);
}
+#if ENABLE_GCODE_VIEWER
+ m_processor.process_file(path_tmp);
+ DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics);
+ if (result != nullptr)
+ *result = std::move(m_processor.extract_result());
+#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();
@@ -772,8 +800,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();
@@ -785,6 +812,7 @@ 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(
@@ -801,7 +829,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();
@@ -873,8 +902,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();
@@ -898,7 +937,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)
{
@@ -1020,24 +1060,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);
@@ -1127,15 +1171,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.
@@ -1226,12 +1284,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();
@@ -1336,9 +1398,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);
@@ -1489,9 +1556,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.
{
@@ -1516,25 +1588,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));
@@ -1543,9 +1623,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");
@@ -1815,10 +1899,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
@@ -1837,20 +1926,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";
gcode += config.pause_print_gcode;
- }
+#endif // !ENABLE_GCODE_VIEWER
+ }
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;
@@ -1993,6 +2094,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;
@@ -2206,9 +2323,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;
@@ -2312,11 +2435,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;
@@ -2327,12 +2452,14 @@ void GCode::process_layer(
#endif /* HAS_PRESSURE_EQUALIZER */
_write(file, gcode);
- BOOST_LOG_TRIVIAL(trace) << "Exported layer " << layer.id() << " print_z " << print_z <<
+#if !ENABLE_GCODE_VIEWER
+ 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: " <<
+ ", analyzer memory: " <<
format_memsize_MB(m_analyzer.memory_used()) <<
- log_memory_info();
+ log_memory_info();
+#endif // !ENABLE_GCODE_VIEWER
}
void GCode::apply_print_config(const PrintConfig &print_config)
@@ -2463,7 +2590,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)
@@ -2651,7 +2778,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();
}
@@ -2671,11 +2798,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;
@@ -2688,7 +2813,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]);
@@ -2708,14 +2832,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));
}
}
@@ -2723,7 +2843,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.
@@ -2972,15 +3091,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
}
}
@@ -3119,42 +3244,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) {
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..13b1ed1a8d
--- /dev/null
+++ b/src/libslic3r/GCode/GCodeProcessor.cpp
@@ -0,0 +1,2163 @@
+#include "libslic3r/libslic3r.h"
+#include "libslic3r/Utils.hpp"
+#include "libslic3r/Print.hpp"
+#include "GCodeProcessor.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+
+#if ENABLE_GCODE_VIEWER
+
+#if ENABLE_GCODE_VIEWER_STATISTICS
+#include
+#endif // ENABLE_GCODE_VIEWER_STATISTICS
+
+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 std::runtime_error(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 std::runtime_error(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 std::runtime_error(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 std::runtime_error(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 std::runtime_error(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)
+{
+#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);
+ }
+ }
+
+ m_result.id = ++s_result_id;
+ m_result.moves.emplace_back(MoveVertex());
+ m_parser.parse_file(filename, [this](GCodeReader& reader, const GCodeReader::GCodeLine& line) { 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 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;
+ }
+
+ // height tag
+ pos = comment.find(Height_Tag);
+ if (pos != comment.npos) {
+ try {
+ m_height_compare.last_tag_value = std::stof(comment.substr(pos + Height_Tag.length()));
+ }
+ catch (...) {
+ BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid value for Height (" << 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 d_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 / d_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_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)) / (d_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] / d_xyz);
+ else
+ // cross section: rectangle + 2 semicircles
+ m_width = delta_pos[E] * static_cast(M_PI * sqr(filament_radius)) / (d_xyz * m_height) + static_cast(1.0 - 0.25 * M_PI) * 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