mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 13:41:03 +00:00
Merge branch 'main' into belt/baseChanges
This commit is contained in:
1058 files changed
+145478
-12479
No files matched your search
+1
-1
@@ -158,7 +158,7 @@ target_link_libraries(OrcaSlicer libslic3r_gui)
|
||||
if (MSVC)
|
||||
# Generate debug symbols even in release mode.
|
||||
target_link_options(OrcaSlicer PUBLIC "$<$<CONFIG:RELEASE>:/DEBUG>")
|
||||
target_link_libraries(OrcaSlicer user32.lib Setupapi.lib)
|
||||
target_link_libraries(OrcaSlicer ws2_32.lib user32.lib Setupapi.lib)
|
||||
elseif (MINGW)
|
||||
target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi)
|
||||
elseif (APPLE)
|
||||
|
||||
+96
-31
@@ -1192,43 +1192,108 @@ int CLI::run(int argc, char **argv)
|
||||
save_main_thread_id();
|
||||
|
||||
#ifdef __WXGTK__
|
||||
// Safety fallback: if wxWidgets was not built with EGL support, native
|
||||
// Wayland will crash in wxGLCanvas::IsDisplaySupported() because the GLX
|
||||
// backend cannot access an X11 display. Force X11 mode in that case.
|
||||
// NOTE: Do NOT remove this block even after enabling wxHAS_EGL
|
||||
// in the build — it protects against builds where deps were not rebuilt.
|
||||
#if !defined(wxHAS_EGL) || !wxHAS_EGL
|
||||
// ------------------------------------------------------------------
|
||||
// Linux backend selection — runtime, based on GDK_BACKEND env var.
|
||||
//
|
||||
// GDK_BACKEND unset (DEFAULT) Wayland path.
|
||||
// GTK auto-picks Wayland on Wayland
|
||||
// sessions and X11 otherwise. WebKit
|
||||
// XWayland-compositing workaround
|
||||
// applied only when actually on
|
||||
// XWayland. XInitThreads gated on
|
||||
// DISPLAY presence.
|
||||
//
|
||||
// GDK_BACKEND=x11 (OPT-IN) X11/XWayland mode. Applies the
|
||||
// v2.3.2 workarounds that the X11
|
||||
// path benefits from: DRI_PRIME for
|
||||
// AMD/nouveau PRIME, NVIDIA PRIME
|
||||
// render offload (when nvidia.ko is
|
||||
// loaded), XInitThreads.
|
||||
// Multi monitor is compromised
|
||||
//
|
||||
// WEBKIT_DISABLE_COMPOSITING_MODE is deliberately NOT set on the X11
|
||||
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
|
||||
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
|
||||
// unset preserves WebKit hardware acceleration on Device / Setup
|
||||
// Wizard / login / store. The default path still applies it on
|
||||
// XWayland sessions as a conservative fallback for older WebKit.
|
||||
// ------------------------------------------------------------------
|
||||
{
|
||||
const char* wayland_env = ::getenv("WAYLAND_DISPLAY");
|
||||
if (wayland_env && *wayland_env) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
|
||||
::setenv("GDK_BACKEND", "x11", true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const char* gdk_backend = ::getenv("GDK_BACKEND");
|
||||
// Match "x11" and comma-prefixed forms like "x11,wayland" (GTK
|
||||
// honours the first backend in the comma-separated list).
|
||||
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
|
||||
|
||||
// WebKit2GTK compositing can fail under XWayland. Only disable it when
|
||||
// both DISPLAY and WAYLAND_DISPLAY are set (i.e., XWayland is in use).
|
||||
// On pure X11 or native Wayland, compositing is left enabled.
|
||||
{
|
||||
const char* display_env_wk = ::getenv("DISPLAY");
|
||||
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
|
||||
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||
}
|
||||
}
|
||||
if (x11_opt_in) {
|
||||
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
|
||||
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
|
||||
|
||||
// XInitThreads is needed before GStreamer may use Xlib. On native
|
||||
// Wayland without DISPLAY, GStreamer uses waylandsink (no Xlib).
|
||||
#if __has_include(<X11/Xlib.h>)
|
||||
{
|
||||
const char* display_env = ::getenv("DISPLAY");
|
||||
if (display_env && *display_env) {
|
||||
// On Linux dual-GPU systems, request the high-performance
|
||||
// discrete GPU. DRI_PRIME=1 handles AMD and nouveau PRIME
|
||||
// setups; harmless on single-GPU systems (Mesa ignores it).
|
||||
::setenv("DRI_PRIME", "1", /* replace */ false);
|
||||
|
||||
// For NVIDIA proprietary driver PRIME render offload, set
|
||||
// additional variables. Only set if the NVIDIA kernel module
|
||||
// is loaded to avoid breaking systems without NVIDIA.
|
||||
if (::access("/proc/driver/nvidia/version", F_OK) == 0) {
|
||||
::setenv("__NV_PRIME_RENDER_OFFLOAD", "1", /* replace */ false);
|
||||
::setenv("__GLX_VENDOR_LIBRARY_NAME", "nvidia", /* replace */ false);
|
||||
}
|
||||
|
||||
// Tell Xlib we will be using threads, lest we crash when
|
||||
// GStreamer fires up. Safe to call here since the user has
|
||||
// explicitly opted into X11.
|
||||
#if __has_include(<X11/Xlib.h>)
|
||||
XInitThreads();
|
||||
#endif
|
||||
} else {
|
||||
// ===== Wayland default =====
|
||||
|
||||
// Safety fallback: if wxWidgets was not built with EGL
|
||||
// support, native Wayland will crash in
|
||||
// wxGLCanvas::IsDisplaySupported() because the GLX backend
|
||||
// cannot access an X11 display. Force X11 mode in that case.
|
||||
// NOTE: Do NOT remove this block even after enabling
|
||||
// wxHAS_EGL in the build — it protects against builds where
|
||||
// deps were not rebuilt.
|
||||
#if !defined(wxHAS_EGL) || !wxHAS_EGL
|
||||
{
|
||||
const char* wayland_env = ::getenv("WAYLAND_DISPLAY");
|
||||
if (wayland_env && *wayland_env) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
|
||||
::setenv("GDK_BACKEND", "x11", true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// WebKit2GTK compositing can fail under XWayland on older
|
||||
// WebKit releases. Disable it only when both DISPLAY and
|
||||
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
|
||||
// use). On pure X11 or native Wayland, compositing is left
|
||||
// enabled so WebKit retains HW acceleration.
|
||||
{
|
||||
const char* display_env_wk = ::getenv("DISPLAY");
|
||||
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
|
||||
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
|
||||
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
|
||||
}
|
||||
}
|
||||
|
||||
// XInitThreads is needed before GStreamer may use Xlib. On
|
||||
// native Wayland without DISPLAY, GStreamer uses waylandsink
|
||||
// (no Xlib involved), so the call is skipped.
|
||||
#if __has_include(<X11/Xlib.h>)
|
||||
{
|
||||
const char* display_env = ::getenv("DISPLAY");
|
||||
if (display_env && *display_env) {
|
||||
XInitThreads();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#endif // __WXGTK__
|
||||
|
||||
// Switch boost::filesystem to utf8.
|
||||
try {
|
||||
|
||||
@@ -28,17 +28,34 @@ function(encoding_check TARGET)
|
||||
# Obtain target source files
|
||||
get_target_property(T_SOURCES ${TARGET} SOURCES)
|
||||
|
||||
# Define top-level encoding check target for this ${TARGET}
|
||||
add_custom_target(encoding-check-${TARGET}
|
||||
DEPENDS encoding-check ${T_SOURCES}
|
||||
COMMAND $<TARGET_FILE:encoding-check> ${TARGET} ${T_SOURCES}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
set(STAMP_FILE "${CMAKE_CURRENT_BINARY_DIR}/encoding-check-${TARGET}.stamp")
|
||||
set(RSP_FILE "${CMAKE_CURRENT_BINARY_DIR}/encoding-check-${TARGET}.rsp")
|
||||
|
||||
# Resolve absolute paths
|
||||
set(ABS_SOURCES "")
|
||||
foreach(file ${T_SOURCES})
|
||||
if(IS_ABSOLUTE "${file}")
|
||||
list(APPEND ABS_SOURCES "${file}")
|
||||
else()
|
||||
list(APPEND ABS_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Write response file at configure time
|
||||
string(REPLACE ";" "\n" RSP_CONTENT "${ABS_SOURCES}")
|
||||
file(WRITE "${RSP_FILE}" "${RSP_CONTENT}")
|
||||
|
||||
# Single process call via response file — no command line length limit
|
||||
add_custom_command(
|
||||
OUTPUT "${STAMP_FILE}"
|
||||
DEPENDS encoding-check ${ABS_SOURCES}
|
||||
COMMENT "Checking source files encodings for target ${TARGET}"
|
||||
COMMAND $<TARGET_FILE:encoding-check> ${TARGET} "@${RSP_FILE}"
|
||||
COMMAND ${CMAKE_COMMAND} -E touch "${STAMP_FILE}"
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
|
||||
# This adds dependency on encoding-check-${TARGET} to ${TARET}
|
||||
# via the global-encoding-check
|
||||
add_custom_target(encoding-check-${TARGET} DEPENDS "${STAMP_FILE}")
|
||||
add_dependencies(global-encoding-check encoding-check-${TARGET})
|
||||
add_dependencies(${TARGET} global-encoding-check)
|
||||
endif()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <string>
|
||||
|
||||
/*
|
||||
* The utf8_check() function scans the '\0'-terminated string starting
|
||||
@@ -68,63 +68,97 @@ unsigned char *utf8_check(unsigned char *s)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
static const char* target;
|
||||
|
||||
void error_exit(const char* error, const char* filename)
|
||||
{
|
||||
std::cerr << "\n\tError: " << error << ": " << filename << "\n"
|
||||
<< "\tTarget: " << target << "\n"
|
||||
<< std::endl;
|
||||
std::exit(-2);
|
||||
}
|
||||
|
||||
|
||||
void utf8_check_file(const char* filename)
|
||||
// ORCA
|
||||
bool check_file(const char* target, const char* filename)
|
||||
{
|
||||
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
||||
const auto size = file.tellg();
|
||||
|
||||
if (size == 0) {
|
||||
return;
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "\n\tError: Could not open file: " << filename
|
||||
<< "\n\tTarget: " << target << "\n" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::streampos pos = file.tellg();
|
||||
|
||||
// Fix: tellg() returns -1 (cast to streampos) on failure.
|
||||
// Also guard against implausibly large files before casting to size_t.
|
||||
if (pos < 0) {
|
||||
std::cerr << "\n\tError: Could not determine file size: " << filename
|
||||
<< "\n\tTarget: " << target << "\n" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto size = static_cast<std::size_t>(pos);
|
||||
|
||||
if (size == 0)
|
||||
return true;
|
||||
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<char> buffer(size);
|
||||
|
||||
if (file.read(buffer.data(), size)) {
|
||||
buffer.push_back('\0');
|
||||
|
||||
// Check UTF-8 validity
|
||||
if (utf8_check(reinterpret_cast<unsigned char*>(buffer.data())) != nullptr) {
|
||||
error_exit("Source file does not contain (valid) UTF-8", filename);
|
||||
}
|
||||
|
||||
// Check against a BOM mark
|
||||
if (buffer.size() >= 3
|
||||
&& buffer[0] == '\xef'
|
||||
&& buffer[1] == '\xbb'
|
||||
&& buffer[2] == '\xbf') {
|
||||
error_exit("Source file is valid UTF-8 but contains a BOM mark", filename);
|
||||
}
|
||||
} else {
|
||||
error_exit("Could not read source file", filename);
|
||||
// Fix: cast size to streamsize explicitly — streampos and streamsize are
|
||||
// distinct types and passing streampos where streamsize is expected is
|
||||
// implementation-defined behaviour on some platforms.
|
||||
if (!file.read(buffer.data(), static_cast<std::streamsize>(size))) {
|
||||
std::cerr << "\n\tError: Could not read source file: " << filename
|
||||
<< "\n\tTarget: " << target << "\n" << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.push_back('\0');
|
||||
|
||||
if (utf8_check(reinterpret_cast<unsigned char*>(buffer.data())) != nullptr) {
|
||||
std::cerr << "\n\tError: Source file does not contain (valid) UTF-8: " << filename
|
||||
<< "\n\tTarget: " << target << "\n" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer.size() >= 3
|
||||
&& buffer[0] == '\xef'
|
||||
&& buffer[1] == '\xbb'
|
||||
&& buffer[2] == '\xbf') {
|
||||
std::cerr << "\n\tError: Source file is valid UTF-8 but contains a BOM mark: " << filename
|
||||
<< "\n\tTarget: " << target << "\n" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char const *argv[])
|
||||
{
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: " << argv[0] << " <program/library> <file[s]>" << std::endl;
|
||||
std::cerr << "Usage: " << argv[0] << " <target> <file|@responsefile> [...]" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
target = argv[1];
|
||||
const char* target = argv[1];
|
||||
|
||||
// Collect all files — support @responsefile syntax
|
||||
std::vector<std::string> files;
|
||||
for (int i = 2; i < argc; i++) {
|
||||
utf8_check_file(argv[i]);
|
||||
if (argv[i][0] == '@') {
|
||||
// Response file — read paths from it, one per line
|
||||
std::ifstream rsp(argv[i] + 1);
|
||||
if (!rsp.is_open()) {
|
||||
std::cerr << "Could not open response file: " << (argv[i]+1) << std::endl;
|
||||
return -1;
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(rsp, line)) {
|
||||
if (!line.empty())
|
||||
files.push_back(line);
|
||||
}
|
||||
} else {
|
||||
files.push_back(argv[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
bool all_ok = true;
|
||||
for (const auto& f : files) {
|
||||
if (!check_file(target, f.c_str()))
|
||||
all_ok = false;
|
||||
}
|
||||
|
||||
return all_ok ? 0 : -2;
|
||||
}
|
||||
@@ -26,7 +26,13 @@ std::string WideningBeadingStrategy::toString() const
|
||||
|
||||
WideningBeadingStrategy::Beading WideningBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const
|
||||
{
|
||||
if (thickness < optimal_width) {
|
||||
// Use getTransitionThickness(1) to determine if this is a thin wall that should produce
|
||||
// a single bead. This ensures consistency with getOptimalBeadCount() which uses the same
|
||||
// threshold (via RedistributeBeadingStrategy) to decide between 1 and 2 beads.
|
||||
// Previously used optimal_width which could differ from the outer wall width used in
|
||||
// bead count calculations, causing inconsistency where bead_count=2 was requested but
|
||||
// only 1 bead was produced.
|
||||
if (thickness < getTransitionThickness(1)) {
|
||||
Beading ret;
|
||||
ret.total_thickness = thickness;
|
||||
if (thickness >= min_input_width) {
|
||||
|
||||
@@ -107,7 +107,7 @@ public:
|
||||
double linear_defletion = 0.003,
|
||||
double angle_defletion = 0.5);
|
||||
|
||||
std::atomic<bool> m_stop_mesh;
|
||||
std::atomic<bool> m_stop_mesh{false};
|
||||
void update_process(int load_stage, int current, int total, bool& cancel);
|
||||
private:
|
||||
std::string m_path;
|
||||
|
||||
@@ -2573,6 +2573,12 @@ void ModelVolume::update_extruder_count(size_t extruder_count)
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Clear a stale per-volume filament assignment that no longer exists after the extruder count
|
||||
// shrank (e.g. printer switch to one with fewer filaments), so downstream readers never index
|
||||
// per-filament config vectors out of range. Ported from BambuStudio (STUDIO-15763).
|
||||
if (extruder_id() > extruder_count) {
|
||||
this->config.erase("extruder");
|
||||
}
|
||||
}
|
||||
|
||||
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id)
|
||||
@@ -2584,6 +2590,11 @@ void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_cou
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Same stale-assignment cleanup as update_extruder_count, for the filament-delete path.
|
||||
// Ported from BambuStudio (STUDIO-15763).
|
||||
if (extruder_id() > extruder_count) {
|
||||
this->config.erase("extruder");
|
||||
}
|
||||
}
|
||||
|
||||
void ModelVolume::center_geometry_after_creation(bool update_source_offset)
|
||||
|
||||
@@ -2370,6 +2370,10 @@ bool PresetCollection::validate_preset(const std::string &preset_name, std::stri
|
||||
const std::string canonical_inherit_name = this->canonical_preset_name(inherit_name);
|
||||
it = this->find_preset_internal(canonical_inherit_name);
|
||||
found = it != m_presets.end() && it->name == canonical_inherit_name && is_trusted(*it);
|
||||
if (!found) {
|
||||
it = this->find_preset_renamed(canonical_inherit_name);
|
||||
found = it != m_presets.end() && is_trusted(*it);
|
||||
}
|
||||
if (found)
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": preset_name %1%, inherit_name %2%, found inherit in list")%preset_name %inherit_name;
|
||||
else
|
||||
@@ -2461,7 +2465,9 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
||||
|
||||
if (!inherits.empty() && (different_settings_list.size() > 0)) {
|
||||
auto iter = this->find_preset_internal(inherits);
|
||||
if (iter != m_presets.end() && iter->name == inherits) {
|
||||
if (iter == m_presets.end() || iter->name != inherits)
|
||||
iter = this->find_preset_renamed(inherits);
|
||||
if (iter != m_presets.end()) {
|
||||
//std::vector<std::string> dirty_options = cfg.diff(iter->config);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path;
|
||||
cfg.update_non_diff_values_to_base_config(iter->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
@@ -2513,7 +2519,9 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
||||
// and override its settings with the loaded ones.
|
||||
assert(it == m_presets.end());
|
||||
it = this->find_preset_internal(inherits);
|
||||
found = it != m_presets.end() && it->name == inherits;
|
||||
if (it == m_presets.end() || it->name != inherits)
|
||||
it = this->find_preset_renamed(inherits);
|
||||
found = it != m_presets.end();
|
||||
if (found && profile_print_params_same(it->config, cfg)) {
|
||||
// The system preset exists and it matches the values stored inside config.
|
||||
if (select == LoadAndSelect::Always)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "libslic3r.h"
|
||||
#include "I18N.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
@@ -2231,6 +2232,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
|
||||
[&](const tbb::blocked_range<size_t>& range) {
|
||||
for (size_t i = range.begin(); i < range.end(); ++i) {
|
||||
auto bundle = std::make_unique<PresetBundle>();
|
||||
bundle->set_is_validation_mode(validation_mode);
|
||||
try {
|
||||
auto result = bundle->load_vendor_configs_from_json(
|
||||
dir.string(), other_vendors[i], PresetBundle::LoadSystem,
|
||||
@@ -4982,6 +4984,39 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
|
||||
reason = std::string("can not find printer_variant in vendor profile");
|
||||
return reason;
|
||||
}
|
||||
// An instantiation printer profile's nozzle_diameter must match the numeric (diameter)
|
||||
// prefix of its printer_variant: "0.4" -> {0.4}, "0.8HF" -> {0.8} (a trailing
|
||||
// non-numeric suffix such as "HF"/"HS" distinguishes a hardware sub-variant and is
|
||||
// ignored here), and for multi-nozzle printers "0.4+0.6" -> {0.4, 0.6}.
|
||||
// Note: a variant may legitimately repeat across presets of the same model (e.g. speed
|
||||
// modes, IDEX copy/mirror, or different control boards), so only the diameter is
|
||||
// validated, not variant uniqueness. Validation-only so the app keeps loading existing
|
||||
// profiles unchanged.
|
||||
if (validation_mode && instantiation == "true") {
|
||||
const auto *nd = config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
std::set<double> nozzles, variant_nozzles;
|
||||
if (nd != nullptr)
|
||||
nozzles.insert(nd->values.begin(), nd->values.end());
|
||||
std::vector<std::string> variant_tokens;
|
||||
boost::algorithm::split(variant_tokens, printer_variant, boost::algorithm::is_any_of("+"));
|
||||
bool variant_ok = true; // printer_variant is already guaranteed non-empty above
|
||||
for (const std::string &tok : variant_tokens) {
|
||||
size_t consumed = 0;
|
||||
double d = string_to_double_decimal_point(tok, &consumed);
|
||||
// Require a leading numeric diameter; a trailing suffix (e.g. "HF") is allowed.
|
||||
if (consumed == 0) { variant_ok = false; break; }
|
||||
variant_nozzles.insert(d);
|
||||
}
|
||||
if (!variant_ok || variant_nozzles != nozzles) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" <<
|
||||
preset_name << "\" has printer_variant \"" << printer_variant <<
|
||||
"\" that does not match its nozzle_diameter \"" << (nd ? nd->serialize() : std::string()) << "\". "
|
||||
"printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix "
|
||||
"(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in "
|
||||
"nozzle order (e.g. \"0.4+0.6\").";
|
||||
}
|
||||
}
|
||||
}
|
||||
const Preset *preset_existing = presets_collection->find_preset(preset_name, false);
|
||||
if (preset_existing != nullptr) {
|
||||
|
||||
@@ -1877,15 +1877,12 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check junction deviation
|
||||
// Orca: Only marlin FW supports max junction deviation. Dont display warning if firmware is not supporting it.
|
||||
const bool support_max_junction_deviation = ( m_config.gcode_flavor == gcfMarlinFirmware);
|
||||
if (warning_key.empty() && m_default_object_config.default_junction_deviation.value > max_junction_deviation && support_max_junction_deviation) {
|
||||
// check junction deviation
|
||||
else if (m_default_object_config.default_junction_deviation.value > max_junction_deviation) {
|
||||
warning->string = L( "Junction deviation setting exceeds the printer's maximum value (machine_max_junction_deviation).\n"
|
||||
"Orca will automatically cap the junction deviation to ensure it doesn't surpass the printer's capabilities.\n"
|
||||
"You can adjust the machine_max_junction_deviation value in your printer's configuration to get higher limits.");
|
||||
warning->opt_key = warning_key;
|
||||
warning->opt_key = "default_junction_deviation";
|
||||
}
|
||||
|
||||
// check acceleration
|
||||
|
||||
@@ -151,7 +151,8 @@ static t_config_enum_values s_keys_map_PrintHostType {
|
||||
{ "flashforge", htFlashforge },
|
||||
{ "simplyprint", htSimplyPrint },
|
||||
{ "elegoolink", htElegooLink },
|
||||
{ "3dprinteros", ht3DPrinterOS }
|
||||
{ "3dprinteros", ht3DPrinterOS },
|
||||
{ "moonraker", htMoonraker }
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType)
|
||||
|
||||
@@ -4912,6 +4913,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_values.push_back("simplyprint");
|
||||
def->enum_values.push_back("elegoolink");
|
||||
def->enum_values.push_back("3dprinteros");
|
||||
def->enum_values.push_back("moonraker");
|
||||
def->enum_labels.push_back("PrusaLink");
|
||||
def->enum_labels.push_back("PrusaConnect");
|
||||
def->enum_labels.push_back("Octo/Klipper");
|
||||
@@ -4927,6 +4929,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back("SimplyPrint");
|
||||
def->enum_labels.push_back("Elegoo Link");
|
||||
def->enum_labels.push_back("3DPrinterOS");
|
||||
def->enum_labels.push_back("Moonraker (Klipper)");
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionEnum<PrintHostType>(htOctoPrint));
|
||||
@@ -10563,7 +10566,19 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
|
||||
int stride = 1;
|
||||
if (key_set2.find(opt) != key_set2.end())
|
||||
stride = 2;
|
||||
opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride);
|
||||
// set_only_diff() requires the base vector length to equal variant_index.size()*stride, where
|
||||
// variant_index is sized from the parent's printer_extruder_variant list (or 1 when it has none).
|
||||
// Legacy / multi-extruder / multi-variant presets can violate this in either direction when a key's
|
||||
// stored length and the variant-list length were sized inconsistently - e.g. a Snapmaker U1 base
|
||||
// (4 nozzles) whose stride-2 machine limits were length-extended to nozzles*2 while its
|
||||
// printer_extruder_variant list is empty (variant_index == 1), so base length != variant_index*stride.
|
||||
// Rather than throw (which is caught upstream and DELETES the user preset file), fall back to the
|
||||
// child's explicit value, which is authoritative for its own extruder/variant layout.
|
||||
if (opt_vec_src->size() != variant_index.size() * size_t(stride)) {
|
||||
opt_src->set(opt_target);
|
||||
}
|
||||
else
|
||||
opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ enum class WipeTowerType {
|
||||
};
|
||||
|
||||
enum PrintHostType {
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htESP3D, htCrealityPrint, htObico, htFlashforge, htSimplyPrint, htElegooLink, ht3DPrinterOS, htMoonraker
|
||||
};
|
||||
|
||||
enum AuthorizationType {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "clipper.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "KDTreeIndirect.hpp"
|
||||
#include "MutablePriorityQueue.hpp"
|
||||
#include "Print.hpp"
|
||||
@@ -15,6 +16,30 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Orca: Some support entities may contain empty nested paths, which cannot be reordered safely.
|
||||
static bool extrusion_entity_has_endpoints(const ExtrusionEntity *entity)
|
||||
{
|
||||
auto paths_have_endpoints = [](const ExtrusionPaths &paths) {
|
||||
return !paths.empty() &&
|
||||
!paths.front().polyline.points.empty() &&
|
||||
!paths.back().polyline.points.empty();
|
||||
};
|
||||
|
||||
if (entity == nullptr)
|
||||
return false;
|
||||
if (const auto *collection = dynamic_cast<const ExtrusionEntityCollection *>(entity))
|
||||
return !collection->entities.empty() &&
|
||||
extrusion_entity_has_endpoints(collection->entities.front()) &&
|
||||
extrusion_entity_has_endpoints(collection->entities.back());
|
||||
if (const auto *path = dynamic_cast<const ExtrusionPath *>(entity))
|
||||
return !path->polyline.points.empty();
|
||||
if (const auto *multipath = dynamic_cast<const ExtrusionMultiPath *>(entity))
|
||||
return paths_have_endpoints(multipath->paths);
|
||||
if (const auto *loop = dynamic_cast<const ExtrusionLoop *>(entity))
|
||||
return paths_have_endpoints(loop->paths);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Naive implementation of the Traveling Salesman Problem, it works by always taking the next closest neighbor.
|
||||
// This implementation will always produce valid result even if some segments cannot reverse.
|
||||
template<typename EndPointType, typename KDTreeType, typename CouldReverseFunc>
|
||||
@@ -1035,8 +1060,10 @@ void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entitie
|
||||
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near)
|
||||
{
|
||||
// this function crashes if there are empty elements in entities
|
||||
entities.erase(std::remove_if(entities.begin(), entities.end(), [](ExtrusionEntity *entity) { return static_cast<ExtrusionEntityCollection *>(entity)->empty(); }),
|
||||
// Orca: Reordering queries first_point() / last_point(); drop entities that cannot provide valid endpoints.
|
||||
entities.erase(std::remove_if(entities.begin(), entities.end(), [](ExtrusionEntity *entity) {
|
||||
return !extrusion_entity_has_endpoints(entity);
|
||||
}),
|
||||
entities.end());
|
||||
reorder_extrusion_entities(entities, chain_extrusion_entities(entities, start_near));
|
||||
}
|
||||
|
||||
@@ -1733,8 +1733,8 @@ void generate_support_toolpaths(
|
||||
interface_as_base ? ExtrusionRole::erSupportMaterial : ExtrusionRole::erSupportMaterialInterface, interface_flow);
|
||||
}
|
||||
};
|
||||
const bool top_interfaces = config.support_interface_top_layers.value != 0;
|
||||
const bool bottom_interfaces = top_interfaces && config.support_interface_bottom_layers != 0;
|
||||
const bool top_interfaces = support_params.num_top_interface_layers > 0;
|
||||
const bool bottom_interfaces = top_interfaces && support_params.num_bottom_interface_layers > 0;
|
||||
extrude_interface(top_contact_layer, raft_layer ? InterfaceLayerType::RaftContact : top_interfaces ? InterfaceLayerType::TopContact : InterfaceLayerType::InterfaceAsBase);
|
||||
if (!organic_tree)
|
||||
extrude_interface(bottom_contact_layer, bottom_interfaces ? InterfaceLayerType::BottomContact : InterfaceLayerType::InterfaceAsBase);
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
#include "../Flow.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
inline int number_of_support_interface_bottom_layers(const PrintObjectConfig& object_config)
|
||||
{
|
||||
return object_config.support_interface_bottom_layers.value < 0 ?
|
||||
object_config.support_interface_top_layers.value :
|
||||
object_config.support_interface_bottom_layers.value;
|
||||
}
|
||||
|
||||
struct SupportParameters {
|
||||
SupportParameters() = delete;
|
||||
SupportParameters(const PrintObject& object)
|
||||
@@ -26,8 +34,7 @@ struct SupportParameters {
|
||||
|
||||
{
|
||||
this->num_top_interface_layers = std::max(0, object_config.support_interface_top_layers.value);
|
||||
this->num_bottom_interface_layers = object_config.support_interface_bottom_layers < 0 ?
|
||||
num_top_interface_layers : object_config.support_interface_bottom_layers;
|
||||
this->num_bottom_interface_layers = number_of_support_interface_bottom_layers(object_config);
|
||||
this->has_top_contacts = num_top_interface_layers > 0;
|
||||
this->has_bottom_contacts = num_bottom_interface_layers > 0;
|
||||
// BBS: if support interface and support base do not use the same filament, add a base layer to improve their adhesion
|
||||
|
||||
@@ -1357,7 +1357,12 @@ static void make_perimeter_and_infill(ExtrusionEntitiesPtr& dst, const ExPolygon
|
||||
dst = std::move(loops_entities);
|
||||
}
|
||||
}
|
||||
dst.erase(std::remove_if(dst.begin(), dst.end(), [](ExtrusionEntity *entity) { return static_cast<ExtrusionEntityCollection *>(entity)->empty(); }), dst.end());
|
||||
|
||||
// Orca: Some entities are direct paths, so check the type before testing for an empty collection.
|
||||
dst.erase(std::remove_if(dst.begin(), dst.end(), [](ExtrusionEntity *entity) {
|
||||
return entity != nullptr && entity->is_collection() && static_cast<ExtrusionEntityCollection *>(entity)->empty();
|
||||
}), dst.end());
|
||||
|
||||
if (infill_first) {
|
||||
// sort regions to reduce travel
|
||||
Points ordering_points;
|
||||
@@ -2109,8 +2114,8 @@ void TreeSupport::draw_circles()
|
||||
|
||||
// generate areas
|
||||
const coordf_t layer_height = config.layer_height.value;
|
||||
const size_t top_interface_layers = config.support_interface_top_layers.value;
|
||||
const size_t bottom_interface_layers = config.support_interface_bottom_layers.value < 0 ? top_interface_layers : config.support_interface_bottom_layers.value;
|
||||
const size_t top_interface_layers = m_support_params.num_top_interface_layers;
|
||||
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
|
||||
const double nozzle_diameter = m_object->print()->config().nozzle_diameter.get_at(0);
|
||||
const coordf_t line_width = config.get_abs_value("support_line_width", nozzle_diameter);
|
||||
const coordf_t line_width_scaled = scale_(line_width);
|
||||
@@ -2770,7 +2775,7 @@ void TreeSupport::drop_nodes()
|
||||
const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value;
|
||||
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON
|
||||
&& belt_floor_mode == BeltSupportFloorMode::GeneratorOnly;
|
||||
const size_t bottom_interface_layers = config.support_interface_bottom_layers.value < 0 ? top_interface_layers : config.support_interface_bottom_layers.value;
|
||||
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
|
||||
SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor;
|
||||
float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm
|
||||
|
||||
|
||||
@@ -2930,6 +2930,7 @@ static std::pair<float, float> extrude_branch(
|
||||
const TreeSupportSettings &config,
|
||||
const SlicingParameters &slicing_params,
|
||||
const std::vector<SupportElements> &move_bounds,
|
||||
bool has_root,
|
||||
indexed_triangle_set &result)
|
||||
{
|
||||
Vec3d p1, p2, p3;
|
||||
@@ -2951,24 +2952,38 @@ static std::pair<float, float> extrude_branch(
|
||||
v1 = (p2 - p1).normalized();
|
||||
if (ipath == 1) {
|
||||
nprev = v1;
|
||||
// Extrude the bottom half sphere.
|
||||
float radius = unscaled<float>(support_element_radius(config, prev));
|
||||
float angle_step = 2. * acos(1. - eps / radius);
|
||||
auto nsteps = int(ceil(M_PI / (2. * angle_step)));
|
||||
angle_step = M_PI / (2. * nsteps);
|
||||
int ifan = int(result.vertices.size());
|
||||
result.vertices.emplace_back((p1 - nprev * radius).cast<float>());
|
||||
zmin = result.vertices.back().z();
|
||||
float angle = angle_step;
|
||||
for (int i = 1; i < nsteps; ++ i, angle += angle_step) {
|
||||
std::pair<int, int> strip = discretize_circle((p1 - nprev * radius * cos(angle)).cast<float>(), nprev.cast<float>(), radius * sin(angle), eps, result.vertices);
|
||||
if (i == 1)
|
||||
triangulate_fan<false>(result, ifan, strip.first, strip.second);
|
||||
else
|
||||
triangulate_strip(result, prev_strip.first, prev_strip.second, strip.first, strip.second);
|
||||
// sprintf(fname, "d:\\temp\\meshes\\tree-partial-%d.obj", ++ irun);
|
||||
// its_write_obj(result, fname);
|
||||
prev_strip = strip;
|
||||
if (has_root && prev.state.layer_idx == 0) {
|
||||
// Orca: Buildplate roots need a flat foot. A rounded cap can extend far
|
||||
// below the bed and make the first layer slice cut unrelated trunk geometry.
|
||||
const Vec3f normal(0.f, 0.f, 1.f);
|
||||
const Vec3f bottom_center(float(p1.x()), float(p1.y()), 0.f);
|
||||
const Vec3f top_center(float(p1.x()), float(p1.y()), float(p1.z()));
|
||||
int ifan = int(result.vertices.size());
|
||||
result.vertices.emplace_back(bottom_center);
|
||||
std::pair<int, int> bottom_strip = discretize_circle(bottom_center, normal, radius, eps, result.vertices);
|
||||
triangulate_fan<false>(result, ifan, bottom_strip.first, bottom_strip.second);
|
||||
prev_strip = discretize_circle(top_center, normal, radius, eps, result.vertices);
|
||||
triangulate_strip(result, bottom_strip.first, bottom_strip.second, prev_strip.first, prev_strip.second);
|
||||
zmin = 0.f;
|
||||
} else {
|
||||
// Extrude the bottom half sphere.
|
||||
float angle_step = 2. * acos(1. - eps / radius);
|
||||
auto nsteps = int(ceil(M_PI / (2. * angle_step)));
|
||||
angle_step = M_PI / (2. * nsteps);
|
||||
int ifan = int(result.vertices.size());
|
||||
result.vertices.emplace_back((p1 - nprev * radius).cast<float>());
|
||||
zmin = result.vertices.back().z();
|
||||
float angle = angle_step;
|
||||
for (int i = 1; i < nsteps; ++ i, angle += angle_step) {
|
||||
std::pair<int, int> strip = discretize_circle((p1 - nprev * radius * cos(angle)).cast<float>(), nprev.cast<float>(), radius * sin(angle), eps, result.vertices);
|
||||
if (i == 1)
|
||||
triangulate_fan<false>(result, ifan, strip.first, strip.second);
|
||||
else
|
||||
triangulate_strip(result, prev_strip.first, prev_strip.second, strip.first, strip.second);
|
||||
|
||||
prev_strip = strip;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ipath + 1 == path.size()) {
|
||||
@@ -3151,13 +3166,60 @@ static void organic_smooth_branches_avoid_collisions(
|
||||
static constexpr const double max_nudge_collision_avoidance = 0.5;
|
||||
static constexpr const double max_nudge_smoothing = 0.2;
|
||||
static constexpr const size_t num_iter = 100; // 1000;
|
||||
|
||||
// Orca:
|
||||
// Collision and Laplacian smoothing run iteratively; keep each candidate reachable from linked upper/lower layers to avoid accumulated drift.
|
||||
auto limit_candidate_to_linked_layers = [&collision_spheres, &linear_data_layers, &config](const size_t collision_sphere_id, Vec2d candidate) {
|
||||
auto constrain_to_anchor = [](Vec2d candidate, const Vec2d ¤t_pos, const Vec2d &anchor, double allowed_shift) {
|
||||
const Vec2d delta = candidate - anchor;
|
||||
const double candidate_dist = delta.norm();
|
||||
const double current_dist = (current_pos - anchor).norm();
|
||||
allowed_shift = std::max(allowed_shift, current_dist);
|
||||
return candidate_dist > allowed_shift && candidate_dist > EPSILON ?
|
||||
anchor + delta * (allowed_shift / candidate_dist) :
|
||||
candidate;
|
||||
};
|
||||
|
||||
const CollisionSphere &sphere = collision_spheres[collision_sphere_id];
|
||||
const LayerIndex layer_idx = sphere.element.state.layer_idx;
|
||||
const Vec2d current_pos = to_2d(sphere.position).cast<double>();
|
||||
const double current_radius = double(support_element_radius(config, sphere.element));
|
||||
const double maximum_move_distance_slow = double(config.maximum_move_distance_slow);
|
||||
|
||||
if (sphere.element_below_id != -1 && layer_idx > 0) {
|
||||
const size_t lower_id = linear_data_layers[layer_idx - 1] + size_t(sphere.element_below_id);
|
||||
if (lower_id < collision_spheres.size()) {
|
||||
const CollisionSphere &lower = collision_spheres[lower_id];
|
||||
const double lower_radius = double(support_element_radius(config, lower.element));
|
||||
const double allowed_shift = unscaled<double>(std::max(0., lower_radius - current_radius) + maximum_move_distance_slow);
|
||||
candidate = constrain_to_anchor(candidate, current_pos, to_2d(lower.prev_position).cast<double>(), allowed_shift);
|
||||
}
|
||||
}
|
||||
|
||||
const LayerIndex upper_layer_idx = layer_idx + 1;
|
||||
if (!sphere.element.parents.empty() && upper_layer_idx < LayerIndex(linear_data_layers.size())) {
|
||||
const size_t upper_offset = linear_data_layers[upper_layer_idx];
|
||||
for (int32_t parent_idx : sphere.element.parents) {
|
||||
const size_t upper_id = upper_offset + size_t(parent_idx);
|
||||
if (upper_id >= collision_spheres.size())
|
||||
continue;
|
||||
const CollisionSphere &upper = collision_spheres[upper_id];
|
||||
const double upper_radius = double(support_element_radius(config, upper.element));
|
||||
const double allowed_shift = unscaled<double>(std::max(0., current_radius - upper_radius) + maximum_move_distance_slow);
|
||||
candidate = constrain_to_anchor(candidate, current_pos, to_2d(upper.prev_position).cast<double>(), allowed_shift);
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
};
|
||||
|
||||
for (size_t iter = 0; iter < num_iter; ++ iter) {
|
||||
// Back up prev position before Laplacian smoothing.
|
||||
for (CollisionSphere &collision_sphere : collision_spheres)
|
||||
collision_sphere.prev_position = collision_sphere.position;
|
||||
std::atomic<size_t> num_moved{ 0 };
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, collision_spheres.size()),
|
||||
[&collision_spheres, &layer_collision_cache, &slicing_params, &config, &linear_data_layers, &num_moved, &throw_on_cancel](const tbb::blocked_range<size_t> range) {
|
||||
[&collision_spheres, &layer_collision_cache, &slicing_params, &config, &linear_data_layers, &num_moved, &throw_on_cancel, &limit_candidate_to_linked_layers](const tbb::blocked_range<size_t> range) {
|
||||
for (size_t collision_sphere_id = range.begin(); collision_sphere_id < range.end(); ++ collision_sphere_id)
|
||||
if (CollisionSphere &collision_sphere = collision_spheres[collision_sphere_id]; ! collision_sphere.locked) {
|
||||
// Calculate collision of multiple 2D layers against a collision sphere.
|
||||
@@ -3186,10 +3248,12 @@ static void organic_smooth_branches_avoid_collisions(
|
||||
if (collision_sphere.last_collision_depth > EPSILON)
|
||||
// a little bit of hysteresis to detect end of
|
||||
++ num_moved;
|
||||
// Shift by maximum 2mm.
|
||||
// Limit collision-avoidance nudge per iteration.
|
||||
double nudge_dist = std::min(std::max(0., collision_sphere.last_collision_depth + collision_extra_gap), max_nudge_collision_avoidance);
|
||||
Vec2d nudge_vector = (to_2d(collision_sphere.position) - to_2d(collision_sphere.last_collision)).cast<double>().normalized() * nudge_dist;
|
||||
collision_sphere.position.head<2>() += (nudge_vector * nudge_dist).cast<float>();
|
||||
Vec2d candidate = to_2d(collision_sphere.position).cast<double>() + nudge_vector * nudge_dist;
|
||||
candidate = limit_candidate_to_linked_layers(collision_sphere_id, candidate);
|
||||
collision_sphere.position.head<2>() = candidate.cast<float>();
|
||||
}
|
||||
// Laplacian smoothing
|
||||
Vec2d avg{ 0, 0 };
|
||||
@@ -3213,9 +3277,13 @@ static void organic_smooth_branches_avoid_collisions(
|
||||
Vec2d new_pos = (1. - smoothing_factor) * old_pos + smoothing_factor * avg;
|
||||
Vec2d shift = new_pos - old_pos;
|
||||
double nudge_dist_max = shift.norm();
|
||||
// Shift by maximum 1mm, less than the collision avoidance factor.
|
||||
// Limit Laplacian smoothing nudge per iteration.
|
||||
double nudge_dist = std::min(std::max(0., nudge_dist_max), max_nudge_smoothing);
|
||||
collision_sphere.position.head<2>() += (shift.normalized() * nudge_dist).cast<float>();
|
||||
if (nudge_dist > 0.) {
|
||||
Vec2d candidate = old_pos + shift * (nudge_dist / nudge_dist_max);
|
||||
candidate = limit_candidate_to_linked_layers(collision_sphere_id, candidate);
|
||||
collision_sphere.position.head<2>() = candidate.cast<float>();
|
||||
}
|
||||
|
||||
throw_on_cancel();
|
||||
}
|
||||
@@ -3860,7 +3928,7 @@ void organic_draw_branches(
|
||||
for (const Branch &branch : tree.branches) {
|
||||
// Triangulate the tube.
|
||||
partial_mesh.clear();
|
||||
std::pair<float, float> zspan = extrude_branch(branch.path, config, slicing_params, move_bounds, partial_mesh);
|
||||
std::pair<float, float> zspan = extrude_branch(branch.path, config, slicing_params, move_bounds, branch.has_root, partial_mesh);
|
||||
LayerIndex layer_begin = branch.has_root ?
|
||||
branch.path.front()->state.layer_idx :
|
||||
std::min(branch.path.front()->state.layer_idx, layer_idx_ceil(slicing_params, config, zspan.first));
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "../Polygon.hpp"
|
||||
#include "SupportCommon.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
|
||||
namespace Slic3r
|
||||
@@ -60,12 +61,10 @@ struct TreeSupportMeshGroupSettings {
|
||||
this->support_angle = 0.5 * M_PI - std::clamp<double>((config.support_threshold_angle + 1) * M_PI / 180., 0., 0.5 * M_PI);
|
||||
this->support_line_width = support_material_flow(&print_object, config.layer_height).scaled_width();
|
||||
this->support_roof_line_width = support_material_interface_flow(&print_object, config.layer_height).scaled_width();
|
||||
//FIXME add it to SlicingParameters and reuse in both tree and normal supports?
|
||||
this->support_bottom_enable = config.support_interface_top_layers.value > 0 && config.support_interface_bottom_layers.value != 0;
|
||||
const int bottom_interface_layers = number_of_support_interface_bottom_layers(config);
|
||||
this->support_bottom_enable = config.support_interface_top_layers.value > 0 && bottom_interface_layers > 0;
|
||||
this->support_bottom_height = this->support_bottom_enable ?
|
||||
(config.support_interface_bottom_layers.value > 0 ?
|
||||
config.support_interface_bottom_layers.value :
|
||||
config.support_interface_top_layers.value) * this->layer_height :
|
||||
bottom_interface_layers * this->layer_height :
|
||||
0;
|
||||
this->support_material_buildplate_only = config.support_on_build_plate_only;
|
||||
this->support_xy_distance = scaled<coord_t>(config.support_object_xy_distance.value);
|
||||
@@ -76,8 +75,8 @@ struct TreeSupportMeshGroupSettings {
|
||||
this->support_bottom_distance = scaled<coord_t>(slicing_params.gap_object_support);
|
||||
this->support_roof_enable = config.support_interface_top_layers.value > 0;
|
||||
this->support_roof_layers = config.support_interface_top_layers.value;
|
||||
this->support_floor_enable = config.support_interface_bottom_layers.value > 0;
|
||||
this->support_floor_layers = config.support_interface_bottom_layers.value;
|
||||
this->support_floor_enable = bottom_interface_layers > 0;
|
||||
this->support_floor_layers = bottom_interface_layers;
|
||||
this->support_roof_pattern = config.support_interface_pattern;
|
||||
this->support_pattern = config.support_base_pattern;
|
||||
this->support_line_spacing = scaled<coord_t>(config.support_base_pattern_spacing.value);
|
||||
@@ -614,19 +613,35 @@ inline double layer_z(const SlicingParameters &slicing_params, const TreeSupport
|
||||
slicing_params.object_print_z_min + slicing_params.first_object_layer_height + (layer_idx - config.raft_layers.size()) * slicing_params.layer_height :
|
||||
config.raft_layers[layer_idx];
|
||||
}
|
||||
|
||||
inline double first_object_support_layer_z(const SlicingParameters &slicing_params)
|
||||
{
|
||||
return slicing_params.object_print_z_min + slicing_params.first_object_layer_height;
|
||||
}
|
||||
|
||||
// Orca: Reverse layer_z() for support layers below the first object layer.
|
||||
// config.raft_layers may include raft/contact/intermediate support Zs, so do not collapse them to the first object support layer.
|
||||
// Lowest collision layer
|
||||
inline LayerIndex layer_idx_ceil(const SlicingParameters &slicing_params, const TreeSupportSettings &config, const double z)
|
||||
{
|
||||
return
|
||||
LayerIndex(config.raft_layers.size()) +
|
||||
std::max<LayerIndex>(0, ceil((z - slicing_params.object_print_z_min - slicing_params.first_object_layer_height) / slicing_params.layer_height));
|
||||
const double first_object_z = first_object_support_layer_z(slicing_params);
|
||||
if (!config.raft_layers.empty() && z < first_object_z - EPSILON) {
|
||||
auto it = std::lower_bound(config.raft_layers.begin(), config.raft_layers.end(), z - EPSILON);
|
||||
return LayerIndex(it == config.raft_layers.end() ? config.raft_layers.size() : std::distance(config.raft_layers.begin(), it));
|
||||
}
|
||||
return LayerIndex(config.raft_layers.size()) +
|
||||
std::max<LayerIndex>(0, LayerIndex(std::ceil((z - first_object_z) / slicing_params.layer_height)));
|
||||
}
|
||||
// Highest collision layer
|
||||
inline LayerIndex layer_idx_floor(const SlicingParameters &slicing_params, const TreeSupportSettings &config, const double z)
|
||||
{
|
||||
return
|
||||
LayerIndex(config.raft_layers.size()) +
|
||||
std::max<LayerIndex>(0, floor((z - slicing_params.object_print_z_min - slicing_params.first_object_layer_height) / slicing_params.layer_height));
|
||||
const double first_object_z = first_object_support_layer_z(slicing_params);
|
||||
if (!config.raft_layers.empty() && z < first_object_z - EPSILON) {
|
||||
auto it = std::upper_bound(config.raft_layers.begin(), config.raft_layers.end(), z + EPSILON);
|
||||
return LayerIndex(it == config.raft_layers.begin() ? 0 : std::distance(config.raft_layers.begin(), it) - 1);
|
||||
}
|
||||
return LayerIndex(config.raft_layers.size()) +
|
||||
std::max<LayerIndex>(0, LayerIndex(std::floor((z - first_object_z) / slicing_params.layer_height)));
|
||||
}
|
||||
|
||||
inline SupportGeneratorLayer& layer_initialize(
|
||||
|
||||
@@ -194,6 +194,7 @@ std::string debug_out_path(const char *name, ...);
|
||||
// smaller level means less log. level=5 means saving all logs.
|
||||
void set_log_path_and_level(const std::string& file, unsigned int level);
|
||||
void flush_logs();
|
||||
boost::filesystem::path get_log_file_name();
|
||||
|
||||
// A special type for strings encoded in the local Windows 8-bit code page.
|
||||
// This type is only needed for Perl bindings to relay to Perl that the string is raw, not UTF-8 encoded.
|
||||
|
||||
@@ -392,6 +392,14 @@ void flush_logs()
|
||||
return;
|
||||
}
|
||||
|
||||
// ORCA
|
||||
boost::filesystem::path get_log_file_name()
|
||||
{
|
||||
if (g_log_sink)
|
||||
return g_log_sink->locked_backend()->get_current_file_name();
|
||||
return {};
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// The following helpers are borrowed from the LLVM project https://github.com/llvm
|
||||
namespace WindowsSupport
|
||||
|
||||
@@ -56,6 +56,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/BitmapComboBox.hpp
|
||||
GUI/BonjourDialog.cpp
|
||||
GUI/BonjourDialog.hpp
|
||||
GUI/CrealityDiscoveryDialog.cpp
|
||||
GUI/CrealityDiscoveryDialog.hpp
|
||||
GUI/calib_dlg.cpp
|
||||
GUI/calib_dlg.hpp
|
||||
GUI/Calibration.cpp
|
||||
@@ -466,6 +468,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/TextLines.hpp
|
||||
GUI/TickCode.cpp
|
||||
GUI/TickCode.hpp
|
||||
GUI/TroubleshootDialog.cpp
|
||||
GUI/TroubleshootDialog.hpp
|
||||
GUI/UnsavedChangesDialog.cpp
|
||||
GUI/UnsavedChangesDialog.hpp
|
||||
GUI/UpdateDialogs.cpp
|
||||
@@ -580,6 +584,10 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/ColorSpaceConvert.hpp
|
||||
Utils/CrealityPrint.cpp
|
||||
Utils/CrealityPrint.hpp
|
||||
Utils/CrealityPrintAgent.cpp
|
||||
Utils/CrealityPrintAgent.hpp
|
||||
Utils/CrealityHostDiscovery.cpp
|
||||
Utils/CrealityHostDiscovery.hpp
|
||||
Utils/Duet.cpp
|
||||
Utils/Duet.hpp
|
||||
Utils/ElegooLink.cpp
|
||||
@@ -626,6 +634,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/SnapmakerPrinterAgent.hpp
|
||||
Utils/MoonrakerPrinterAgent.cpp
|
||||
Utils/MoonrakerPrinterAgent.hpp
|
||||
Utils/Moonraker.cpp
|
||||
Utils/Moonraker.hpp
|
||||
Utils/BBLCloudServiceAgent.cpp
|
||||
Utils/BBLCloudServiceAgent.hpp
|
||||
Utils/BBLPrinterAgent.cpp
|
||||
@@ -754,7 +764,7 @@ else()
|
||||
set(_opengl_link_lib OpenGL::GL)
|
||||
endif()
|
||||
|
||||
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise)
|
||||
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise)
|
||||
|
||||
|
||||
if (MSVC)
|
||||
|
||||
@@ -807,6 +807,8 @@ wxAuiToolBarItem* BBLTopbar::FindToolByCurrentPosition()
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
#include <windowsx.h>
|
||||
|
||||
WXLRESULT CenteredTitle::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
||||
{
|
||||
switch (nMsg) {
|
||||
@@ -823,6 +825,8 @@ WXLRESULT BBLTopbar::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam
|
||||
{
|
||||
switch (nMsg) {
|
||||
case WM_NCHITTEST: {
|
||||
m_last_mouse_position = ScreenToClient({GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)});
|
||||
|
||||
wxAuiToolBarItem* item = this->FindToolByCurrentPosition();
|
||||
if (item != NULL && item->GetWindow() != m_title_ctrl) {
|
||||
break;
|
||||
|
||||
@@ -571,7 +571,11 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
|
||||
{
|
||||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||||
|
||||
auto gcflavor = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value;
|
||||
const GCodeFlavor gcflavor = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value;
|
||||
|
||||
// Orca: use booleans to avoid repeated comparisons with enum values
|
||||
const bool gcf_is_marlin_firmware = gcflavor == GCodeFlavor::gcfMarlinFirmware;
|
||||
const bool gcf_is_klipper = gcflavor == GCodeFlavor::gcfKlipper;
|
||||
|
||||
// Belt printer: detect early since it affects multiple toggle decisions below.
|
||||
bool is_belt_printer = false;
|
||||
@@ -683,23 +687,28 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
|
||||
"top_surface_acceleration", "travel_acceleration", "bridge_acceleration", "sparse_infill_acceleration", "internal_solid_infill_acceleration"})
|
||||
toggle_field(el, have_default_acceleration);
|
||||
|
||||
bool machine_supports_junction_deviation = false;
|
||||
if (gcflavor == gcfMarlinFirmware) {
|
||||
if (const auto *machine_jd = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloats>("machine_max_junction_deviation")) {
|
||||
machine_supports_junction_deviation = !machine_jd->values.empty() && machine_jd->values.front() > 0.0;
|
||||
}
|
||||
}
|
||||
toggle_line("default_junction_deviation", gcflavor == gcfMarlinFirmware);
|
||||
if (machine_supports_junction_deviation) {
|
||||
toggle_field("default_junction_deviation", true);
|
||||
toggle_field("default_jerk", false);
|
||||
for (auto el : { "outer_wall_jerk", "inner_wall_jerk", "initial_layer_jerk", "initial_layer_travel_jerk", "top_surface_jerk", "travel_jerk", "infill_jerk"})
|
||||
const bool junction_deviation_enabled =
|
||||
gcf_is_marlin_firmware &&
|
||||
[&]() {
|
||||
const auto *machine_jd = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloats>("machine_max_junction_deviation");
|
||||
return machine_jd && !machine_jd->values.empty() && machine_jd->values.front() > 0.0;
|
||||
}();
|
||||
|
||||
toggle_line("default_junction_deviation", gcf_is_marlin_firmware);
|
||||
toggle_field("default_junction_deviation", junction_deviation_enabled);
|
||||
toggle_field("default_jerk", !junction_deviation_enabled);
|
||||
|
||||
const std::initializer_list<const char*> jerk_options = {
|
||||
"outer_wall_jerk", "inner_wall_jerk", "initial_layer_jerk",
|
||||
"initial_layer_travel_jerk", "top_surface_jerk", "travel_jerk", "infill_jerk"
|
||||
};
|
||||
|
||||
if (junction_deviation_enabled) {
|
||||
for (auto el : jerk_options)
|
||||
toggle_line(el, false);
|
||||
} else {
|
||||
toggle_field("default_junction_deviation", false);
|
||||
toggle_field("default_jerk", true);
|
||||
bool have_default_jerk = config->has("default_jerk") && config->opt_float("default_jerk") > 0;
|
||||
for (auto el : { "outer_wall_jerk", "inner_wall_jerk", "initial_layer_jerk", "initial_layer_travel_jerk", "top_surface_jerk", "travel_jerk", "infill_jerk"}) {
|
||||
const bool have_default_jerk = config->has("default_jerk") && config->opt_float("default_jerk") > 0;
|
||||
for (auto el : jerk_options) {
|
||||
toggle_line(el, true);
|
||||
toggle_field(el, have_default_jerk);
|
||||
}
|
||||
@@ -922,8 +931,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
|
||||
toggle_field("wipe_speed",!is_role_based_wipe_speed);
|
||||
|
||||
for (auto el : {"accel_to_decel_enable", "accel_to_decel_factor"})
|
||||
toggle_line(el, gcflavor == gcfKlipper);
|
||||
if(gcflavor == gcfKlipper)
|
||||
toggle_line(el, gcf_is_klipper);
|
||||
if(gcf_is_klipper)
|
||||
toggle_field("accel_to_decel_factor", config->opt_bool("accel_to_decel_enable"));
|
||||
|
||||
bool have_make_overhang_printable = config->opt_bool("make_overhang_printable");
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "CrealityDiscoveryDialog.hpp"
|
||||
#include "slic3r/Utils/CrealityHostDiscovery.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Widgets/DialogButtons.hpp"
|
||||
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/listctrl.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/utils.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
CrealityDiscoveryDialog::CrealityDiscoveryDialog(wxWindow* parent)
|
||||
: wxDialog(parent, wxID_ANY, _L("Detect Creality K-series printer"),
|
||||
wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
|
||||
{
|
||||
const int em = wxGetApp().em_unit();
|
||||
|
||||
m_status = new wxStaticText(this, wxID_ANY, _L("Click Scan to look for K-series printers on your network."));
|
||||
|
||||
m_list = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
|
||||
wxLC_REPORT | wxSIMPLE_BORDER | wxLC_SINGLE_SEL);
|
||||
m_list->SetMinSize(wxSize(50 * em, 18 * em));
|
||||
m_list->AppendColumn(_L("Model"), wxLIST_FORMAT_LEFT, 8 * em);
|
||||
m_list->AppendColumn(_L("Hostname"), wxLIST_FORMAT_LEFT, 14 * em);
|
||||
m_list->AppendColumn(_L("IP"), wxLIST_FORMAT_LEFT, 14 * em);
|
||||
|
||||
auto* dlg_btns = new DialogButtons(this, {"Scan", "OK", "Cancel"}, "", 1 /*left_aligned*/);
|
||||
auto* ok_btn = dlg_btns->GetOK();
|
||||
ok_btn->SetLabel(_L("Use Selected"));
|
||||
ok_btn->Disable();
|
||||
|
||||
auto* vsizer = new wxBoxSizer(wxVERTICAL);
|
||||
vsizer->Add(m_status, 0, wxEXPAND | wxALL, em);
|
||||
vsizer->Add(m_list, 1, wxEXPAND | wxALL, em);
|
||||
vsizer->Add(dlg_btns, 0, wxEXPAND);
|
||||
SetSizerAndFit(vsizer);
|
||||
|
||||
dlg_btns->GetFIRST()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { run_discovery(); });
|
||||
|
||||
m_list->Bind(wxEVT_LIST_ITEM_SELECTED, [ok_btn](wxListEvent&) { ok_btn->Enable(); });
|
||||
m_list->Bind(wxEVT_LIST_ITEM_DESELECTED, [ok_btn](wxListEvent&) { ok_btn->Disable(); });
|
||||
m_list->Bind(wxEVT_LIST_ITEM_ACTIVATED, [this](wxListEvent&) { on_ok(); EndModal(wxID_OK); });
|
||||
|
||||
ok_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_ok(); EndModal(wxID_OK); });
|
||||
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
|
||||
|
||||
wxGetApp().UpdateDlgDarkUI(this);
|
||||
|
||||
// Run discovery synchronously from the ctor so results are ready by the
|
||||
// time the dialog is shown. Posting an async CallAfter from a ShowModal
|
||||
// override risked the event firing after the modal loop had exited -- the
|
||||
// captured `this` would then be a dangling stack pointer and subsequent
|
||||
// UI access could fault.
|
||||
run_discovery();
|
||||
}
|
||||
|
||||
void CrealityDiscoveryDialog::run_discovery()
|
||||
{
|
||||
m_status->SetLabel(_L("Scanning the LAN for K-series printers... this takes a few seconds."));
|
||||
m_list->DeleteAllItems();
|
||||
m_rows.clear();
|
||||
Layout();
|
||||
Update();
|
||||
|
||||
std::vector<CrealityHost> hosts;
|
||||
{
|
||||
wxBusyCursor cursor;
|
||||
wxWindowDisabler disabler(this);
|
||||
hosts = CrealityHostDiscovery::scan(/*probe_info=*/true);
|
||||
}
|
||||
|
||||
for (const auto& h : hosts) {
|
||||
Row row;
|
||||
row.ip = h.ip;
|
||||
row.hostname = h.hostname;
|
||||
if (!h.model_name.empty())
|
||||
row.model = h.model_name;
|
||||
else if (h.cfs_capable)
|
||||
row.model = "(unknown K-series)";
|
||||
else
|
||||
row.model = "Creality";
|
||||
m_rows.push_back(std::move(row));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_rows.size(); ++i) {
|
||||
long idx = m_list->InsertItem(i, wxString::FromUTF8(m_rows[i].model));
|
||||
m_list->SetItem(idx, 1, wxString::FromUTF8(m_rows[i].hostname));
|
||||
m_list->SetItem(idx, 2, wxString::FromUTF8(m_rows[i].ip));
|
||||
}
|
||||
|
||||
if (m_rows.empty()) {
|
||||
m_status->SetLabel(_L(
|
||||
"No K-series printers found. Make sure the printer is on the same "
|
||||
"network and not blocked by Wi-Fi client isolation, then click Scan again."));
|
||||
} else {
|
||||
m_status->SetLabel(wxString::Format(
|
||||
_L("Found %zu Creality printer(s). Select one and click Use Selected."),
|
||||
m_rows.size()));
|
||||
m_list->Select(0);
|
||||
}
|
||||
}
|
||||
|
||||
void CrealityDiscoveryDialog::on_ok()
|
||||
{
|
||||
auto sel = m_list->GetFirstSelected();
|
||||
if (sel >= 0 && sel < int(m_rows.size())) {
|
||||
m_selected_ip = m_rows[sel].ip;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,49 @@
|
||||
#ifndef slic3r_CrealityDiscoveryDialog_hpp_
|
||||
#define slic3r_CrealityDiscoveryDialog_hpp_
|
||||
|
||||
#include <wx/dialog.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class wxListView;
|
||||
class wxStaticText;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// Modal dialog that finds Creality K-series printers on the LAN via DNS-SD
|
||||
// mDNS (vendored mjansson/mdns + cxmdns wrapper) and lets the user pick one.
|
||||
//
|
||||
// Discovery is synchronous (~5 second mDNS listen + ~2-4 sec /info probe per
|
||||
// host) and runs during ShowModal() with a busy cursor. The user-facing busy
|
||||
// time is bounded by the mDNS listener's 5-second deadline plus any in-flight
|
||||
// probes; in practice 5-10 seconds total for a typical LAN with one K2.
|
||||
//
|
||||
// After ShowModal() returns wxID_OK, selected_ip() yields the chosen
|
||||
// printer's IPv4 address. Returns the empty string on Cancel or if no match
|
||||
// was found.
|
||||
class CrealityDiscoveryDialog : public wxDialog
|
||||
{
|
||||
public:
|
||||
CrealityDiscoveryDialog(wxWindow* parent);
|
||||
~CrealityDiscoveryDialog() override = default;
|
||||
|
||||
std::string selected_ip() const { return m_selected_ip; }
|
||||
|
||||
private:
|
||||
void run_discovery();
|
||||
void on_ok();
|
||||
|
||||
struct Row { std::string ip; std::string model; std::string hostname; };
|
||||
|
||||
wxListView* m_list = nullptr;
|
||||
wxStaticText* m_status = nullptr;
|
||||
std::vector<Row> m_rows;
|
||||
std::string m_selected_ip;
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -2647,8 +2647,14 @@ void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessor
|
||||
{
|
||||
auto plate_print_statistics = plate->get_slice_result()->print_statistics;
|
||||
auto plate_extruders = plate->get_extruders(true);
|
||||
auto max_extruders_colors = wxGetApp().plater()->get_extruders_colors().size();
|
||||
for (size_t extruder_id : plate_extruders) {
|
||||
extruder_id -= 1;
|
||||
// Skip stale/overflow extruder indices (e.g. from object assignments that outlived a
|
||||
// filament-count change) so downstream per-extruder lookups stay in range. Ported
|
||||
// from BambuStudio (STUDIO-15763).
|
||||
if (extruder_id >= max_extruders_colors)
|
||||
continue;
|
||||
if (plate_print_statistics.model_volumes_per_extruder.find(extruder_id) == plate_print_statistics.model_volumes_per_extruder.end())
|
||||
model_volume_of_extruders_all_plates[extruder_id] += 0;
|
||||
else {
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
#include "ParamsDialog.hpp"
|
||||
#include "KBShortcutsDialog.hpp"
|
||||
#include "DownloadProgressDialog.hpp"
|
||||
#include "TroubleshootDialog.hpp"
|
||||
|
||||
#include "BitmapCache.hpp"
|
||||
#include "Notebook.hpp"
|
||||
@@ -4116,6 +4117,12 @@ void GUI_App::keyboard_shortcuts()
|
||||
dlg.ShowModal();
|
||||
}
|
||||
|
||||
void GUI_App::troubleshoot()
|
||||
{
|
||||
TroubleshootDialog dlg;
|
||||
if (dlg.ShowModal() == wxID_REMOVE)
|
||||
wxGetApp().mainframe->Close(false);
|
||||
}
|
||||
|
||||
void GUI_App::ShowUserGuide() {
|
||||
// BBS:Show NewUser Guide
|
||||
@@ -4933,6 +4940,7 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
// Parse the conflict body to extract the error code and server profile id
|
||||
int conflict_code = 0;
|
||||
std::string conflict_setting_id;
|
||||
std::string conflict_preset_name;
|
||||
try {
|
||||
json conflict_body = json::parse(body_str);
|
||||
if (conflict_body.contains("code"))
|
||||
@@ -4940,21 +4948,40 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
if (conflict_body.contains("server_profile") && conflict_body["server_profile"].contains("id")
|
||||
&& conflict_body["server_profile"]["id"].is_string())
|
||||
conflict_setting_id = conflict_body["server_profile"]["id"].get<std::string>();
|
||||
// The local preset name is injected into the conflict body by the agent (sync_push),
|
||||
// since the server response itself omits it for tombstone (-3) conflicts.
|
||||
if (conflict_body.contains("name") && conflict_body["name"].is_string())
|
||||
conflict_preset_name = conflict_body["name"].get<std::string>();
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to parse 409 conflict body.";
|
||||
}
|
||||
// Capture the user id up front so the force-push closure does not have to touch m_agent.
|
||||
std::string conflict_user_id = m_agent ? m_agent->get_user_id() : std::string();
|
||||
auto* plater = wxGetApp().plater();
|
||||
if (plater != nullptr && wxGetApp().imgui()->display_initialized()) {
|
||||
std::string text;
|
||||
if (conflict_code == -1) {
|
||||
|
||||
switch (conflict_code) {
|
||||
case -1:
|
||||
text = _u8L("Cloud sync conflict: this preset has a newer version in OrcaCloud.\n"
|
||||
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
|
||||
} else {
|
||||
break;
|
||||
case -2:
|
||||
text = _u8L("Cloud sync conflict: a preset with this name already exists in OrcaCloud.\n"
|
||||
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
|
||||
}
|
||||
break;
|
||||
case -3:
|
||||
text = _u8L("Cloud sync conflict: a preset with the same name was previously deleted from the cloud.\n"
|
||||
"Delete will delete your local preset. Force push overwrites it with your local preset.");
|
||||
break;
|
||||
default:
|
||||
text = _u8L("Cloud sync conflict: there was an unexpected or unidentified preset conflict.\n"
|
||||
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
|
||||
break;
|
||||
};
|
||||
|
||||
plater->get_notification_manager()->push_orca_sync_conflict_notification(
|
||||
text,
|
||||
text, conflict_code,
|
||||
[this](wxEvtHandler*) {
|
||||
// Runs on the GUI thread (on_http_error is a queued wx event); restart_sync_user_preset()
|
||||
// already joins the old sync thread off the UI thread, so no extra thread is needed here.
|
||||
@@ -4964,7 +4991,7 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
restart_sync_user_preset();
|
||||
return true;
|
||||
},
|
||||
[this, conflict_setting_id](wxEvtHandler*) {
|
||||
[this, conflict_setting_id, conflict_preset_name, conflict_user_id](wxEvtHandler*) {
|
||||
if (mainframe == nullptr)
|
||||
return false;
|
||||
MessageDialog
|
||||
@@ -4974,7 +5001,13 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
return false;
|
||||
|
||||
force_push_conflicting_preset(conflict_setting_id);
|
||||
std::string setting_id = conflict_setting_id;
|
||||
if (setting_id.empty()) {
|
||||
setting_id = OrcaCloudServiceAgent::generate_uuid_for_setting_id(conflict_preset_name, conflict_user_id);
|
||||
BOOST_LOG_TRIVIAL(info) << "conflict setting id empty, generated one: " << setting_id;
|
||||
}
|
||||
|
||||
force_push_conflicting_preset(setting_id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -6153,7 +6186,7 @@ bool GUI_App::maybe_migrate_user_presets_on_login()
|
||||
wxString source_description;
|
||||
if (source_is_bbl) {
|
||||
source_description = wxString::Format(
|
||||
_L("your Bambu Cloud profile (user ID: \"%s\")"),
|
||||
_L("your Orca Cloud profile (user ID: \"%s\")"),
|
||||
from_u8(source_dir.filename().string()));
|
||||
} else if (source_is_default) {
|
||||
source_description = _L("your default profile");
|
||||
@@ -7048,15 +7081,26 @@ void GUI_App::force_push_conflicting_preset(const std::string& setting_id)
|
||||
m_pending_conflict_setting_ids.push_back(setting_id);
|
||||
}
|
||||
|
||||
const std::string user_id = m_agent ? m_agent->get_user_id() : std::string();
|
||||
|
||||
// The 409 left this preset on "hold", which get_user_presets() skips. Restore it to
|
||||
// "update" so the next push-sync re-includes it and consumes the queued force flag.
|
||||
// (We must NOT pull from the cloud here as the Pull path does — that would overwrite
|
||||
// the local changes the user is trying to force-push.)
|
||||
// For a -3 tombstone on a newly created preset the on-disk setting_id is EMPTY (it only
|
||||
// gets assigned after a successful first push), so derive it on the fly from the preset
|
||||
// name and stamp it onto the preset — otherwise sync_with_lock's `id == preset.setting_id`
|
||||
// check never fires and the force-push silently no-ops.
|
||||
PresetCollection* collections[] = {&preset_bundle->prints, &preset_bundle->filaments, &preset_bundle->printers};
|
||||
for (PresetCollection* coll : collections) {
|
||||
for (const Preset& preset : coll->get_presets()) {
|
||||
if (preset.setting_id == setting_id && preset.sync_info == "hold") {
|
||||
coll->set_sync_info_and_save(preset.name, preset.setting_id, "update", 0);
|
||||
if (preset.sync_info != "hold")
|
||||
continue;
|
||||
const std::string preset_id = preset.setting_id.empty()
|
||||
? OrcaCloudServiceAgent::generate_uuid_for_setting_id(preset.name, user_id)
|
||||
: preset.setting_id;
|
||||
if (preset_id == setting_id) {
|
||||
coll->set_sync_info_and_save(preset.name, setting_id, "update", 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +467,7 @@ public:
|
||||
void recreate_GUI(const wxString& message);
|
||||
void system_info();
|
||||
void keyboard_shortcuts();
|
||||
void troubleshoot();
|
||||
void load_project(wxWindow *parent, wxString& input_file) const;
|
||||
void import_model(wxWindow *parent, wxArrayString& input_files) const;
|
||||
void import_zip(wxWindow* parent, wxString& input_file) const;
|
||||
|
||||
@@ -578,55 +578,47 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty
|
||||
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
|
||||
auto sub_menu = new wxMenu;
|
||||
|
||||
for (auto &item : {L("Orca Cube"), L("OrcaSliced Combo"), L("Orca Tolerance Test"), L("3DBenchy"), L("Cali Cat"), L("Autodesk FDM Test"),
|
||||
L("Voron Cube"), L("Stanford Bunny"), L("Orca String Hell") }) {
|
||||
append_menu_item(
|
||||
sub_menu, wxID_ANY, _(item), "",
|
||||
[type, item](wxCommandEvent&) {
|
||||
std::vector<boost::filesystem::path> input_files;
|
||||
bool is_stringhell = false;
|
||||
std::vector<std::string> file_names;
|
||||
bool arrange_after_import = false;
|
||||
if (item == L("Orca Cube")){
|
||||
file_names = { "OrcaCube_v2.drc", "OrcaPlug_v2.drc"};
|
||||
arrange_after_import = true;
|
||||
}
|
||||
else if (item == L("OrcaSliced Combo"))
|
||||
{
|
||||
file_names = { "OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc" };
|
||||
arrange_after_import = true;
|
||||
}
|
||||
else if (item == L("Orca Tolerance Test"))
|
||||
file_names = { "OrcaToleranceTest.drc" };
|
||||
else if (item == L("3DBenchy"))
|
||||
file_names = { "3DBenchy.drc" };
|
||||
else if (item == L("Cali Cat"))
|
||||
file_names = { "calicat.drc" };
|
||||
else if (item == L("Autodesk FDM Test"))
|
||||
file_names = { "ksr_fdmtest_v4.drc" };
|
||||
else if (item == L("Voron Cube"))
|
||||
file_names = { "Voron_Design_Cube_v7.drc" };
|
||||
else if (item == L("Stanford Bunny"))
|
||||
file_names = { "Stanford_Bunny.drc" };
|
||||
else if (item == L("Orca String Hell")) {
|
||||
file_names = { "Orca_stringhell.drc" };
|
||||
is_stringhell = true;
|
||||
} else
|
||||
return;
|
||||
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table
|
||||
// keeps the menu label, the files to load and the per-model behavior in a single place and
|
||||
// avoids repeating the label strings (and the value-vs-pointer comparison pitfalls that come
|
||||
// with that). Labels are wrapped in L() so they are picked up for translation.
|
||||
struct HandyModel
|
||||
{
|
||||
const char* label;
|
||||
std::vector<std::string> file_names;
|
||||
bool arrange_after_import = false;
|
||||
bool is_stringhell = false;
|
||||
};
|
||||
static const std::vector<HandyModel> handy_models = {
|
||||
{L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
||||
{L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
|
||||
{L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
|
||||
{L("3DBenchy"), {"3DBenchy.drc"}},
|
||||
{L("Cali Cat"), {"calicat.drc"}},
|
||||
{L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
|
||||
{L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
|
||||
{L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
|
||||
{L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
|
||||
};
|
||||
|
||||
input_files.reserve(file_names.size());
|
||||
for (const auto& file_name : file_names)
|
||||
for (const auto& model : handy_models) {
|
||||
append_menu_item(
|
||||
sub_menu, wxID_ANY, _(model.label), "",
|
||||
[&model](wxCommandEvent&) {
|
||||
std::vector<boost::filesystem::path> input_files;
|
||||
input_files.reserve(model.file_names.size());
|
||||
for (const auto& file_name : model.file_names)
|
||||
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
|
||||
|
||||
plater()->load_files(input_files, LoadStrategy::LoadModel);
|
||||
if (arrange_after_import) {
|
||||
if (model.arrange_after_import) {
|
||||
plater()->set_prepare_state(Job::PREPARE_STATE_MENU);
|
||||
plater()->arrange();
|
||||
}
|
||||
|
||||
// Suggest to change settings for stringhell
|
||||
// This serves as mini tutorial for new users
|
||||
if (is_stringhell) {
|
||||
if (model.is_stringhell) {
|
||||
wxGetApp().CallAfter([=] {
|
||||
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
|
||||
|
||||
@@ -514,6 +514,16 @@ void ObjectList::create_objects_ctrl()
|
||||
for (int cn = colName; cn < colCount; cn++)
|
||||
GetColumn(cn)->SetWidth(m_columns_width[cn] * em);
|
||||
#endif
|
||||
|
||||
// Force an explicit row height on all platforms so the object-list spacing is
|
||||
// consistent and the filament colour badge (2*em tall, see
|
||||
// get_extruder_color_icons()) always fits. This is required on macOS, where wx
|
||||
// 3.3's native wxDataViewCtrl uses a fixed font-line-height row and does NOT
|
||||
// grow it to fit custom renderers' GetSize() (badges would otherwise overflow
|
||||
// and merge into adjacent rows); on Windows/Linux the generic control normally
|
||||
// derives the height from the renderers, but we set it here too so all
|
||||
// platforms match.
|
||||
SetRowHeight(2 * em + FromDIP(2));
|
||||
}
|
||||
|
||||
void ObjectList::get_selected_item_indexes(int& obj_idx, int& vol_idx, const wxDataViewItem& input_item/* = wxDataViewItem(nullptr)*/)
|
||||
@@ -754,6 +764,10 @@ void ObjectList::update_filament_values_for_items(const size_t filaments_count)
|
||||
if (!object->volumes[id]->config.has("extruder") ||
|
||||
size_t(object->volumes[id]->config.extruder()) > filaments_count) {
|
||||
extruder = wxString::Format("%d", object->config.extruder());
|
||||
// Clear the stale per-volume assignment so it falls back to the object's
|
||||
// extruder; otherwise the out-of-range index survives. Ported from
|
||||
// BambuStudio (STUDIO-15763).
|
||||
object->volumes[id]->config.erase("extruder");
|
||||
}
|
||||
else {
|
||||
extruder = wxString::Format("%d", object->volumes[id]->config.extruder());
|
||||
@@ -6337,6 +6351,11 @@ void ObjectList::msw_rescale()
|
||||
for (int cn = colName; cn < colCount; cn++)
|
||||
GetColumn(cn)->SetWidth(m_columns_width[cn] * em);
|
||||
|
||||
// Keep the explicit row height (see create_objects_ctrl) in sync with the
|
||||
// rescaled em so the filament colour badge keeps fitting after a DPI or theme
|
||||
// change.
|
||||
SetRowHeight(2 * em + FromDIP(2));
|
||||
|
||||
// rescale/update existing items with bitmaps
|
||||
m_objects_model->Rescale();
|
||||
|
||||
|
||||
@@ -2546,10 +2546,24 @@ static wxMenu* generate_help_menu()
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Setup Wizard"), _L("Setup Wizard"), [](wxCommandEvent &) {wxGetApp().ShowUserGuide();});
|
||||
|
||||
helpMenu->AppendSeparator();
|
||||
|
||||
// Open Config Folder
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Show Configuration Folder"), _L("Show Configuration Folder"),
|
||||
[](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); });
|
||||
|
||||
helpMenu->AppendSeparator();
|
||||
|
||||
// Troubleshoot center
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Troubleshoot Center"), "",
|
||||
[](wxCommandEvent&) { wxGetApp().troubleshoot(); });
|
||||
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Open Network Test"), _L("Open Network Test"), [](wxCommandEvent&) {
|
||||
NetworkTestDialog dlg(wxGetApp().mainframe);
|
||||
dlg.ShowModal();
|
||||
});
|
||||
|
||||
helpMenu->AppendSeparator();
|
||||
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Show Tip of the Day"), _L("Show Tip of the Day"), [](wxCommandEvent&) {
|
||||
wxGetApp().plater()->get_dailytips()->open();
|
||||
wxGetApp().plater()->get_current_canvas3D()->set_as_dirty();
|
||||
@@ -2568,11 +2582,6 @@ static wxMenu* generate_help_menu()
|
||||
return true;
|
||||
});
|
||||
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Open Network Test"), _L("Open Network Test"), [](wxCommandEvent&) {
|
||||
NetworkTestDialog dlg(wxGetApp().mainframe);
|
||||
dlg.ShowModal();
|
||||
});
|
||||
|
||||
// About
|
||||
#ifndef __APPLE__
|
||||
wxString about_title = wxString::Format(_L("&About %s"), SLIC3R_APP_FULL_NAME);
|
||||
|
||||
@@ -2416,7 +2416,7 @@ void NotificationManager::OrcaSyncConflictNotification::render_text(ImGuiWrapper
|
||||
}
|
||||
|
||||
const float action_y = starting_y + m_endlines.size() * shift_y;
|
||||
const std::string pull_text = _u8L("Pull");
|
||||
const std::string pull_text = conflict_code == -3 ? _u8L("Delete") : _u8L("Pull");
|
||||
render_hyperlink_action(imgui, x_offset, action_y, pull_text, "##orca_sync_pull",
|
||||
[this] { if (m_pull_callback && m_pull_callback(m_evt_handler)) close(); });
|
||||
if (m_force_push_callback) {
|
||||
@@ -2437,13 +2437,14 @@ void NotificationManager::push_shared_profiles_notification(const std::string& e
|
||||
}
|
||||
|
||||
void NotificationManager::push_orca_sync_conflict_notification(const std::string& text,
|
||||
int conflict_code,
|
||||
std::function<bool(wxEvtHandler*)> pull_callback,
|
||||
std::function<bool(wxEvtHandler*)> force_push_callback)
|
||||
{
|
||||
close_notification_of_type(NotificationType::OrcaSyncConflict);
|
||||
NotificationData data{ NotificationType::OrcaSyncConflict, NotificationLevel::WarningNotificationLevel, 0, text };
|
||||
push_notification_data(std::make_unique<NotificationManager::OrcaSyncConflictNotification>(
|
||||
data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback)), 0);
|
||||
data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback), conflict_code), 0);
|
||||
}
|
||||
|
||||
void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback)
|
||||
|
||||
@@ -279,6 +279,7 @@ public:
|
||||
// Shared profiles available for selected printer
|
||||
void push_shared_profiles_notification(const std::string& explore_url);
|
||||
void push_orca_sync_conflict_notification(const std::string& text,
|
||||
int conflict_code,
|
||||
std::function<bool(wxEvtHandler*)> pull_callback,
|
||||
std::function<bool(wxEvtHandler*)> force_push_callback);
|
||||
|
||||
@@ -905,10 +906,12 @@ private:
|
||||
public:
|
||||
OrcaSyncConflictNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler,
|
||||
std::function<bool(wxEvtHandler*)> pull_callback,
|
||||
std::function<bool(wxEvtHandler*)> force_push_callback)
|
||||
std::function<bool(wxEvtHandler*)> force_push_callback,
|
||||
int conflict_code)
|
||||
: PopNotification(n, id_provider, evt_handler)
|
||||
, m_pull_callback(std::move(pull_callback))
|
||||
, m_force_push_callback(std::move(force_push_callback))
|
||||
, conflict_code(conflict_code)
|
||||
{
|
||||
m_multiline = true;
|
||||
}
|
||||
@@ -920,6 +923,7 @@ private:
|
||||
|
||||
std::function<bool(wxEvtHandler*)> m_pull_callback;
|
||||
std::function<bool(wxEvtHandler*)> m_force_push_callback;
|
||||
int conflict_code;
|
||||
};
|
||||
class SlicingProgressNotification;
|
||||
|
||||
|
||||
@@ -1931,11 +1931,20 @@ bool PartPlate::check_filament_printable(const DynamicPrintConfig &config, wxStr
|
||||
|
||||
std::vector<int> used_filaments = get_extruders(true); // 1 base
|
||||
if (!used_filaments.empty()) {
|
||||
const std::vector<std::string>& filament_types = config.option<ConfigOptionStrings>("filament_type")->values;
|
||||
const std::vector<int>& filament_printables = config.option<ConfigOptionInts>("filament_printable")->values;
|
||||
const std::vector<int>& filament_map = get_real_filament_maps(config);
|
||||
// This runs synchronously mid printer-switch (load_current_preset -> reload_scene), before the
|
||||
// filament-count reconciliation clears stale per-object assignments, so the plate objects can
|
||||
// still reference filament indices beyond the freshly selected printer config. Skip those to
|
||||
// avoid an out-of-range access. Matches BambuStudio's guards in the same function.
|
||||
const int filament_count = std::min({(int) filament_types.size(), (int) filament_printables.size(), (int) filament_map.size()});
|
||||
for (auto filament_idx : used_filaments) {
|
||||
int filament_id = filament_idx - 1;
|
||||
std::string filament_type = config.option<ConfigOptionStrings>("filament_type")->values.at(filament_id);
|
||||
int filament_printable_status = config.option<ConfigOptionInts>("filament_printable")->values.at(filament_id);
|
||||
std::vector<int> filament_map = get_real_filament_maps(config);
|
||||
if (filament_id < 0 || filament_id >= filament_count)
|
||||
continue;
|
||||
std::string filament_type = filament_types[filament_id];
|
||||
int filament_printable_status = filament_printables[filament_id];
|
||||
int extruder_idx = filament_map[filament_id] - 1;
|
||||
if (!(filament_printable_status >> extruder_idx & 1)) {
|
||||
wxString extruder_name = extruder_idx == 0 ? _L("left") : _L("right");
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "RemovableDriveManager.hpp"
|
||||
#include "BitmapCache.hpp"
|
||||
#include "BonjourDialog.hpp"
|
||||
#include "CrealityDiscoveryDialog.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "OAuthDialog.hpp"
|
||||
#include "SimplyPrint.hpp"
|
||||
@@ -203,11 +204,28 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
|
||||
return sizer;
|
||||
};
|
||||
|
||||
auto printhost_browse = [=](wxWindow* parent)
|
||||
auto printhost_browse = [=](wxWindow* parent)
|
||||
{
|
||||
auto sizer = create_sizer_with_btn(parent, &m_printhost_browse_btn, "printer_host_browser", _L("Browse") + " " + dots);
|
||||
m_printhost_browse_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) {
|
||||
const auto host_type = m_config->opt_enum<PrintHostType>("host_type");
|
||||
|
||||
// Creality K-series printers announce themselves via DNS-SD under a
|
||||
// per-device-unique service type _Creality-<MAC-hex>._udp, so the
|
||||
// standard fixed-service-name Bonjour browser does not find them.
|
||||
// Dispatch to the Creality-specific scanner instead.
|
||||
if (host_type == htCrealityPrint) {
|
||||
CrealityDiscoveryDialog dialog(this);
|
||||
if (dialog.ShowModal() == wxID_OK && !dialog.selected_ip().empty()) {
|
||||
// set_value expects the value wrapped as wxString -- TextCtrl::set_value
|
||||
// any_casts to wxString, so a raw std::string throws bad_any_cast.
|
||||
wxString new_url = wxString::FromUTF8("http://" + dialog.selected_ip());
|
||||
m_optgroup->set_value("print_host", new_url, true);
|
||||
m_optgroup->get_field("print_host")->field_changed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (host_type == htFlashforge) {
|
||||
wxBusyCursor wait;
|
||||
std::vector<FlashforgeDiscoveredPrinter> printers;
|
||||
@@ -232,12 +250,13 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
|
||||
update_printhost_buttons();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BonjourDialog dialog(this, Preset::printer_technology(*m_config));
|
||||
if (dialog.show_and_lookup()) {
|
||||
m_optgroup->set_value("print_host", dialog.get_selected(), true);
|
||||
m_optgroup->get_field("print_host")->field_changed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
BonjourDialog dialog(this, Preset::printer_technology(*m_config));
|
||||
if (dialog.show_and_lookup()) {
|
||||
m_optgroup->set_value("print_host", dialog.get_selected(), true);
|
||||
m_optgroup->get_field("print_host")->field_changed();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+51
-45
@@ -65,6 +65,7 @@
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/Utils/CrealityPrint.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/ObjColorUtils.hpp"
|
||||
// For stl export
|
||||
@@ -1746,19 +1747,23 @@ Sidebar::Sidebar(Plater *parent)
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
// hover state data for printer panel
|
||||
auto printer_preset_hovered = std::make_shared<std::unordered_set<wxWindow*>>();
|
||||
|
||||
p->btn_edit_printer = new ScalableButton(p->panel_printer_preset, wxID_ANY, "edit");
|
||||
p->btn_edit_printer->SetToolTip(_L("Click to edit preset"));
|
||||
p->btn_edit_printer->Hide(); // hide for first launch
|
||||
p->btn_edit_printer->Bind(wxEVT_BUTTON, [this, panel_color](wxCommandEvent){
|
||||
p->btn_edit_printer->Bind(wxEVT_BUTTON, [this, panel_color, printer_preset_hovered](wxCommandEvent){
|
||||
p->editing_filament = -1;
|
||||
if (p->combo_printer->switch_to_tab())
|
||||
p->editing_filament = 0;
|
||||
// ORCA: FIX crash on wxGTK, directly modifying UI (self->Hide() / parent->Layout()) inside a button event can crash because callbacks are not re-entrant, leaving widgets in an inconsistent state
|
||||
wxGetApp().CallAfter([this, panel_color]() {
|
||||
wxGetApp().CallAfter([this, panel_color, printer_preset_hovered]() {
|
||||
// ORCA clicking edit button not triggers wxEVT_KILL_FOCUS wxEVT_LEAVE_WINDOW make changes manually to prevent stucked colors when opening printer settings
|
||||
if (!p || !p->panel_printer_preset || !p->btn_edit_printer)
|
||||
return;
|
||||
p->panel_printer_preset->SetBorderColor(panel_color.bd_normal);
|
||||
printer_preset_hovered->clear();
|
||||
p->btn_edit_printer->Hide();
|
||||
p->panel_printer_preset->Layout();
|
||||
});
|
||||
@@ -1797,38 +1802,33 @@ Sidebar::Sidebar(Plater *parent)
|
||||
*/
|
||||
// ORCA use Show/Hide to gain text area instead using blank icon. also manages hover effect for border
|
||||
for (wxWindow *w : std::initializer_list<wxWindow *>{p->panel_printer_preset, p->btn_edit_printer, p->image_printer, p->combo_printer}) {
|
||||
w->Bind(wxEVT_ENTER_WINDOW, [this, panel_color](wxMouseEvent &e) {
|
||||
w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent &e) {
|
||||
printer_preset_hovered->insert(w);
|
||||
if(!p->combo_printer->HasFocus())
|
||||
p->panel_printer_preset->SetBorderColor(panel_color.bd_hover);
|
||||
if(!p->btn_edit_printer->IsShown()){
|
||||
p->btn_edit_printer->Show();
|
||||
p->panel_printer_preset->Layout();
|
||||
}
|
||||
e.Skip();
|
||||
});
|
||||
w->Bind(wxEVT_LEAVE_WINDOW, [this, panel_color](wxMouseEvent &e) {
|
||||
// Orca: if the edit button is already hidden the handler has no
|
||||
// state to change, so skip the expensive wxFindWindowAtPoint tree
|
||||
// walk. Without this guard, when the parent window is on an
|
||||
// inactive Hyprland/Wayland workspace, GTK keeps delivering
|
||||
// synthetic leave events and the Hide()+Layout() below re-enters
|
||||
// the same handler in a feedback loop that pegs a CPU core.
|
||||
// (IsShownOnScreen() can't be used here — see Plater.cpp:9304.)
|
||||
if (!p->btn_edit_printer->IsShown()) { e.Skip(); return; }
|
||||
// Use event-relative coords instead of wxGetMousePosition() which
|
||||
// returns (0,0) on Wayland for global screen coordinates.
|
||||
wxWindow* evtObj = dynamic_cast<wxWindow*>(e.GetEventObject());
|
||||
wxPoint screenPos = evtObj ? evtObj->ClientToScreen(e.GetPosition()) : wxGetMousePosition();
|
||||
wxWindow* next_w = wxFindWindowAtPoint(screenPos);
|
||||
if (!next_w || !p->panel_printer_preset->IsDescendant(next_w)){
|
||||
if(!p->combo_printer->HasFocus())
|
||||
p->panel_printer_preset->SetBorderColor(panel_color.bd_normal);
|
||||
p->btn_edit_printer->Hide();
|
||||
p->panel_printer_preset->Layout();
|
||||
}
|
||||
w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent &e) {
|
||||
printer_preset_hovered->erase(w);
|
||||
if (printer_preset_hovered->empty() && !p->combo_printer->HasFocus())
|
||||
p->panel_printer_preset->SetBorderColor(panel_color.bd_normal);
|
||||
e.Skip();
|
||||
});
|
||||
}
|
||||
// Perform show/hide in wxEVT_IDLE after enter/leave events have settled.
|
||||
// This prevents the extraneous enter/leave events generated by the
|
||||
// layout change itself from causing a feedback loop.
|
||||
Bind(wxEVT_IDLE, [this, printer_preset_hovered](wxIdleEvent &e) {
|
||||
auto hovered = !printer_preset_hovered->empty();
|
||||
if (p->btn_edit_printer->IsShown() != hovered) {
|
||||
if (hovered) {
|
||||
p->btn_edit_printer->Show();
|
||||
} else {
|
||||
p->btn_edit_printer->Hide();
|
||||
}
|
||||
p->panel_printer_preset->Layout();
|
||||
}
|
||||
});
|
||||
|
||||
// ORCA unified Nozzle diameter selection
|
||||
p->panel_nozzle_dia = new StaticBox(p->m_panel_printer_content);
|
||||
@@ -1874,19 +1874,17 @@ Sidebar::Sidebar(Plater *parent)
|
||||
p->label_nozzle_type->SetMaxSize(FromDIP(wxSize(56, -1)));
|
||||
|
||||
// highlight border on hover
|
||||
auto nozzle_dia_hovered = std::make_shared<std::unordered_set<wxWindow*>>();
|
||||
for (wxWindow *w : std::initializer_list<wxWindow *>{p->panel_nozzle_dia, p->label_nozzle_title, p->label_nozzle_type, p->combo_nozzle_dia}) {
|
||||
w->Bind(wxEVT_ENTER_WINDOW, [this, panel_color](wxMouseEvent &e) {
|
||||
w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent &e) {
|
||||
nozzle_dia_hovered->insert(w);
|
||||
if(!p->combo_nozzle_dia->HasFocus())
|
||||
p->panel_nozzle_dia->SetBorderColor(panel_color.bd_hover);
|
||||
e.Skip();
|
||||
});
|
||||
w->Bind(wxEVT_LEAVE_WINDOW, [this, panel_color](wxMouseEvent &e) {
|
||||
// Use event-relative coords instead of wxGetMousePosition() which
|
||||
// returns (0,0) on Wayland for global screen coordinates.
|
||||
wxWindow* evtObj = dynamic_cast<wxWindow*>(e.GetEventObject());
|
||||
wxPoint screenPos = evtObj ? evtObj->ClientToScreen(e.GetPosition()) : wxGetMousePosition();
|
||||
wxWindow* next_w = wxFindWindowAtPoint(screenPos);
|
||||
if (!p->combo_nozzle_dia->HasFocus() && (!next_w || !p->panel_nozzle_dia->IsDescendant(next_w)))
|
||||
w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent &e) {
|
||||
nozzle_dia_hovered->erase(w);
|
||||
if (nozzle_dia_hovered->empty() && !p->combo_nozzle_dia->HasFocus())
|
||||
p->panel_nozzle_dia->SetBorderColor(panel_color.bd_normal);
|
||||
e.Skip();
|
||||
});
|
||||
@@ -1946,21 +1944,19 @@ Sidebar::Sidebar(Plater *parent)
|
||||
p->combo_printer_bed->Bind(wxEVT_KILL_FOCUS, [this, bed_focus_bg](auto& e) {bed_focus_bg(false); e.Skip();});
|
||||
|
||||
// highlight border on hover
|
||||
auto printer_bed_hovered = std::make_shared<std::unordered_set<wxWindow*>>();
|
||||
for (wxWindow *w : std::initializer_list<wxWindow *>{p->panel_printer_bed, p->image_printer_bed, p->combo_printer_bed}) {
|
||||
w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color](wxMouseEvent &e) {
|
||||
w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent &e) {
|
||||
printer_bed_hovered->insert(w);
|
||||
if(!p->combo_printer_bed->HasFocus())
|
||||
p->panel_printer_bed->SetBorderColor(panel_color.bd_hover);
|
||||
if(w == p->image_printer_bed && !p->combo_printer_bed->is_drop_down()) // dont trigger while combo open
|
||||
on_enter_image_printer_bed(e);
|
||||
e.Skip();
|
||||
});
|
||||
w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color](wxMouseEvent &e) {
|
||||
// Use event-relative coords instead of wxGetMousePosition() which
|
||||
// returns (0,0) on Wayland for global screen coordinates.
|
||||
wxWindow* evtObj = dynamic_cast<wxWindow*>(e.GetEventObject());
|
||||
wxPoint screenPos = evtObj ? evtObj->ClientToScreen(e.GetPosition()) : wxGetMousePosition();
|
||||
wxWindow* next_w = wxFindWindowAtPoint(screenPos);
|
||||
if (!p->combo_printer_bed->HasFocus() && (!next_w || !p->panel_printer_bed->IsDescendant(next_w)))
|
||||
w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent &e) {
|
||||
printer_bed_hovered->erase(w);
|
||||
if (printer_bed_hovered->empty() && !p->combo_printer_bed->HasFocus())
|
||||
p->panel_printer_bed->SetBorderColor(panel_color.bd_normal);
|
||||
if(w == p->image_printer_bed)
|
||||
on_leave_image_printer_bed(e);
|
||||
@@ -3681,6 +3677,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish pop_finsish_sync_ams_dialog";
|
||||
}
|
||||
|
||||
|
||||
bool Sidebar::should_show_SEMM_buttons()
|
||||
{
|
||||
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
|
||||
@@ -9549,6 +9546,12 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
|
||||
wxGetApp().get_tab(preset_type)->select_preset(preset_name);
|
||||
}
|
||||
|
||||
// ORCA: Always refresh the selected filament combo so its color swatch (clr_picker)
|
||||
// matches the chosen preset. update_ams_color() (in OnSelect) updates the project
|
||||
// filament color when the preset defines one; this repaints the swatch to match.
|
||||
if (preset_type == Preset::TYPE_FILAMENT)
|
||||
combo->update();
|
||||
|
||||
// update plater with new config
|
||||
q->on_config_change(wxGetApp().preset_bundle->full_config());
|
||||
if (preset_type == Preset::TYPE_PRINTER) {
|
||||
@@ -13333,8 +13336,6 @@ void Plater::calib_VFA(const Calib_Params& params)
|
||||
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
|
||||
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
||||
printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true});
|
||||
printer_config->set_key_value("input_shaping_type", new ConfigOptionEnum<InputShaperType>(InputShaperType::Disable));
|
||||
filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 });
|
||||
print_config->set_key_value("enable_overhang_speed", new ConfigOptionBool { false });
|
||||
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
@@ -16165,6 +16166,11 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
|
||||
pDlg = std::make_unique<ElegooPrintHostSendDialog>(default_output_file, upload_job.printhost->get_post_upload_actions(), groups,
|
||||
storage_paths, storage_names,
|
||||
config->get_bool("open_device_tab_post_upload"));
|
||||
} else if (host_type == htCrealityPrint) {
|
||||
pDlg = std::make_unique<CrealityPrintHostSendDialog>(default_output_file, upload_job.printhost->get_post_upload_actions(), groups,
|
||||
storage_paths, storage_names,
|
||||
config->get_bool("open_device_tab_post_upload"),
|
||||
upload_job.printhost.get());
|
||||
} else if (flashforge_local_api) {
|
||||
auto* flashforge_host = dynamic_cast<Flashforge*>(upload_job.printhost.get());
|
||||
if (flashforge_host == nullptr) {
|
||||
|
||||
@@ -234,7 +234,19 @@ int PresetComboBox::update_ams_color()
|
||||
std::string ctype;
|
||||
std::vector<std::string> colors;
|
||||
if (idx < 0) {
|
||||
auto name = Preset::remove_suffix_modified(GetValue().ToUTF8().data());
|
||||
// ORCA: The combo displays the preset alias while
|
||||
// the stored preset name usually carries a printer suffix. Resolving with the raw display
|
||||
// value via find_preset() fails for such presets, so this returned early and the
|
||||
// filament color swatch (clr_picker) kept showing the previous color. Prefer the
|
||||
// internal preset name stored per item, then fall back to alias resolution.
|
||||
std::string name;
|
||||
if (m_last_selected >= 0) {
|
||||
wxString stored = GetItemAlias(m_last_selected);
|
||||
if (!stored.empty())
|
||||
name = Preset::remove_suffix_modified(stored.ToUTF8().data());
|
||||
}
|
||||
if (name.empty())
|
||||
name = m_collection->get_preset_name_by_alias(Preset::remove_suffix_modified(GetValue().ToUTF8().data()));
|
||||
auto *preset = m_collection->find_preset(name);
|
||||
if (preset)
|
||||
color = preset->config.opt_string("default_filament_colour", 0u);
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
#include "NotificationManager.hpp"
|
||||
#include "ExtraRenderers.hpp"
|
||||
#include "format.hpp"
|
||||
#include "../Utils/CrealityPrint.hpp"
|
||||
#include "BitmapComboBox.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
using json = nlohmann::json;
|
||||
@@ -1854,4 +1859,251 @@ void ElegooPrintHostSendDialog::refresh()
|
||||
this->Fit();
|
||||
}
|
||||
|
||||
CrealityPrintHostSendDialog::CrealityPrintHostSendDialog(const fs::path& path,
|
||||
PrintHostPostUploadActions post_actions,
|
||||
const wxArrayString& groups,
|
||||
const wxArrayString& storage_paths,
|
||||
const wxArrayString& storage_names,
|
||||
bool switch_to_device_tab,
|
||||
PrintHost* printhost)
|
||||
: PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab)
|
||||
, m_enableSelfTest(false)
|
||||
, m_printhost(printhost)
|
||||
{}
|
||||
|
||||
void CrealityPrintHostSendDialog::init()
|
||||
{
|
||||
PrintHostSendDialog::init();
|
||||
|
||||
auto* creality_host = static_cast<CrealityPrint*>(m_printhost);
|
||||
bool multi_color;
|
||||
std::string printer_name;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
multi_color = creality_host->supports_multi_color_print();
|
||||
if (multi_color)
|
||||
printer_name = creality_host->model_name();
|
||||
}
|
||||
if (!multi_color)
|
||||
return;
|
||||
|
||||
auto* group_box = new wxStaticBox(this, wxID_ANY,
|
||||
wxString::Format(_L("Printer: %s"), printer_name));
|
||||
auto* group_sizer = new wxStaticBoxSizer(group_box, wxVERTICAL);
|
||||
content_sizer->Add(group_sizer, 0, wxEXPAND);
|
||||
|
||||
const AppConfig* app_config = wxGetApp().app_config;
|
||||
std::string saved = app_config->get("recent", CONFIG_KEY_ENABLESELFTEST);
|
||||
if (!saved.empty()) {
|
||||
try { m_enableSelfTest = std::stoi(saved) != 0; } catch (...) {}
|
||||
}
|
||||
|
||||
// Calibration checkbox
|
||||
{
|
||||
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto checkbox = new ::CheckBox(this);
|
||||
checkbox->SetValue(m_enableSelfTest);
|
||||
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
|
||||
m_enableSelfTest = e.IsChecked();
|
||||
AppConfig* ac = wxGetApp().app_config;
|
||||
ac->set("recent", CONFIG_KEY_ENABLESELFTEST, m_enableSelfTest ? "1" : "0");
|
||||
e.Skip();
|
||||
});
|
||||
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
|
||||
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Calibrate before printing"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
|
||||
checkbox_text->SetFont(::Label::Body_13);
|
||||
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
|
||||
group_sizer->Add(checkbox_sizer);
|
||||
group_sizer->AddSpacer(VERT_SPACING);
|
||||
}
|
||||
|
||||
// --- Color mapping UI ---
|
||||
// Get gcode filament info from slicer
|
||||
auto preset_bundle = wxGetApp().preset_bundle;
|
||||
auto full_config = preset_bundle->full_config();
|
||||
auto* filament_colors = full_config.option<ConfigOptionStrings>("filament_colour");
|
||||
auto* filament_types = full_config.option<ConfigOptionStrings>("filament_type");
|
||||
int gcode_filament_count = filament_colors ? (int)filament_colors->values.size() : 0;
|
||||
|
||||
// Query printer for loaded materials
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
std::string boxes_json = creality_host->query_boxes_info();
|
||||
if (!boxes_json.empty()) {
|
||||
try {
|
||||
auto resp = nlohmann::json::parse(boxes_json);
|
||||
if (resp.contains("boxsInfo") && resp["boxsInfo"].contains("materialBoxs")) {
|
||||
for (auto& box : resp["boxsInfo"]["materialBoxs"]) {
|
||||
int box_id = box["id"].get<int>();
|
||||
int box_type = box.value("type", 0);
|
||||
// Skip inactive CFS boxes (type 0 with state != 1)
|
||||
// Spool holder (type 1) is always available
|
||||
if (box_type == 0 && box.value("state", 0) != 1)
|
||||
continue;
|
||||
for (auto& mat : box["materials"]) {
|
||||
int slot_id = mat["id"].get<int>();
|
||||
std::string tool_id = "T" + std::to_string(box_id) + std::string(1, 'A' + slot_id);
|
||||
// Creality uses "#0RRGGBB" (7 hex digits), normalize to "#RRGGBB"
|
||||
std::string color = mat.value("color", "#FFFFFF");
|
||||
if (color.size() == 8 && color[0] == '#')
|
||||
color = "#" + color.substr(2);
|
||||
m_printer_slots.push_back({
|
||||
tool_id,
|
||||
mat.value("type", ""),
|
||||
color,
|
||||
box_id,
|
||||
slot_id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const nlohmann::json::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "CrealityPrint dialog: Failed to parse boxsInfo: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gcode_filament_count > 0 && !m_printer_slots.empty()) {
|
||||
auto* label = new wxStaticText(this, wxID_ANY, _L("Filament Mapping:"));
|
||||
label->SetFont(::Label::Body_13);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
|
||||
group_sizer->Add(label);
|
||||
group_sizer->AddSpacer(4);
|
||||
|
||||
for (int i = 0; i < gcode_filament_count; i++) {
|
||||
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
// Left side: gcode filament color swatch + type
|
||||
std::string gc_color = (filament_colors && i < (int)filament_colors->values.size())
|
||||
? filament_colors->values[i] : "#FFFFFF";
|
||||
std::string gc_type = (filament_types && i < (int)filament_types->values.size())
|
||||
? filament_types->values[i] : "?";
|
||||
|
||||
// Color indicator panel
|
||||
auto* color_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
|
||||
color_panel->SetBackgroundColour(wxColour(gc_color));
|
||||
color_panel->SetMinSize(wxSize(FromDIP(16), FromDIP(16)));
|
||||
row_sizer->Add(color_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
|
||||
|
||||
auto* type_label = new wxStaticText(this, wxID_ANY,
|
||||
wxString::Format("%d (%s)", i + 1, gc_type.c_str()));
|
||||
type_label->SetFont(::Label::Body_13);
|
||||
type_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
|
||||
type_label->SetMinSize(wxSize(FromDIP(80), -1));
|
||||
row_sizer->Add(type_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
|
||||
|
||||
// Arrow
|
||||
auto* arrow_label = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92"));
|
||||
arrow_label->SetFont(::Label::Body_13);
|
||||
row_sizer->Add(arrow_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
|
||||
|
||||
// Right side: dropdown with color icons per slot
|
||||
int icon_sz = FromDIP(16);
|
||||
auto* combo = new BitmapComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
|
||||
for (auto& slot : m_printer_slots) {
|
||||
wxBitmap* bmp = get_extruder_color_icon(slot.color, "", icon_sz, icon_sz);
|
||||
wxString label_str;
|
||||
if (slot.box_id == 0)
|
||||
label_str = wxString::Format("Ext - %s", slot.type.c_str());
|
||||
else
|
||||
label_str = wxString::Format("%s - %s", slot.tool_id.substr(1).c_str(), slot.type.c_str());
|
||||
combo->Append(label_str, bmp ? *bmp : wxNullBitmap);
|
||||
}
|
||||
// Find best default: CFS exact color+type, CFS type-only,
|
||||
// Ext exact, Ext type-only, else positional
|
||||
int default_sel = (i < (int)m_printer_slots.size()) ? i : 0;
|
||||
bool matched = false;
|
||||
for (int pass = 0; pass < 4 && !matched; pass++) {
|
||||
for (int s = 0; s < (int)m_printer_slots.size(); s++) {
|
||||
bool is_ext = (m_printer_slots[s].box_id == 0);
|
||||
bool type_match = (m_printer_slots[s].type == gc_type);
|
||||
bool color_match = (wxColour(m_printer_slots[s].color) == wxColour(gc_color));
|
||||
bool hit = false;
|
||||
switch (pass) {
|
||||
case 0: hit = !is_ext && type_match && color_match; break;
|
||||
case 1: hit = !is_ext && type_match; break;
|
||||
case 2: hit = is_ext && type_match && color_match; break;
|
||||
case 3: hit = is_ext && type_match; break;
|
||||
}
|
||||
if (hit) {
|
||||
default_sel = s;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
combo->SetSelection(default_sel);
|
||||
row_sizer->Add(combo, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
group_sizer->Add(row_sizer);
|
||||
group_sizer->AddSpacer(4);
|
||||
m_slot_combos.push_back(combo);
|
||||
}
|
||||
|
||||
int ext_slot_idx = -1;
|
||||
for (int s = 0; s < (int)m_printer_slots.size(); s++) {
|
||||
if (m_printer_slots[s].box_id == 0) {
|
||||
ext_slot_idx = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ext_slot_idx >= 0) {
|
||||
for (int ci = 0; ci < (int)m_slot_combos.size(); ci++) {
|
||||
int sel = m_slot_combos[ci]->GetSelection();
|
||||
if (sel >= 0 && sel < (int)m_printer_slots.size() &&
|
||||
m_printer_slots[sel].box_id == 0) {
|
||||
for (int cj = 0; cj < (int)m_slot_combos.size(); cj++) {
|
||||
if (cj != ci)
|
||||
m_slot_combos[cj]->Enable(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto* c : m_slot_combos) {
|
||||
c->Bind(wxEVT_COMBOBOX, [this, ext_slot_idx](wxCommandEvent& e) {
|
||||
int sel = e.GetSelection();
|
||||
if (sel >= 0 && sel < (int)m_printer_slots.size() &&
|
||||
m_printer_slots[sel].box_id == 0) {
|
||||
for (auto* c2 : m_slot_combos) {
|
||||
if (c2 != e.GetEventObject())
|
||||
c2->Enable(false);
|
||||
}
|
||||
} else {
|
||||
for (auto* c2 : m_slot_combos)
|
||||
c2->Enable(true);
|
||||
}
|
||||
e.Skip();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->Layout();
|
||||
this->Fit();
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> CrealityPrintHostSendDialog::extendedInfo() const
|
||||
{
|
||||
std::map<std::string, std::string> info;
|
||||
info["enableSelfTest"] = m_enableSelfTest ? "1" : "0";
|
||||
|
||||
// Color mapping: colorMatch_0, colorMatch_1, ... tab-delimited
|
||||
for (int i = 0; i < (int)m_slot_combos.size(); i++) {
|
||||
int sel = m_slot_combos[i]->GetSelection();
|
||||
if (sel >= 0 && sel < (int)m_printer_slots.size()) {
|
||||
auto& slot = m_printer_slots[sel];
|
||||
// id = gcode tool index (T1A for first filament, T1B for second, ...),
|
||||
// not the destination CFS slot — firmware matches by gcode tool.
|
||||
std::string gcode_tool = "T1" + std::string(1, 'A' + i);
|
||||
info["colorMatch_" + std::to_string(i)] =
|
||||
gcode_tool + "\t" + slot.type + "\t" + slot.color + "\t" +
|
||||
std::to_string(slot.box_id) + "\t" + std::to_string(slot.material_id);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
}}
|
||||
@@ -25,6 +25,8 @@ class wxStaticText;
|
||||
class wxWrapSizer;
|
||||
class CheckBox;
|
||||
|
||||
namespace Slic3r { namespace GUI { class BitmapComboBox; } }
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace GUI {
|
||||
@@ -186,6 +188,37 @@ private:
|
||||
BedType m_BedType;
|
||||
};
|
||||
|
||||
class CrealityPrintHostSendDialog : public PrintHostSendDialog
|
||||
{
|
||||
public:
|
||||
CrealityPrintHostSendDialog(const boost::filesystem::path& path,
|
||||
PrintHostPostUploadActions post_actions,
|
||||
const wxArrayString& groups,
|
||||
const wxArrayString& storage_paths,
|
||||
const wxArrayString& storage_names,
|
||||
bool switch_to_device_tab,
|
||||
PrintHost* printhost);
|
||||
|
||||
virtual void init() override;
|
||||
virtual std::map<std::string, std::string> extendedInfo() const;
|
||||
|
||||
private:
|
||||
static constexpr const char* CONFIG_KEY_ENABLESELFTEST = "crealityprint_enable_self_test";
|
||||
|
||||
bool m_enableSelfTest;
|
||||
PrintHost* m_printhost;
|
||||
|
||||
struct SlotInfo {
|
||||
std::string tool_id; // e.g. "T1A"
|
||||
std::string type; // e.g. "PLA"
|
||||
std::string color; // e.g. "#ffffff"
|
||||
int box_id;
|
||||
int material_id;
|
||||
};
|
||||
std::vector<SlotInfo> m_printer_slots;
|
||||
std::vector<BitmapComboBox*> m_slot_combos; // one per gcode filament
|
||||
};
|
||||
|
||||
class FlashforgePrintHostSendDialog : public PrintHostSendDialog
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -258,10 +258,9 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line
|
||||
wxStaticText *mesh_face_number_title = new wxStaticText(this, wxID_ANY, _L("Number of triangular facets") + ": ");
|
||||
mesh_face_number_title->SetFont(::Label::Body_14);
|
||||
mesh_face_number_title->SetForegroundColour(StateColor::darkModeColorFor(FONT_COLOR));
|
||||
mesh_face_number_text = new wxStaticText(this, wxID_ANY, "0");
|
||||
mesh_face_number_text = new wxStaticText(this, wxID_ANY, _L("Calculating, please wait..."));
|
||||
mesh_face_number_text->SetFont(::Label::Body_14);
|
||||
mesh_face_number_text->SetForegroundColour(StateColor::darkModeColorFor(FONT_COLOR));
|
||||
mesh_face_number_text->SetMinSize(wxSize(FromDIP(150), -1));
|
||||
mesh_face_number_sizer->Add(mesh_face_number_title, 0, wxALIGN_LEFT);
|
||||
mesh_face_number_sizer->Add(mesh_face_number_text, 0, wxALIGN_LEFT);
|
||||
bSizer->Add(mesh_face_number_sizer, 0, wxEXPAND | wxALL, LEFT_RIGHT_PADING);
|
||||
|
||||
+23
-15
@@ -2383,7 +2383,7 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("ironing_angle", "quality_settings_ironing#angle-offset");
|
||||
optgroup->append_single_option_line("ironing_angle_fixed", "quality_settings_ironing#fixed-angle");
|
||||
|
||||
optgroup = page->new_optgroup("Z Contouring", L"param_advanced");
|
||||
optgroup = page->new_optgroup(L("Z contouring"), L"param_advanced");
|
||||
optgroup->append_single_option_line("zaa_enabled", "quality_settings_z_contouring");
|
||||
optgroup->append_single_option_line("zaa_minimize_perimeter_height", "quality_settings_z_contouring#minimize-wall-height-angle");
|
||||
optgroup->append_single_option_line("zaa_min_z", "quality_settings_z_contouring#minimum-z-height");
|
||||
@@ -2567,8 +2567,10 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("accel_to_decel_enable", "speed_settings_acceleration");
|
||||
optgroup->append_single_option_line("accel_to_decel_factor", "speed_settings_acceleration");
|
||||
|
||||
optgroup = page->new_optgroup(L("Jerk(XY)"), L"param_jerk", 15);
|
||||
optgroup = page->new_optgroup(L("Junction Deviation"), L"param_junction_deviation", 15);
|
||||
optgroup->append_single_option_line("default_junction_deviation", "speed_settings_jerk_xy#junction-deviation");
|
||||
|
||||
optgroup = page->new_optgroup(L("Jerk(XY)"), L"param_jerk", 15);
|
||||
optgroup->append_single_option_line("default_jerk", "speed_settings_jerk_xy#default");
|
||||
optgroup->append_single_option_line("outer_wall_jerk", "speed_settings_jerk_xy#outer-wall");
|
||||
optgroup->append_single_option_line("inner_wall_jerk", "speed_settings_jerk_xy#inner-wall");
|
||||
@@ -5535,8 +5537,8 @@ void TabPrinter::toggle_options()
|
||||
for (auto el : {"use_firmware_retraction", "use_relative_e_distances", "support_multi_bed_types", "pellet_modded_printer", "bed_mesh_max", "bed_mesh_min", "bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "thumbnails"})
|
||||
toggle_line(el, !is_BBL_printer);
|
||||
|
||||
auto gcf = m_config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value;
|
||||
toggle_line("enable_power_loss_recovery", is_BBL_printer || gcf == gcfMarlinFirmware);
|
||||
bool gcf_is_marlin_firmware = m_config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value == GCodeFlavor::gcfMarlinFirmware;
|
||||
toggle_line("enable_power_loss_recovery", is_BBL_printer || gcf_is_marlin_firmware);
|
||||
|
||||
// Belt printer: show belt-specific settings only when belt_printer is enabled.
|
||||
bool is_belt = m_config->opt_bool("belt_printer");
|
||||
@@ -5706,18 +5708,25 @@ void TabPrinter::toggle_options()
|
||||
if (m_active_page->title() == L("Motion ability")) {
|
||||
auto gcf = m_config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value;
|
||||
update_input_shaper_menu(gcf);
|
||||
|
||||
// Orca: use booleans to avoid repeated comparisons with enum values
|
||||
const bool gcf_is_marlin_legacy = gcf == GCodeFlavor::gcfMarlinLegacy;
|
||||
const bool gcf_is_marlin_firmware = gcf == GCodeFlavor::gcfMarlinFirmware;
|
||||
const bool gcf_is_klipper = gcf == GCodeFlavor::gcfKlipper;
|
||||
const bool gcf_is_reprap_firmware = gcf == GCodeFlavor::gcfRepRapFirmware;
|
||||
|
||||
bool silent_mode = m_config->opt_bool("silent_mode");
|
||||
int max_field = silent_mode ? 2 : 1;
|
||||
for (int i = 0; i < max_field; ++i)
|
||||
toggle_option("machine_max_acceleration_travel", gcf != gcfMarlinLegacy && gcf != gcfKlipper, i);
|
||||
toggle_line("machine_max_acceleration_travel", gcf != gcfMarlinLegacy && gcf != gcfKlipper);
|
||||
toggle_option("machine_max_acceleration_travel", !gcf_is_marlin_legacy && !gcf_is_klipper, i);
|
||||
toggle_line("machine_max_acceleration_travel", !gcf_is_marlin_legacy && !gcf_is_klipper);
|
||||
for (int i = 0; i < max_field; ++i)
|
||||
toggle_option("machine_max_junction_deviation", gcf == gcfMarlinFirmware, i);
|
||||
toggle_line("machine_max_junction_deviation", gcf == gcfMarlinFirmware);
|
||||
toggle_option("machine_max_junction_deviation", gcf_is_marlin_firmware, i);
|
||||
toggle_line("machine_max_junction_deviation", gcf_is_marlin_firmware);
|
||||
|
||||
// Check if junction deviation value is non-zero and firmware is Marlin
|
||||
bool enable_jerk = gcf != gcfMarlinFirmware;
|
||||
if (gcf == gcfMarlinFirmware) {
|
||||
bool enable_jerk = !gcf_is_marlin_firmware;
|
||||
if (gcf_is_marlin_firmware) {
|
||||
const auto *junction_deviation = m_config->option<ConfigOptionFloats>("machine_max_junction_deviation");
|
||||
if (junction_deviation != nullptr) {
|
||||
const auto &values = junction_deviation->values;
|
||||
@@ -5733,14 +5742,14 @@ void TabPrinter::toggle_options()
|
||||
toggle_option("machine_max_jerk_e", enable_jerk, i);
|
||||
}
|
||||
|
||||
bool emittable_limits = m_config->opt_enum<GCodeFlavor>("gcode_flavor") == GCodeFlavor::gcfMarlinLegacy || m_config->opt_enum<GCodeFlavor>("gcode_flavor") == GCodeFlavor::gcfMarlinFirmware || m_config->opt_enum<GCodeFlavor>("gcode_flavor") == GCodeFlavor::gcfRepRapFirmware;
|
||||
bool emittable_limits = gcf_is_marlin_legacy || gcf_is_marlin_firmware || gcf_is_reprap_firmware;
|
||||
toggle_option("emit_machine_limits_to_gcode", emittable_limits);
|
||||
|
||||
bool resonance_avoidance = m_config->opt_bool("resonance_avoidance");
|
||||
toggle_option("min_resonance_avoidance_speed", resonance_avoidance);
|
||||
toggle_option("max_resonance_avoidance_speed", resonance_avoidance);
|
||||
|
||||
bool input_shaping_compatible = m_config->opt_enum<GCodeFlavor>("gcode_flavor") == GCodeFlavor::gcfMarlinFirmware || m_config->opt_enum<GCodeFlavor>("gcode_flavor") == GCodeFlavor::gcfRepRapFirmware;
|
||||
bool input_shaping_compatible = gcf_is_marlin_firmware || gcf_is_reprap_firmware;
|
||||
|
||||
for (auto is : {"input_shaping_emit", "input_shaping_type", "input_shaping_freq_x", "input_shaping_freq_y",
|
||||
"input_shaping_damp_x", "input_shaping_damp_y"})
|
||||
@@ -5750,12 +5759,11 @@ void TabPrinter::toggle_options()
|
||||
bool emit_machine_limits_to_gcode = m_config->opt_bool("emit_machine_limits_to_gcode");
|
||||
toggle_option("input_shaping_emit", emit_machine_limits_to_gcode);
|
||||
bool input_shaping_emit = emit_machine_limits_to_gcode && m_config->opt_bool("input_shaping_emit");
|
||||
bool reprap = m_config->opt_enum<GCodeFlavor>("gcode_flavor") == GCodeFlavor::gcfRepRapFirmware;
|
||||
toggle_option("input_shaping_type", input_shaping_emit);
|
||||
toggle_option("input_shaping_freq_x", input_shaping_emit);
|
||||
toggle_option("input_shaping_freq_y", input_shaping_emit && !reprap);
|
||||
toggle_option("input_shaping_freq_y", input_shaping_emit && !gcf_is_reprap_firmware);
|
||||
toggle_option("input_shaping_damp_x", input_shaping_emit);
|
||||
toggle_option("input_shaping_damp_y", input_shaping_emit && !reprap);
|
||||
toggle_option("input_shaping_damp_y", input_shaping_emit && !gcf_is_reprap_firmware);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,201 @@
|
||||
#ifndef slic3r_GUI_TroublesootDialog_hpp_
|
||||
#define slic3r_GUI_TroublesootDialog_hpp_
|
||||
|
||||
#include <wx/wx.h>
|
||||
#include <wx/zipstrm.h>
|
||||
#include <wx/dir.h>
|
||||
#include <wx/dcbuffer.h>
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <map>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class TroubleshootDialog : public DPIDialog
|
||||
{
|
||||
|
||||
#define DESIGN_COMBOBOX_SIZE wxSize(FromDIP(120), -1)
|
||||
|
||||
public:
|
||||
TroubleshootDialog();
|
||||
//~TroubleshootDialog();
|
||||
|
||||
private:
|
||||
ScalableBitmap m_logo;
|
||||
wxStaticBitmap* m_header_logo;
|
||||
Label* m_logs_storage;
|
||||
int m_printers__act;
|
||||
int m_printers__usr;
|
||||
int m_filaments_act;
|
||||
int m_filaments_usr;
|
||||
int m_processes_act;
|
||||
int m_processes_usr;
|
||||
bool m_sys_panel_mode = true;
|
||||
|
||||
protected:
|
||||
wxFlexGridSizer* create_item_loaded_profiles();
|
||||
ComboBox* create_item_log_level_combo();
|
||||
|
||||
wxString GetTimestamp();
|
||||
|
||||
wxString GetSysInfoAll();
|
||||
//wxString GetConfigStr();
|
||||
wxString GetProfilesOverview();
|
||||
wxString GetOStype();
|
||||
wxString GetOSinfo();
|
||||
#ifdef __WINDOWS__
|
||||
wxString GetWinVersion();
|
||||
wxString GetWinDisplayVersion();
|
||||
#elif defined(__LINUX__)
|
||||
wxString GetLinuxDistroName();
|
||||
wxString GetLinuxDisplayServer();
|
||||
#endif
|
||||
wxString GetPackageType();
|
||||
wxString GetCPUinfo();
|
||||
wxString GetGPUinfo();
|
||||
wxString GetRAMinfo();
|
||||
wxString GetMONinfo();
|
||||
|
||||
void PackAll();
|
||||
void RebuildSystemProfiles();
|
||||
bool RestartApplication();
|
||||
void ClearLogs();
|
||||
void UpdateLogsStorage();
|
||||
|
||||
void BrowseFolder(std::string path);
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
static wxString get_cpu_info_from_registry();
|
||||
#else
|
||||
static std::map<std::string, std::string> parse_lscpu_etc(const std::string& name, char delimiter);
|
||||
#endif
|
||||
|
||||
bool ExportAsJson(const wxString& json_data, const wxString& export_name = wxEmptyString);
|
||||
bool ExportAsZip(const std::vector<wxString>& sources, const wxString& export_name);
|
||||
bool AddToZip(wxZipOutputStream& zip, const wxString& fullPathOrTextData, const wxString& rootDir);
|
||||
bool SaveAsZip(const std::vector<wxString>& sourcePaths, const wxString& zipFullPath);
|
||||
|
||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||
};
|
||||
|
||||
class CenteredMultiLinePanel : public wxPanel
|
||||
{
|
||||
static constexpr double m_block_gap = 0.55;
|
||||
static constexpr double m_line_height = 1.15;
|
||||
std::vector<wxString> m_lines;
|
||||
|
||||
public:
|
||||
CenteredMultiLinePanel(wxWindow* parent, const std::vector<wxString>& lines = {})
|
||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE)
|
||||
{
|
||||
SetFont(Label::Body_14);
|
||||
SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
Bind(wxEVT_PAINT, &CenteredMultiLinePanel::OnPaint, this);
|
||||
Bind(wxEVT_SIZE, &CenteredMultiLinePanel::OnSize, this);
|
||||
|
||||
SetText(lines);
|
||||
}
|
||||
|
||||
void SetText(const std::vector<wxString>& lines)
|
||||
{
|
||||
if(!lines.empty()){
|
||||
m_lines = lines;
|
||||
Refresh();
|
||||
UpdateMinSize();
|
||||
}
|
||||
}
|
||||
private:
|
||||
std::vector<wxString> Wrap(wxDC& dc, const wxString& text, int maxW)
|
||||
{
|
||||
std::vector<wxString> out;
|
||||
wxString cur;
|
||||
for (const auto& word : wxSplit(text, ' ', true)) {
|
||||
wxString trial = cur.empty() ? word : cur + " " + word;
|
||||
wxCoord w, h;
|
||||
dc.GetTextExtent(trial, &w, &h);
|
||||
if (w <= maxW)
|
||||
cur = trial;
|
||||
else if (!cur.empty()){
|
||||
out.push_back(cur);
|
||||
cur = word;
|
||||
}
|
||||
else
|
||||
out.push_back(word);
|
||||
}
|
||||
if (!cur.empty())
|
||||
out.push_back(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
void OnPaint(wxPaintEvent&)
|
||||
{
|
||||
wxAutoBufferedPaintDC dc(this);
|
||||
dc.Clear();
|
||||
|
||||
int cWidth = GetClientSize().GetWidth();
|
||||
if (m_lines.empty() || cWidth < 50) return;
|
||||
|
||||
dc.SetTextForeground(GetForegroundColour());
|
||||
|
||||
int y = 0;
|
||||
for (size_t i = 0; i < m_lines.size(); ++i) {
|
||||
wxCoord tw, th;
|
||||
for (auto& line : Wrap(dc, m_lines[i], cWidth)) {
|
||||
line.Trim();
|
||||
line.Trim(false);
|
||||
dc.GetTextExtent(line, &tw, &th);
|
||||
dc.DrawText(line, (cWidth - tw) / 2, y);
|
||||
y += static_cast<int>(th * m_line_height);
|
||||
}
|
||||
if (i < m_lines.size() - 1)
|
||||
y += static_cast<int>(th * m_block_gap);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateMinSize()
|
||||
{
|
||||
if (m_lines.empty() || !GetParent()) {
|
||||
SetMinSize(wxDefaultSize);
|
||||
return;
|
||||
}
|
||||
wxClientDC dc(this);
|
||||
int cWidth = GetClientSize().GetWidth();
|
||||
|
||||
int y = 0;
|
||||
for (size_t i = 0; i < m_lines.size(); ++i) {
|
||||
wxCoord th;
|
||||
for (auto& line : Wrap(dc, m_lines[i], cWidth)) {
|
||||
dc.GetTextExtent(line, nullptr, &th);
|
||||
y += static_cast<int>(th * m_line_height);
|
||||
}
|
||||
if (i < m_lines.size() - 1)
|
||||
y += static_cast<int>(th * m_block_gap);
|
||||
}
|
||||
|
||||
if(GetMinSize().GetHeight() != y){
|
||||
SetMinSize(wxSize(-1, y));
|
||||
if(GetParent())
|
||||
GetParent()->Layout();
|
||||
}
|
||||
}
|
||||
|
||||
void OnSize(wxSizeEvent& e)
|
||||
{
|
||||
Refresh();
|
||||
UpdateMinSize();
|
||||
e.Skip();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -44,7 +44,7 @@ public:
|
||||
const wxString preset_name)
|
||||
: Slic3r::GUI::DPIDialog(parent,
|
||||
wxID_ANY,
|
||||
"3DPrinterOS Cloud upload options",
|
||||
_L("3DPrinterOS Cloud upload options"),
|
||||
wxDefaultPosition,
|
||||
wxSize(100 * Slic3r::GUI::wxGetApp().em_unit(), -1),
|
||||
wxDEFAULT_DIALOG_STYLE),
|
||||
@@ -54,15 +54,15 @@ public:
|
||||
SetBackgroundColour(*wxWHITE);
|
||||
SetForegroundColour(*wxBLACK);
|
||||
|
||||
singleRadio = new wxRadioButton(this, wxID_ANY, "Single file", wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
|
||||
projectRadio = new wxRadioButton(this, wxID_ANY, "Project File");
|
||||
projectsLabel = new wxStaticText(this, wxID_ANY, "Project:");
|
||||
wxStaticText* printerLabel = new wxStaticText(this, wxID_ANY, "Printer type:");
|
||||
singleRadio = new wxRadioButton(this, wxID_ANY, _L("Single file"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
|
||||
projectRadio = new wxRadioButton(this, wxID_ANY, _L("Project File"));
|
||||
projectsLabel = new wxStaticText(this, wxID_ANY, _L("Project:"));
|
||||
wxStaticText* printerLabel = new wxStaticText(this, wxID_ANY, _L("Printer type:"));
|
||||
|
||||
projectsComboBox = new wxComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON);
|
||||
printerTypeComboBox = new wxComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON | wxTE_READONLY);
|
||||
|
||||
printerWarningLabel = new wxStaticText(this, wxID_ANY, "Printer type not found, please select manually.");
|
||||
printerWarningLabel = new wxStaticText(this, wxID_ANY, _L("Printer type not found, please select manually."));
|
||||
printerWarningLabel->SetForegroundColour(*wxRED);
|
||||
printerWarningLabel->Hide();
|
||||
|
||||
@@ -83,8 +83,8 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
okButton = new wxButton(this, wxID_OK, "OK");
|
||||
wxButton* cancelButton = new wxButton(this, wxID_CANCEL, "Cancel");
|
||||
okButton = new wxButton(this, wxID_OK, _L("OK"));
|
||||
wxButton* cancelButton = new wxButton(this, wxID_CANCEL, _L("Cancel"));
|
||||
|
||||
wxBoxSizer* radioSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
wxBoxSizer* btnSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
@@ -120,7 +120,7 @@ public:
|
||||
wxRadioButton* selectedRadio = dynamic_cast<wxRadioButton*>(event.GetEventObject());
|
||||
if (selectedRadio) {
|
||||
wxString label = selectedRadio->GetLabel();
|
||||
if (label == wxString("Project File")) {
|
||||
if (label == _L("Project File")) {
|
||||
projectsComboBox->Show();
|
||||
projectsLabel->Show();
|
||||
} else {
|
||||
@@ -182,8 +182,8 @@ public:
|
||||
SetForegroundColour(*wxBLACK);
|
||||
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(new wxStaticText(this, wxID_ANY, "Authorizing..."), 1, wxALL | wxCENTER, 10);
|
||||
auto* cancelBtn = new wxButton(this, wxID_CANCEL, "Cancel");
|
||||
sizer->Add(new wxStaticText(this, wxID_ANY, _L("Authorizing...")), 1, wxALL | wxCENTER, 10);
|
||||
auto* cancelBtn = new wxButton(this, wxID_CANCEL, _L("Cancel"));
|
||||
sizer->Add(cancelBtn, 0, wxALL | wxALIGN_CENTER, 10);
|
||||
SetSizerAndFit(sizer);
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#include "CrealityHostDiscovery.hpp"
|
||||
#include "cxmdns.h"
|
||||
#include "Http.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
struct ModelEntry { const char* code; const char* name; };
|
||||
constexpr ModelEntry kCfsCapableModels[] = {
|
||||
{"F008", "K2 Plus"},
|
||||
{"F012", "K2 Pro"},
|
||||
{"F021", "K2"},
|
||||
};
|
||||
|
||||
bool is_cfs_capable(const std::string& code)
|
||||
{
|
||||
for (const auto& m : kCfsCapableModels)
|
||||
if (code == m.code) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string model_name_for(const std::string& code)
|
||||
{
|
||||
for (const auto& m : kCfsCapableModels)
|
||||
if (code == m.code) return m.name;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Extract the device suffix from a service name like
|
||||
// "_Creality-543324280CDB19._udp.local." and synthesise a hostname-ish label
|
||||
// (e.g. "K2-DB19" using the last 4 hex of the MAC-derived suffix).
|
||||
std::string hostname_from_service(const std::string& service_name)
|
||||
{
|
||||
auto dash = service_name.find_last_of('-');
|
||||
if (dash == std::string::npos) return {};
|
||||
auto dot = service_name.find('.', dash);
|
||||
if (dot == std::string::npos) return {};
|
||||
std::string suffix = service_name.substr(dash + 1, dot - dash - 1);
|
||||
if (suffix.size() >= 4) {
|
||||
return "K2-" + suffix.substr(suffix.size() - 4);
|
||||
}
|
||||
return suffix.empty() ? std::string{} : "K2-" + suffix;
|
||||
}
|
||||
|
||||
// Synchronously probe http://<ip>/info for {model, mac}. Short timeout --
|
||||
// we don't want one slow host to drag down discovery.
|
||||
void probe_info(CrealityHost& host)
|
||||
{
|
||||
const std::string url = "http://" + host.ip + "/info";
|
||||
auto http = Http::get(url);
|
||||
http.timeout_connect(2)
|
||||
.timeout_max(4)
|
||||
.on_complete([&host](std::string body, unsigned /*status*/) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(body);
|
||||
if (j.contains("model") && j["model"].is_string())
|
||||
host.model_code = j["model"].get<std::string>();
|
||||
if (j.contains("mac") && j["mac"].is_string())
|
||||
host.mac = j["mac"].get<std::string>();
|
||||
if (is_cfs_capable(host.model_code)) {
|
||||
host.cfs_capable = true;
|
||||
host.model_name = model_name_for(host.model_code);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "CrealityHostDiscovery: /info parse failed for "
|
||||
<< host.ip << ": " << e.what();
|
||||
}
|
||||
})
|
||||
.on_error([&host](std::string /*body*/, std::string error, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityHostDiscovery: /info GET failed for "
|
||||
<< host.ip << ": " << error << " (HTTP " << status << ")";
|
||||
})
|
||||
.perform_sync();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<CrealityHost> CrealityHostDiscovery::scan(bool probe)
|
||||
{
|
||||
const std::vector<std::string> prefixes{ "Creality", "creality" };
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityHostDiscovery: starting DNS-SD discovery (prefixes: Creality, creality)";
|
||||
|
||||
auto raw = cxnet::syncDiscoveryService(prefixes);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityHostDiscovery: mDNS returned " << raw.size() << " match(es)";
|
||||
|
||||
std::vector<CrealityHost> hosts;
|
||||
hosts.reserve(raw.size());
|
||||
|
||||
// Dedupe by IP -- one printer may announce twice if multi-homed or if
|
||||
// we capture both IPv4/IPv6 replies.
|
||||
std::vector<std::string> seen_ips;
|
||||
for (const auto& m : raw) {
|
||||
if (m.machineIp.empty()) continue;
|
||||
if (std::find(seen_ips.begin(), seen_ips.end(), m.machineIp) != seen_ips.end())
|
||||
continue;
|
||||
seen_ips.push_back(m.machineIp);
|
||||
|
||||
CrealityHost h;
|
||||
h.ip = m.machineIp;
|
||||
h.service_name = m.answer;
|
||||
h.hostname = hostname_from_service(m.answer);
|
||||
|
||||
if (probe) {
|
||||
probe_info(h);
|
||||
}
|
||||
|
||||
hosts.push_back(std::move(h));
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityHostDiscovery: " << hosts.size() << " unique host(s) after dedup";
|
||||
|
||||
return hosts;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef slic3r_CrealityHostDiscovery_hpp_
|
||||
#define slic3r_CrealityHostDiscovery_hpp_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// One discovered Creality K-series host on the LAN.
|
||||
struct CrealityHost
|
||||
{
|
||||
std::string ip; // dotted-quad IPv4
|
||||
std::string service_name; // raw mDNS service type, e.g. "_Creality-543324280CDB19._udp.local."
|
||||
std::string hostname; // e.g. "K2-DB19" (derived from service-name suffix)
|
||||
std::string model_code; // "F008" / "F012" / "F021" (empty if /info probe failed)
|
||||
std::string model_name; // "K2 Plus" / "K2 Pro" / "K2" (empty if model not in our table)
|
||||
std::string mac; // from /info if probed
|
||||
bool cfs_capable = false; // true when model_code is in the K2 family
|
||||
};
|
||||
|
||||
// Synchronous LAN discovery for Creality K-series printers via DNS-SD mDNS.
|
||||
//
|
||||
// Sends a meta-discovery query (_services._dns-sd._udp.local.) and listens
|
||||
// for ~5 seconds for service announcements whose type-name contains the
|
||||
// "Creality" / "creality" substring. K-series firmware announces each
|
||||
// printer under a per-device-unique type _Creality-<MAC-derived-hex>._udp.local,
|
||||
// so a fixed-name query does not work -- the meta-discovery is the only
|
||||
// reliable way to find them.
|
||||
//
|
||||
// When probe_info is true, each discovered host is followed up with an HTTP
|
||||
// GET http://<ip>/info call to fetch the printer's model code (F008/F012/F021)
|
||||
// and MAC. The probe step adds ~2-4 seconds per host but yields enriched
|
||||
// results that let the UI display "K2" / "K2 Plus" / "K2 Pro" instead of
|
||||
// just an IP.
|
||||
//
|
||||
// Call from a background thread -- the function blocks for at least 5 seconds.
|
||||
class CrealityHostDiscovery
|
||||
{
|
||||
public:
|
||||
static std::vector<CrealityHost> scan(bool probe_info = true);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "CrealityPrint.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <exception>
|
||||
#include <boost/format.hpp>
|
||||
@@ -88,7 +89,8 @@ bool CrealityPrint::test(wxString& msg) const
|
||||
// Here we do not have to add custom "Host" header - the url contains host filled by user and libCurl will set the header by itself.
|
||||
auto http = Http::get(std::move(url));
|
||||
set_auth(http);
|
||||
http.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
http.timeout_max(5)
|
||||
.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status %
|
||||
body;
|
||||
res = false;
|
||||
@@ -96,6 +98,15 @@ bool CrealityPrint::test(wxString& msg) const
|
||||
})
|
||||
.on_complete([&, this](std::string body, unsigned) {
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got version: %2%") % name % body;
|
||||
try {
|
||||
auto info = json::parse(body);
|
||||
if (info.contains("model")) {
|
||||
m_model = info["model"].get<std::string>();
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Detected model: %2%") % name % m_model;
|
||||
}
|
||||
} catch (const json::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Failed to parse /info response: %2%") % name % e.what();
|
||||
}
|
||||
})
|
||||
#ifdef WIN32
|
||||
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
||||
@@ -130,14 +141,19 @@ bool CrealityPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn,
|
||||
|
||||
auto http = Http::post(url); // std::move(url));
|
||||
set_auth(http);
|
||||
http.form_add("path", upload_parent_path.string())
|
||||
.form_add_file("file", upload_data.source_path.string(), upload_filename.string())
|
||||
if (!supports_multi_color_print())
|
||||
http.form_add("path", upload_parent_path.string());
|
||||
http.form_add_file("file", upload_data.source_path.string(), upload_filename.string())
|
||||
|
||||
.on_complete([&](std::string body, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body;
|
||||
|
||||
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) {
|
||||
start_print(safe_filename(upload_filename.string()));
|
||||
wxString errormsg;
|
||||
if (!start_print(errormsg, safe_filename(upload_filename.string()), upload_data.extended_info)) {
|
||||
error_fn(std::move(errormsg));
|
||||
res = false;
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
@@ -182,53 +198,231 @@ std::string CrealityPrint::safe_filename(const std::string &filename) const
|
||||
return safe_filename;
|
||||
}
|
||||
|
||||
void CrealityPrint::start_print(const std::string &filename) const
|
||||
static void ws_connect(net::io_context& ioc, websocket::stream<beast::tcp_stream>& ws,
|
||||
const std::string& host_url, const std::string& port)
|
||||
{
|
||||
std::string host = Http::get_host_from_url(host_url);
|
||||
|
||||
tcp::resolver resolver{ioc};
|
||||
beast::get_lowest_layer(ws).expires_after(std::chrono::seconds(5));
|
||||
auto const results = resolver.resolve(host, port);
|
||||
beast::get_lowest_layer(ws).connect(results);
|
||||
host += ':' + std::to_string(beast::get_lowest_layer(ws).socket().remote_endpoint().port());
|
||||
|
||||
ws.set_option(websocket::stream_base::decorator(
|
||||
[](websocket::request_type& req) {
|
||||
req.set(http::field::user_agent,
|
||||
std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
|
||||
}));
|
||||
ws.handshake(host, "/");
|
||||
|
||||
#ifdef _WIN32
|
||||
DWORD recv_timeout = 3000;
|
||||
#else
|
||||
struct timeval recv_timeout = {3, 0};
|
||||
#endif
|
||||
setsockopt(beast::get_lowest_layer(ws).socket().native_handle(),
|
||||
SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&recv_timeout), sizeof(recv_timeout));
|
||||
}
|
||||
|
||||
static std::string ws_send_and_read(websocket::stream<beast::tcp_stream>& ws, const json& cmd, const std::string& expected_key, int max_reads = 20)
|
||||
{
|
||||
ws.write(net::buffer(to_string(cmd)));
|
||||
|
||||
for (int i = 0; i < max_reads; i++) {
|
||||
beast::flat_buffer buf;
|
||||
beast::error_code ec;
|
||||
ws.read(buf, ec);
|
||||
if (ec == net::error::would_block)
|
||||
break;
|
||||
if (ec)
|
||||
throw beast::system_error{ec};
|
||||
std::string msg = beast::buffers_to_string(buf.data());
|
||||
if (msg.find(expected_key) != std::string::npos)
|
||||
return msg;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(warning) << "CrealityPrint: No '" << expected_key << "' response after " << max_reads << " messages";
|
||||
return {};
|
||||
}
|
||||
|
||||
void CrealityPrint::query_model() const
|
||||
{
|
||||
if (!m_model.empty())
|
||||
return;
|
||||
|
||||
wxString msg;
|
||||
test(msg);
|
||||
}
|
||||
|
||||
bool CrealityPrint::supports_multi_color_print() const
|
||||
{
|
||||
query_model();
|
||||
// K2-platform printers with CFS support
|
||||
return m_model == "F008" // K2 Plus
|
||||
|| m_model == "F012" // K2 Pro
|
||||
|| m_model == "F021" // K2
|
||||
|| m_model == "F022"; // SPARKX i7
|
||||
}
|
||||
|
||||
std::string CrealityPrint::model_name() const
|
||||
{
|
||||
static const std::map<std::string, std::string> names = {
|
||||
{"F008", "K2 Plus"},
|
||||
{"F012", "K2 Pro"},
|
||||
{"F021", "K2"},
|
||||
{"F022", "SPARKX i7"},
|
||||
};
|
||||
query_model();
|
||||
if (m_model.empty())
|
||||
return "unreachable";
|
||||
auto it = names.find(m_model);
|
||||
return it != names.end() ? it->second : "unknown (" + m_model + ")";
|
||||
}
|
||||
|
||||
std::string CrealityPrint::query_boxes_info() const
|
||||
{
|
||||
try {
|
||||
std::string host = m_host;
|
||||
auto const port = "9999";
|
||||
net::io_context ioc;
|
||||
websocket::stream<beast::tcp_stream> ws{ioc};
|
||||
ws_connect(ioc, ws, m_host, "9999");
|
||||
|
||||
json j2 = {
|
||||
{ "method", "set" },
|
||||
{
|
||||
"params", {
|
||||
{ "opGcodeFile", "printprt:/usr/data/printer_data/gcodes/" + filename }
|
||||
}
|
||||
}
|
||||
};
|
||||
json boxs_query = {{"method", "get"}, {"params", {{"boxsInfo", 1}}}};
|
||||
std::string result = ws_send_and_read(ws, boxs_query, "boxsInfo");
|
||||
ws.close(websocket::close_code::normal);
|
||||
return result;
|
||||
} catch (std::exception const& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "CrealityPrint: Failed to query boxsInfo: " << e.what();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::string CrealityPrint::get_print_host_webui(DynamicPrintConfig* config)
|
||||
{
|
||||
// K-series printers (K2 / K2 Plus / K2 Pro) ship with Mainsail on port 4408.
|
||||
// Port 80 hosts only the Creality control / upload API, which returns 404
|
||||
// for unknown paths and therefore renders as a blank/404 page in Orca's
|
||||
// Device WebView. Default to the Mainsail URL when the user hasn't
|
||||
// explicitly set print_host_webui.
|
||||
if (config == nullptr)
|
||||
return {};
|
||||
|
||||
std::string explicit_url = config->opt_string("print_host_webui");
|
||||
if (!explicit_url.empty())
|
||||
return explicit_url;
|
||||
|
||||
std::string host = config->opt_string("print_host");
|
||||
if (host.empty())
|
||||
return {};
|
||||
|
||||
if (boost::algorithm::istarts_with(host, "http://"))
|
||||
host = host.substr(7);
|
||||
else if (boost::algorithm::istarts_with(host, "https://"))
|
||||
host = host.substr(8);
|
||||
if (auto slash = host.find('/'); slash != std::string::npos)
|
||||
host = host.substr(0, slash);
|
||||
if (auto colon = host.find(':'); colon != std::string::npos)
|
||||
host = host.substr(0, colon);
|
||||
|
||||
return "http://" + host + ":4408/";
|
||||
}
|
||||
|
||||
bool CrealityPrint::start_print(wxString &msg, const std::string &filename, const std::map<std::string, std::string>& extended_info) const
|
||||
{
|
||||
try {
|
||||
const std::string gcode_path = "/mnt/UDISK/printer_data/gcodes/" + filename;
|
||||
|
||||
net::io_context ioc;
|
||||
websocket::stream<beast::tcp_stream> ws{ioc};
|
||||
ws_connect(ioc, ws, m_host, "9999");
|
||||
|
||||
tcp::resolver resolver{ioc};
|
||||
websocket::stream<tcp::socket> ws{ioc};
|
||||
if (supports_multi_color_print()) {
|
||||
// Build colorMatch list from the mapping provided by the dialog
|
||||
bool use_spool_holder = false;
|
||||
json color_list = json::array();
|
||||
for (int i = 0; ; i++) {
|
||||
auto it = extended_info.find("colorMatch_" + std::to_string(i));
|
||||
if (it == extended_info.end())
|
||||
break;
|
||||
// Value format: "toolId\ttype\tcolor\tboxId\tmaterialId"
|
||||
auto val = it->second;
|
||||
std::vector<std::string> parts;
|
||||
std::istringstream iss(val);
|
||||
std::string part;
|
||||
while (std::getline(iss, part, '\t'))
|
||||
parts.push_back(part);
|
||||
if (parts.size() >= 5) {
|
||||
int box_id = std::stoi(parts[3]);
|
||||
if (box_id == 0)
|
||||
use_spool_holder = true;
|
||||
color_list.push_back({
|
||||
{"id", parts[0]},
|
||||
{"type", parts[1]},
|
||||
{"color", parts[2]},
|
||||
{"boxId", box_id},
|
||||
{"materialId", std::stoi(parts[4])}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
auto const results = resolver.resolve(host, port);
|
||||
|
||||
auto ep = net::connect(ws.next_layer(), results);
|
||||
|
||||
host += ':' + std::to_string(ep.port());
|
||||
|
||||
ws.set_option(websocket::stream_base::decorator(
|
||||
[](websocket::request_type& req)
|
||||
int enable_self_test = 0;
|
||||
{
|
||||
req.set(http::field::user_agent,
|
||||
std::string(BOOST_BEAST_VERSION_STRING) +
|
||||
" websocket-client-coro");
|
||||
}));
|
||||
auto it = extended_info.find("enableSelfTest");
|
||||
if (it != extended_info.end())
|
||||
enable_self_test = std::stoi(it->second);
|
||||
}
|
||||
|
||||
ws.handshake(host, "/");
|
||||
|
||||
ws.write(net::buffer(to_string(j2)));
|
||||
if (use_spool_holder) {
|
||||
json cmd = {
|
||||
{"method", "set"},
|
||||
{"params", {
|
||||
{"opGcodeFile", "printprt:" + gcode_path},
|
||||
{"enableSelfTest", enable_self_test}
|
||||
}}
|
||||
};
|
||||
ws.write(net::buffer(to_string(cmd)));
|
||||
} else {
|
||||
json color_match = {
|
||||
{"method", "set"},
|
||||
{"params", {
|
||||
{"colorMatch", {
|
||||
{"path", gcode_path},
|
||||
{"list", color_list}
|
||||
}}
|
||||
}}
|
||||
};
|
||||
ws.write(net::buffer(to_string(color_match)));
|
||||
|
||||
beast::flat_buffer buffer;
|
||||
json multi_color_print = {
|
||||
{"method", "set"},
|
||||
{"params", {
|
||||
{"multiColorPrint", {
|
||||
{"gcode", gcode_path},
|
||||
{"enableSelfTest", enable_self_test}
|
||||
}}
|
||||
}}
|
||||
};
|
||||
ws.write(net::buffer(to_string(multi_color_print)));
|
||||
}
|
||||
} else {
|
||||
json cmd = {
|
||||
{"method", "set"},
|
||||
{"params", {
|
||||
{"opGcodeFile", "printprt:/usr/data/printer_data/gcodes/" + filename}
|
||||
}}
|
||||
};
|
||||
ws.write(net::buffer(to_string(cmd)));
|
||||
|
||||
ws.read(buffer);
|
||||
beast::flat_buffer buffer;
|
||||
ws.read(buffer);
|
||||
}
|
||||
|
||||
ws.close(websocket::close_code::normal);
|
||||
return true;
|
||||
} catch(std::exception const& e) {
|
||||
std::cerr << "Error: " << e.what() << std::endl;
|
||||
BOOST_LOG_TRIVIAL(error) << "CrealityPrint: Error starting print: " << e.what();
|
||||
msg = wxString::FromUTF8(e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef slic3r_CrealityPrint_hpp_
|
||||
#define slic3r_CrealityPrint_hpp_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
#include <boost/optional.hpp>
|
||||
@@ -29,6 +30,13 @@ public:
|
||||
virtual bool test(wxString& curl_msg) const override;
|
||||
PrintHostPostUploadActions get_post_upload_actions() const;
|
||||
bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override;
|
||||
bool supports_multi_color_print() const;
|
||||
std::string query_boxes_info() const;
|
||||
std::string model_name() const;
|
||||
|
||||
// Mainsail on K-series printers listens on port 4408. Use that as the
|
||||
// default Device-tab WebView URL when the user has not set print_host_webui.
|
||||
static std::string get_print_host_webui(DynamicPrintConfig *config);
|
||||
|
||||
protected:
|
||||
virtual void set_auth(Http& http) const;
|
||||
@@ -39,10 +47,12 @@ private:
|
||||
std::string m_cafile;
|
||||
std::string m_web_ui;
|
||||
bool m_ssl_revoke_best_effort;
|
||||
mutable std::string m_model;
|
||||
|
||||
std::string make_url(const std::string& path) const;
|
||||
void start_print(const std::string& path) const;
|
||||
bool start_print(wxString& msg, const std::string& filename, const std::map<std::string, std::string>& extended_info) const;
|
||||
std::string safe_filename(const std::string& filename) const;
|
||||
void query_model() const;
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
#include "CrealityPrintAgent.hpp"
|
||||
#include "CrealityPrint.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* CrealityPrintAgent_VERSION = "0.1.0";
|
||||
|
||||
bool has_visible_base_preset(const PresetCollection& filaments, const std::string& filament_id)
|
||||
{
|
||||
for (const auto& p : filaments.get_presets()) {
|
||||
if (p.is_visible && p.is_compatible
|
||||
&& filaments.get_preset_base(p) == &p
|
||||
&& p.filament_id == filament_id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Score visible compatible filament presets against the CFS spool metadata and
|
||||
// return the best-matching filament_id. Scoring:
|
||||
// +20 preset name contains brand_name as a substring
|
||||
// (e.g. "Hyper PLA" in "Hyper PLA @Creality K2 0.4 nozzle")
|
||||
// +10 preset name contains the vendor substring (e.g. "Creality")
|
||||
// Tiebreak: prefer the SYSTEM (shipped) preset over user copies. Brand-
|
||||
// specific system presets carry their own filament_id; user copies of
|
||||
// generic presets inherit a generic filament_id from their parent, so
|
||||
// preferring the user copy can collapse a brand-specific match back to
|
||||
// "Generic PLA" via the inherited id. Plus: this code targets upstream
|
||||
// OrcaSlicer where shipping the user's local tuning would be wrong.
|
||||
// Requires the preset's declared filament_type to equal the spool's base type
|
||||
// (PLA/PETG/ABS/...) so we never auto-pick a PETG preset for a PLA spool.
|
||||
// Falls back to filaments.filament_id_by_type(base_type) when nothing scores.
|
||||
std::string CrealityPrintAgent::match_filament_preset(const PresetCollection& filaments,
|
||||
const std::string& vendor,
|
||||
const std::string& brand_name,
|
||||
const std::string& base_type)
|
||||
{
|
||||
auto to_lower = [](std::string s) {
|
||||
for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
};
|
||||
|
||||
const std::string vendor_lower = to_lower(vendor);
|
||||
const std::string brand_lower = to_lower(brand_name);
|
||||
const std::string type_lower = to_lower(base_type);
|
||||
|
||||
struct Match {
|
||||
const Preset* preset;
|
||||
int score;
|
||||
bool is_user;
|
||||
};
|
||||
std::vector<Match> matches;
|
||||
|
||||
int considered = 0;
|
||||
for (const auto& p : filaments.get_presets()) {
|
||||
if (!p.is_visible || !p.is_compatible) continue;
|
||||
// Note: we deliberately do NOT filter on get_preset_base(p) == &p.
|
||||
// K2 owners frequently keep tweaked copies of system presets
|
||||
// (e.g. "Creality Hyper PLA @K2 (Harky)" with their per-spool PA),
|
||||
// which are derived presets — filtering to bases-only would skip
|
||||
// exactly the presets users care about most.
|
||||
++considered;
|
||||
|
||||
std::string preset_type;
|
||||
if (const auto* ft = p.config.option<ConfigOptionStrings>("filament_type"))
|
||||
if (!ft->values.empty()) preset_type = ft->values.front();
|
||||
if (to_lower(preset_type) != type_lower) continue;
|
||||
|
||||
const std::string name_lower = to_lower(p.name);
|
||||
int score = 0;
|
||||
if (!brand_lower.empty() && name_lower.find(brand_lower) != std::string::npos)
|
||||
score += 20;
|
||||
if (!vendor_lower.empty() && name_lower.find(vendor_lower) != std::string::npos)
|
||||
score += 10;
|
||||
|
||||
if (score > 0)
|
||||
matches.push_back({&p, score, !p.is_system && !p.is_default});
|
||||
}
|
||||
|
||||
if (matches.empty()) {
|
||||
const std::string fallback = filaments.filament_id_by_type(base_type);
|
||||
const bool fallback_ok = has_visible_base_preset(filaments, fallback);
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityPrintAgent: no preset scored for spool {" << vendor << " "
|
||||
<< brand_name << " (" << base_type << ")} after considering " << considered
|
||||
<< " presets; falling back to generic preset id \"" << fallback << "\""
|
||||
<< (fallback_ok ? "" : " (NOT visible — returning empty)");
|
||||
return fallback_ok ? fallback : std::string();
|
||||
}
|
||||
|
||||
std::sort(matches.begin(), matches.end(),
|
||||
[](const Match& a, const Match& b) {
|
||||
if (a.score != b.score) return a.score > b.score;
|
||||
if (a.is_user != b.is_user) return !a.is_user; // prefer system over user
|
||||
return false;
|
||||
});
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityPrintAgent: matched spool {" << vendor << " " << brand_name
|
||||
<< " (" << base_type << ")} -> preset \"" << matches.front().preset->name
|
||||
<< "\" (score=" << matches.front().score
|
||||
<< ", " << matches.size() << " candidate(s) of " << considered << " considered)";
|
||||
|
||||
return matches.front().preset->filament_id;
|
||||
}
|
||||
|
||||
CrealityPrintAgent::CrealityPrintAgent(std::string log_dir)
|
||||
: MoonrakerPrinterAgent(std::move(log_dir))
|
||||
{
|
||||
}
|
||||
|
||||
AgentInfo CrealityPrintAgent::get_agent_info_static()
|
||||
{
|
||||
return AgentInfo{
|
||||
"crealityprint",
|
||||
"CrealityPrint",
|
||||
CrealityPrintAgent_VERSION,
|
||||
"Creality K-series printer agent (CFS-aware filament sync)"
|
||||
};
|
||||
}
|
||||
|
||||
std::string CrealityPrintAgent::normalize_filament_type(const std::string& filament_type)
|
||||
{
|
||||
static const std::vector<std::string> bases = {
|
||||
"PETG", "PET", "PLA", "ABS", "ASA", "TPU", "PC", "PA", "PVA", "HIPS"
|
||||
};
|
||||
for (const auto& base : bases) {
|
||||
if (filament_type.rfind(base, 0) == 0) return base;
|
||||
}
|
||||
return filament_type;
|
||||
}
|
||||
|
||||
// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info().
|
||||
// Schema (verified 2026-05-06 against K2 Combo F021 firmware v1.1.260206):
|
||||
// { "boxsInfo": { "materialBoxs": [
|
||||
// { "id": int, "state": int, "type": int, // type 0 = CFS, 1 = single-spool external
|
||||
// "materials": [
|
||||
// { "id": int, "state": int, // state 1 = loaded
|
||||
// "vendor": str, "type": str, "name": str,
|
||||
// "color": "#0RRGGBB" }, ...
|
||||
// ]}, ...
|
||||
// ]}}
|
||||
bool CrealityPrintAgent::parse_cfs_response(const std::string& response,
|
||||
std::vector<CFSSlot>& slots,
|
||||
int& box_count,
|
||||
std::string& error)
|
||||
{
|
||||
using nlohmann::json;
|
||||
|
||||
slots.clear();
|
||||
box_count = 0;
|
||||
|
||||
if (response.empty()) {
|
||||
error = "empty response";
|
||||
return false;
|
||||
}
|
||||
|
||||
json resp;
|
||||
try {
|
||||
resp = json::parse(response);
|
||||
} catch (const std::exception& e) {
|
||||
error = std::string("JSON parse error: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!resp.contains("boxsInfo") || !resp["boxsInfo"].contains("materialBoxs")) {
|
||||
error = "invalid schema (missing boxsInfo.materialBoxs)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sequential AMS-style index for accepted CFS boxes. The K2's raw box.id has
|
||||
// gaps (id 0 is the external spool holder, type=1, skipped) — using the raw id
|
||||
// would publish phantom slots for the gap. Renumber accepted boxes 0,1,2,...
|
||||
int cfs_count = 0;
|
||||
for (const auto& box : resp["boxsInfo"]["materialBoxs"]) {
|
||||
const int box_st = box.value("state", 0);
|
||||
const int box_type = box.value("type", 0);
|
||||
if (box_st != 1) continue; // inactive boxes
|
||||
if (box_type != 0) continue; // non-CFS (external spool holder, handled separately by upload dialog)
|
||||
|
||||
const int cfs_index = cfs_count++;
|
||||
|
||||
if (!box.contains("materials") || !box["materials"].is_array())
|
||||
continue;
|
||||
|
||||
for (const auto& mat : box["materials"]) {
|
||||
// CFS slot state encoding observed across K2 family firmwares:
|
||||
// * K2 (base) / K2 Pro : 0 = empty, 1 = loaded.
|
||||
// * K2 Plus (1.1.5.5/CFS 1.4.2 onwards): 0 = empty,
|
||||
// 1 = loaded AND currently
|
||||
// selected as the active
|
||||
// spool for printing,
|
||||
// 2 = loaded but not selected.
|
||||
// We treat anything non-zero as loaded. Belt-and-braces: also skip
|
||||
// entries that look blank (no vendor and no type) regardless of state.
|
||||
const int s_state = mat.value("state", 0);
|
||||
const std::string s_vendor = mat.value("vendor", std::string());
|
||||
const std::string s_type = mat.value("type", std::string());
|
||||
if (s_state == 0) continue; // explicitly empty
|
||||
if (s_vendor.empty() && s_type.empty()) continue; // blank entry — likely empty under a different state encoding
|
||||
|
||||
CFSSlot s;
|
||||
s.box_id = cfs_index;
|
||||
s.slot_id = mat.value("id", 0);
|
||||
s.vendor = s_vendor;
|
||||
s.brand_name = mat.value("name", "");
|
||||
s.filament_type = s_type;
|
||||
s.color_hex = mat.value("color", "#FFFFFF");
|
||||
|
||||
// Creality reports colour as "#0RRGGBB" (8 chars with a leading zero
|
||||
// after '#'). Normalise to standard "#RRGGBB".
|
||||
if (s.color_hex.size() == 8 && s.color_hex[0] == '#')
|
||||
s.color_hex = "#" + s.color_hex.substr(2);
|
||||
|
||||
slots.push_back(std::move(s));
|
||||
}
|
||||
}
|
||||
|
||||
box_count = cfs_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id)
|
||||
{
|
||||
if (device_info.dev_ip.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent";
|
||||
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
|
||||
}
|
||||
|
||||
// Build a CrealityPrint helper so we can use its model detection + WS helpers
|
||||
// (added in upstream PR #13291).
|
||||
DynamicPrintConfig cfg;
|
||||
cfg.set_key_value("print_host", new ConfigOptionString("http://" + device_info.dev_ip));
|
||||
cfg.set_key_value("print_host_webui", new ConfigOptionString(""));
|
||||
cfg.set_key_value("printhost_cafile", new ConfigOptionString(""));
|
||||
cfg.set_key_value("printhost_port", new ConfigOptionString(""));
|
||||
cfg.set_key_value("printhost_apikey", new ConfigOptionString(device_info.api_key));
|
||||
cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false));
|
||||
|
||||
CrealityPrint host(&cfg);
|
||||
|
||||
// Defer to base if this isn't a K-series board with CFS firmware support.
|
||||
if (!host.supports_multi_color_print()) {
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityPrintAgent: " << host.model_name()
|
||||
<< " is not CFS-capable, deferring to base Moonraker agent";
|
||||
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityPrintAgent: querying CFS slots on " << host.model_name();
|
||||
|
||||
const std::string response = host.query_boxes_info();
|
||||
|
||||
std::vector<CFSSlot> slots;
|
||||
int box_count = 0;
|
||||
std::string parse_err;
|
||||
if (!parse_cfs_response(response, slots, box_count, parse_err)) {
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "CrealityPrintAgent: CFS query failed (" << parse_err << "), "
|
||||
<< "falling back to base agent";
|
||||
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
|
||||
}
|
||||
|
||||
if (box_count == 0) {
|
||||
// No active CFS boxes attached — printer is in direct-spool mode. Let the
|
||||
// base agent take over so the user still gets whatever filament info
|
||||
// Moonraker exposes.
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityPrintAgent: no active CFS boxes, deferring to base agent";
|
||||
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "CrealityPrintAgent: " << box_count << " CFS box(es), "
|
||||
<< slots.size() << " loaded slot(s)";
|
||||
|
||||
// Index loaded slots by (box, slot) for O(1) lookup as we walk the full
|
||||
// box_count * 4 grid, emitting an AmsTrayData entry for each physical slot.
|
||||
std::map<std::pair<int, int>, const CFSSlot*> by_position;
|
||||
for (const auto& s : slots)
|
||||
by_position[{s.box_id, s.slot_id}] = &s;
|
||||
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
|
||||
const int max_slots = box_count * 4;
|
||||
std::vector<AmsTrayData> trays;
|
||||
trays.reserve(max_slots);
|
||||
|
||||
for (int box = 0; box < box_count; ++box) {
|
||||
for (int idx = 0; idx < 4; ++idx) {
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = box * 4 + idx;
|
||||
|
||||
auto it = by_position.find({box, idx});
|
||||
if (it == by_position.end()) {
|
||||
tray.has_filament = false;
|
||||
trays.push_back(std::move(tray));
|
||||
continue;
|
||||
}
|
||||
|
||||
const CFSSlot& s = *it->second;
|
||||
tray.has_filament = true;
|
||||
tray.tray_type = normalize_filament_type(s.filament_type);
|
||||
tray.tray_color = s.color_hex;
|
||||
|
||||
if (bundle) {
|
||||
tray.tray_info_idx = match_filament_preset(
|
||||
bundle->filaments, s.vendor, s.brand_name, tray.tray_type);
|
||||
}
|
||||
|
||||
trays.push_back(std::move(tray));
|
||||
}
|
||||
}
|
||||
|
||||
build_ams_payload(box_count, max_slots - 1, trays);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,67 @@
|
||||
#ifndef __CREALITY_PRINT_AGENT_HPP__
|
||||
#define __CREALITY_PRINT_AGENT_HPP__
|
||||
|
||||
#include "MoonrakerPrinterAgent.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PresetCollection;
|
||||
|
||||
// Filament sync for Creality K-series printers with CFS.
|
||||
//
|
||||
// Inherits MoonrakerPrinterAgent for all communication / certificates / discovery /
|
||||
// binding / print-job operations. Overrides fetch_filament_info() to query the
|
||||
// K-series CFS over its port-9999 WebSocket, convert each loaded slot to an
|
||||
// AmsTrayData entry, and publish via the base-class build_ams_payload() — the
|
||||
// same shape used by QidiPrinterAgent and SnapmakerPrinterAgent.
|
||||
//
|
||||
// Model detection delegated to CrealityPrint::supports_multi_color_print() (PR #13291).
|
||||
// For non-CFS K-series boards or when the WS query fails, falls back to the base
|
||||
// MoonrakerPrinterAgent behaviour.
|
||||
|
||||
class CrealityPrintAgent final : public MoonrakerPrinterAgent
|
||||
{
|
||||
public:
|
||||
struct CFSSlot
|
||||
{
|
||||
int box_id = 0; // CFS unit index (0 for first box, 1 for chained second box)
|
||||
int slot_id = 0; // Slot index within the box (0..3)
|
||||
std::string color_hex; // "#RRGGBB"
|
||||
std::string filament_type; // "PLA", "ABS", "PETG", ...
|
||||
std::string brand_name; // "Hyper PLA", ...
|
||||
std::string vendor; // "Creality", "eSUN", or "" if unknown
|
||||
};
|
||||
|
||||
explicit CrealityPrintAgent(std::string log_dir);
|
||||
~CrealityPrintAgent() override = default;
|
||||
|
||||
static AgentInfo get_agent_info_static();
|
||||
AgentInfo get_agent_info() override { return get_agent_info_static(); }
|
||||
|
||||
bool fetch_filament_info(std::string dev_id) override;
|
||||
|
||||
// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into
|
||||
// a flat list of loaded slots, plus the count of CFS boxes the printer reports.
|
||||
static bool parse_cfs_response(const std::string& response,
|
||||
std::vector<CFSSlot>& slots,
|
||||
int& box_count,
|
||||
std::string& error);
|
||||
|
||||
// Strip PLA/PETG/... subtype suffixes ("PLA Silk", "PLA+", "ABS Pro") to base
|
||||
// type so the preset_bundle->filaments.filament_id_by_type() lookup succeeds.
|
||||
static std::string normalize_filament_type(const std::string& filament_type);
|
||||
|
||||
// Score visible compatible filament presets against the CFS spool metadata and
|
||||
// return the best-matching filament_id. See implementation for scoring details.
|
||||
static std::string match_filament_preset(const PresetCollection& filaments,
|
||||
const std::string& vendor,
|
||||
const std::string& brand_name,
|
||||
const std::string& base_type);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -170,77 +170,12 @@ namespace Slic3r {
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_host_from_url(const std::string& url_in)
|
||||
{
|
||||
std::string url = url_in;
|
||||
// add http:// if there is no scheme
|
||||
size_t double_slash = url.find("//");
|
||||
if (double_slash == std::string::npos)
|
||||
url = "http://" + url;
|
||||
std::string out = url;
|
||||
CURLU* hurl = curl_url();
|
||||
if (hurl) {
|
||||
// Parse the input URL.
|
||||
CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0);
|
||||
if (rc == CURLUE_OK) {
|
||||
// Replace the address.
|
||||
char* host;
|
||||
rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0);
|
||||
if (rc == CURLUE_OK) {
|
||||
char* port;
|
||||
rc = curl_url_get(hurl, CURLUPART_PORT, &port, 0);
|
||||
if (rc == CURLUE_OK && port != nullptr) {
|
||||
out = std::string(host) + ":" + port;
|
||||
curl_free(port);
|
||||
} else {
|
||||
out = host;
|
||||
curl_free(host);
|
||||
}
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to get host form URL " << url;
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to parse URL " << url;
|
||||
curl_url_cleanup(hurl);
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to allocate curl_url";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string get_host_from_url_no_port(const std::string& url_in)
|
||||
{
|
||||
std::string url = url_in;
|
||||
// add http:// if there is no scheme
|
||||
size_t double_slash = url.find("//");
|
||||
if (double_slash == std::string::npos)
|
||||
url = "http://" + url;
|
||||
std::string out = url;
|
||||
CURLU* hurl = curl_url();
|
||||
if (hurl) {
|
||||
// Parse the input URL.
|
||||
CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0);
|
||||
if (rc == CURLUE_OK) {
|
||||
// Replace the address.
|
||||
char* host;
|
||||
rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0);
|
||||
if (rc == CURLUE_OK) {
|
||||
out = host;
|
||||
curl_free(host);
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to get host form URL " << url;
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to parse URL " << url;
|
||||
curl_url_cleanup(hurl);
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "ElegooLink get_host_from_url: failed to allocate curl_url";
|
||||
return out;
|
||||
}
|
||||
|
||||
// NOTE (merge): host parsing was moved into Http::get_host_from_url /
|
||||
// Http::get_host_header_value by the K2 discovery refactor on this branch, so the
|
||||
// former ElegooLink-local get_host_from_url/get_host_from_url_no_port helpers are gone.
|
||||
// main only added the CC2 serial-number lookup below; it is kept here and routed through
|
||||
// Http::get_host_header_value, which has the same host:port semantics the SN cache key
|
||||
// relies on.
|
||||
std::string lookup_cc2_serial_impl(const std::string& printer_model,
|
||||
const std::string& print_host,
|
||||
const std::string& apikey)
|
||||
@@ -248,7 +183,7 @@ namespace Slic3r {
|
||||
if (classify_printer_model(printer_model) != ElegooPrinterType::CC2)
|
||||
return {};
|
||||
|
||||
const std::string host_ip = get_host_from_url(print_host);
|
||||
const std::string host_ip = Http::get_host_header_value(print_host);
|
||||
const std::string token = get_cc2_token(apikey);
|
||||
std::string sn = lookup_sn(host_ip, token);
|
||||
if (sn.empty())
|
||||
@@ -408,9 +343,8 @@ namespace Slic3r {
|
||||
std::string web_path = resources_dir() + "/web/elegoolink/lan_service_web/index.html";
|
||||
std::replace(web_path.begin(), web_path.end(), '\\', '/');
|
||||
web_path = "file://" + web_path;
|
||||
|
||||
const std::string token = get_cc2_token(config->opt_string("printhost_apikey"));
|
||||
const std::string host_ip = get_host_from_url(host);
|
||||
const std::string host_ip = Http::get_host_header_value(host);
|
||||
|
||||
// Pass sn= so the panel can subscribe to the correct MQTT topics.
|
||||
std::string sn = lookup_cc2_serial(config);
|
||||
@@ -551,7 +485,7 @@ namespace Slic3r {
|
||||
msg = format_error(body, error_message.empty() ? "CC2 device not detected" : error_message, status);
|
||||
return;
|
||||
}
|
||||
persist_sn(get_host_from_url(m_host), token, serial_number);
|
||||
persist_sn(Http::get_host_header_value(m_host), token, serial_number);
|
||||
res = true;
|
||||
})
|
||||
#ifdef WIN32
|
||||
@@ -574,7 +508,6 @@ namespace Slic3r {
|
||||
// Msg contains ip string.
|
||||
auto url = substitute_host(make_url(""), GUI::into_u8(msg));
|
||||
msg.Clear();
|
||||
std::string host = get_host_from_url(m_host);
|
||||
auto http = Http::get(url); // std::move(url));
|
||||
// "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable.
|
||||
// And when creating Http object above, libcurl automatically includes "Host" header from address it got.
|
||||
@@ -582,7 +515,7 @@ namespace Slic3r {
|
||||
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse
|
||||
// proxy is used (issue #9734). Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
|
||||
http.header("Host", host);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
set_auth(http);
|
||||
http.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version at %2% : %3%, HTTP %4%, body: `%5%`") % name % url %
|
||||
@@ -626,11 +559,10 @@ namespace Slic3r {
|
||||
bool res = true;
|
||||
const auto token = cc2_token();
|
||||
auto url = substitute_host(make_cc2_info_url(), GUI::into_u8(msg));
|
||||
std::string host_header = get_host_from_url(m_host);
|
||||
auto http = Http::get(url);
|
||||
msg.Clear();
|
||||
|
||||
http.header("Host", host_header);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
http.header("X-Token", token);
|
||||
http.header("Accept", "application/json");
|
||||
http.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
@@ -689,7 +621,7 @@ namespace Slic3r {
|
||||
|
||||
std::string url = substitute_host(make_cc2_upload_url(), resolved_addr.to_string());
|
||||
info_fn(L"resolve", boost::nowide::widen(url));
|
||||
return loopUploadCC2(url, get_host_from_url(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn);
|
||||
return loopUploadCC2(url, Http::get_host_header_value(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn);
|
||||
}
|
||||
|
||||
wxString legacy_msg = GUI::from_u8(resolved_addr.to_string());
|
||||
@@ -735,7 +667,7 @@ namespace Slic3r {
|
||||
}
|
||||
#endif // _WIN32
|
||||
|
||||
return loopUploadCC2(url, get_host_from_url(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn);
|
||||
return loopUploadCC2(url, Http::get_host_header_value(m_host), std::move(upload_data), prorgess_fn, error_fn, info_fn);
|
||||
}
|
||||
|
||||
wxString legacy_msg;
|
||||
@@ -874,8 +806,7 @@ namespace Slic3r {
|
||||
// on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734). Also
|
||||
// when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
|
||||
std::string host = get_host_from_url(m_host);
|
||||
http.header("Host", host);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
http.header("Accept", "application/json, text/plain, */*");
|
||||
#endif // _WIN32
|
||||
set_auth(http);
|
||||
@@ -906,7 +837,7 @@ namespace Slic3r {
|
||||
if (res) {
|
||||
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) {
|
||||
// connect to websocket, since the upload is successful, the file will be printed
|
||||
std::string wsUrl = get_host_from_url_no_port(m_host);
|
||||
std::string wsUrl = Http::get_host_from_url(m_host);
|
||||
WebSocketClient client;
|
||||
try {
|
||||
client.connect(wsUrl, "3030", "/websocket");
|
||||
@@ -1087,7 +1018,7 @@ namespace Slic3r {
|
||||
#ifndef WIN32
|
||||
return upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn);
|
||||
#else
|
||||
std::string host = get_host_from_url(m_host);
|
||||
std::string host = Http::get_host_from_url(m_host);
|
||||
|
||||
// decide what to do based on m_host - resolve hostname or upload to ip
|
||||
std::vector<boost::asio::ip::address> resolved_addr;
|
||||
|
||||
@@ -978,6 +978,51 @@ std::string Http::get_filename_from_url(const std::string &url)
|
||||
return path_url.substr(start_pos + 1, path_url.length() - start_pos - 1);
|
||||
}
|
||||
|
||||
std::string Http::get_host_from_url(const std::string &url_in, std::string *port)
|
||||
{
|
||||
std::string url = url_in;
|
||||
if (url.find("//") == std::string::npos)
|
||||
url = "http://" + url;
|
||||
|
||||
if (port)
|
||||
port->clear();
|
||||
std::string out = url_in;
|
||||
CURLU *hurl = curl_url();
|
||||
if (hurl) {
|
||||
CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0);
|
||||
if (rc == CURLUE_OK) {
|
||||
char *host;
|
||||
rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0);
|
||||
if (rc == CURLUE_OK) {
|
||||
out = host;
|
||||
curl_free(host);
|
||||
if (port) {
|
||||
char *pstr;
|
||||
rc = curl_url_get(hurl, CURLUPART_PORT, &pstr, 0);
|
||||
if (rc == CURLUE_OK && pstr) {
|
||||
*port = pstr;
|
||||
curl_free(pstr);
|
||||
}
|
||||
}
|
||||
} else
|
||||
BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to get host from URL " << url;
|
||||
} else
|
||||
BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to parse URL " << url;
|
||||
curl_url_cleanup(hurl);
|
||||
} else
|
||||
BOOST_LOG_TRIVIAL(error) << "Http::get_host_from_url: failed to allocate curl_url";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Http::get_host_header_value(const std::string &url)
|
||||
{
|
||||
std::string port;
|
||||
std::string host = get_host_from_url(url, &port);
|
||||
if (!port.empty())
|
||||
host += ":" + port;
|
||||
return host;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream &os, const Http::Progress &progress)
|
||||
{
|
||||
os << "Http::Progress("
|
||||
|
||||
@@ -204,6 +204,8 @@ public:
|
||||
static std::string url_decode(const std::string &str);
|
||||
|
||||
static std::string get_filename_from_url(const std::string &url);
|
||||
static std::string get_host_from_url(const std::string &url, std::string *port = nullptr);
|
||||
static std::string get_host_header_value(const std::string &url);
|
||||
private:
|
||||
Http(const std::string &url);
|
||||
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
#include "Moonraker.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
#include "Http.hpp"
|
||||
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
Moonraker::Moonraker(DynamicPrintConfig *config)
|
||||
: m_host(config->opt_string("print_host"))
|
||||
, m_apikey(config->opt_string("printhost_apikey"))
|
||||
, m_cafile(config->opt_string("printhost_cafile"))
|
||||
, m_ssl_revoke_best_effort(config->opt_bool("printhost_ssl_ignore_revoke"))
|
||||
{}
|
||||
|
||||
const char* Moonraker::get_name() const { return "Moonraker"; }
|
||||
|
||||
wxString Moonraker::get_test_ok_msg() const
|
||||
{
|
||||
return _(L("Connection to Moonraker is working correctly."));
|
||||
}
|
||||
|
||||
wxString Moonraker::get_test_failed_msg(wxString &msg) const
|
||||
{
|
||||
return GUI::format_wxstr("%s: %s", _L("Could not connect to Moonraker"), msg);
|
||||
}
|
||||
|
||||
std::string Moonraker::make_url(const std::string &path) const
|
||||
{
|
||||
if (m_host.find("http://") == 0 || m_host.find("https://") == 0) {
|
||||
if (m_host.back() == '/')
|
||||
return (boost::format("%1%%2%") % m_host % path).str();
|
||||
return (boost::format("%1%/%2%") % m_host % path).str();
|
||||
}
|
||||
return (boost::format("http://%1%/%2%") % m_host % path).str();
|
||||
}
|
||||
|
||||
void Moonraker::set_auth(Http &http) const
|
||||
{
|
||||
//ORCA: Moonraker accepts unauthenticated requests by default; X-Api-Key is the only auth header
|
||||
// defined by the Moonraker spec. HTTP Basic / Digest do NOT belong here even if the user
|
||||
// filled the user/password fields — those are PrusaLink/OctoPrint conventions.
|
||||
if (!m_apikey.empty())
|
||||
http.header("X-Api-Key", m_apikey);
|
||||
if (!m_cafile.empty())
|
||||
http.ca_file(m_cafile);
|
||||
}
|
||||
|
||||
bool Moonraker::test(wxString &msg) const
|
||||
{
|
||||
//ORCA: Moonraker's /server/info returns
|
||||
// { "result": { "klippy_state": "ready|startup|shutdown|error|disconnected", ... } }
|
||||
// We treat the connection as healthy as long as the envelope is valid and `klippy_state`
|
||||
// is present — matching the OctoPrint/PrusaLink convention of "can I reach this host?".
|
||||
// Klipper state (idle, error, etc.) is surfaced to the log but does not gate the test:
|
||||
// buddy-fork firmwares legitimately report non-`ready` states at idle, and any real upload
|
||||
// problem will surface a contextual error at upload() time anyway.
|
||||
const char *name = get_name();
|
||||
bool res = true;
|
||||
auto url = make_url("server/info");
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get server info at: %2%") % name % url;
|
||||
|
||||
auto http = Http::get(std::move(url));
|
||||
set_auth(http);
|
||||
http.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting server info: %2%, HTTP %3%, body: `%4%`")
|
||||
% name % error % status % body;
|
||||
res = false;
|
||||
msg = format_error(body, error, status);
|
||||
})
|
||||
.on_complete([&, this](std::string body, unsigned) {
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: /server/info body: %2%") % name % body;
|
||||
try {
|
||||
std::stringstream ss(body);
|
||||
pt::ptree ptree;
|
||||
pt::read_json(ss, ptree);
|
||||
|
||||
const auto klippy_state = ptree.get_optional<std::string>("result.klippy_state");
|
||||
if (!klippy_state) {
|
||||
//ORCA: response wasn't shaped like a Moonraker /server/info reply — likely an OctoPrint
|
||||
// or PrusaLink host the user mis-selected as Moonraker, or a totally different
|
||||
// service. Treat as a connection failure with a clear hint.
|
||||
res = false;
|
||||
msg = _L("The host responded but it doesn't look like Moonraker (missing result.klippy_state).");
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: klippy_state = %2%") % name % (*klippy_state);
|
||||
} catch (const std::exception &ex) {
|
||||
res = false;
|
||||
msg = GUI::format_wxstr(_L("Could not parse Moonraker server response: %s"), ex.what());
|
||||
}
|
||||
})
|
||||
#ifdef WIN32
|
||||
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
||||
#endif
|
||||
.perform_sync();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool Moonraker::get_storage(wxArrayString &storage_path, wxArrayString &storage_name) const
|
||||
{
|
||||
//ORCA: GET /server/files/roots enumerates Moonraker's storage roots (default "gcodes" plus any
|
||||
// configured extras like "config", "logs", "timelapse"). Only roots with permissions
|
||||
// including "rw" or "rwd" can receive uploads; we filter to those so the UI dropdown only
|
||||
// offers usable destinations. The base class returns false (no per-host storage); returning
|
||||
// true here populates the storage picker in PrintHostDialogs's send-to-print dialog.
|
||||
// Failures (404 — older Moonraker, or a buddy-fork that doesn't implement the endpoint)
|
||||
// gracefully degrade to false so upload() falls back to the hardcoded "gcodes" default.
|
||||
const char *name = get_name();
|
||||
bool got_any = false;
|
||||
auto url = make_url("server/files/roots");
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Enumerating storage roots at: %2%") % name % url;
|
||||
|
||||
auto http = Http::get(std::move(url));
|
||||
set_auth(http);
|
||||
http.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
//ORCA: /server/files/roots is optional in the Moonraker spec and absent on older versions
|
||||
// and slimmer shims (e.g. Prusa-Firmware-Buddy 0.8.x prusalink-shim returns 501). A
|
||||
// missing endpoint here is benign — upload() silently falls back to the hardcoded
|
||||
// "gcodes" root — so don't pollute the log at warning level for it. Other HTTP
|
||||
// errors still warn.
|
||||
if (status == 404 || status == 501) {
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: /server/files/roots not implemented (HTTP %2%); upload() will fall back to the \"gcodes\" root.")
|
||||
% name % status;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Could not enumerate roots: %2%, HTTP %3%, body: `%4%`")
|
||||
% name % error % status % body;
|
||||
}
|
||||
})
|
||||
.on_complete([&, this](std::string body, unsigned) {
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: /server/files/roots body: %2%") % name % body;
|
||||
try {
|
||||
std::stringstream ss(body);
|
||||
pt::ptree ptree;
|
||||
pt::read_json(ss, ptree);
|
||||
const auto result_node = ptree.get_child_optional("result");
|
||||
if (!result_node)
|
||||
return;
|
||||
for (const auto &child : *result_node) {
|
||||
const std::string &root = child.second.get<std::string>("name", "");
|
||||
const std::string &perms = child.second.get<std::string>("permissions", "");
|
||||
if (root.empty() || perms.find('w') == std::string::npos)
|
||||
continue;
|
||||
storage_path.Add(wxString::FromUTF8(root));
|
||||
storage_name.Add(wxString::FromUTF8(root));
|
||||
got_any = true;
|
||||
}
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: Could not parse roots: %2%") % name % ex.what();
|
||||
}
|
||||
})
|
||||
#ifdef WIN32
|
||||
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
||||
#endif
|
||||
.perform_sync();
|
||||
|
||||
return got_any;
|
||||
}
|
||||
|
||||
bool Moonraker::start_print(wxString &error_msg, const std::string &filename) const
|
||||
{
|
||||
//ORCA: POST /printer/print/start with JSON body { "filename": "<name>.gcode" }.
|
||||
// `filename` is what /server/files/upload returned as result.item.path (the storage-relative
|
||||
// path inside `root`, no leading slash, with extension). Build the body via property_tree
|
||||
// so that special characters in the filename (server-side collision-suffix could produce
|
||||
// paths with quotes / backslashes on exotic file systems) are properly escaped.
|
||||
const char *name = get_name();
|
||||
bool res = true;
|
||||
auto url = make_url("printer/print/start");
|
||||
pt::ptree body_tree;
|
||||
body_tree.put("filename", filename);
|
||||
std::ostringstream body_ss;
|
||||
pt::write_json(body_ss, body_tree, /*pretty=*/false);
|
||||
std::string body = body_ss.str();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Starting print of %2% at %3%") % name % filename % url;
|
||||
|
||||
auto http = Http::post(std::move(url));
|
||||
set_auth(http);
|
||||
http.header("Content-Type", "application/json")
|
||||
.set_post_body(body)
|
||||
.on_complete([&](std::string body, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: print/start HTTP %2%: %3%") % name % status % body;
|
||||
})
|
||||
.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error starting print at %2%: %3%, HTTP %4%, body: `%5%`")
|
||||
% name % url % error % status % body;
|
||||
res = false;
|
||||
error_msg = format_error(body, error, status);
|
||||
})
|
||||
#ifdef WIN32
|
||||
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
||||
#endif
|
||||
.perform_sync();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool Moonraker::upload(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const
|
||||
{
|
||||
//ORCA: POST /server/files/upload as multipart/form-data with:
|
||||
// file = <gcode file>
|
||||
// root = <storage root> (Moonraker default: "gcodes")
|
||||
// Successful response shape:
|
||||
// { "result": { "item": { "path": "<name>.gcode", "root": "<root>" }, "print_started": <bool> } }
|
||||
// We always start the print explicitly via /printer/print/start regardless of `print_started`
|
||||
// so the user can rely on a single call site for state.
|
||||
wxString test_msg;
|
||||
if (!test(test_msg)) {
|
||||
error_fn(std::move(test_msg));
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *name = get_name();
|
||||
const auto upload_filename = upload_data.upload_path.filename();
|
||||
const auto upload_parent_path = upload_data.upload_path.parent_path();
|
||||
//ORCA: upload_data.storage is plumbed from the (future) per-printer storage dropdown. When unset,
|
||||
// fall back to the Moonraker-standard "gcodes" root. Reading it through here means a UI
|
||||
// addition later (storage picker) needs no change to this method.
|
||||
const std::string root = upload_data.storage.empty() ? std::string("gcodes") : upload_data.storage;
|
||||
|
||||
std::string url = make_url("server/files/upload");
|
||||
bool result = true;
|
||||
std::string uploaded_path;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% to %3% (root=%4%, filename=%5%, start_print=%6%)")
|
||||
% name
|
||||
% upload_data.source_path
|
||||
% url
|
||||
% root
|
||||
% upload_filename.string()
|
||||
% (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false");
|
||||
|
||||
auto http = Http::post(std::move(url));
|
||||
set_auth(http);
|
||||
http.form_add("root", root)
|
||||
.form_add_file("file", upload_data.source_path.string(), upload_filename.string())
|
||||
.on_complete([&](std::string body, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: upload HTTP %2%: %3%") % name % status % body;
|
||||
try {
|
||||
std::stringstream ss(body);
|
||||
pt::ptree ptree;
|
||||
pt::read_json(ss, ptree);
|
||||
|
||||
//ORCA: Moonraker confirms the storage-relative path in result.item.path. We pass exactly
|
||||
// that string to /printer/print/start so any server-side renaming (collision suffix,
|
||||
// etc.) is respected.
|
||||
const auto stored_path = ptree.get_optional<std::string>("result.item.path");
|
||||
if (stored_path) {
|
||||
uploaded_path = *stored_path;
|
||||
} else {
|
||||
//ORCA: fallback if the server response omits result.item.path (older Moonraker, or
|
||||
// a buddy-fork that returns a slimmer envelope). Use the original filename.
|
||||
uploaded_path = upload_filename.string();
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format(
|
||||
"%1%: upload response missing result.item.path, falling back to original filename `%2%`")
|
||||
% name % uploaded_path;
|
||||
}
|
||||
} catch (const std::exception &ex) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format(
|
||||
"%1%: could not parse upload response (%2%); falling back to original filename")
|
||||
% name % ex.what();
|
||||
uploaded_path = upload_filename.string();
|
||||
}
|
||||
})
|
||||
.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading to %2%: %3%, HTTP %4%, body: `%5%`")
|
||||
% name % url % error % status % body;
|
||||
error_fn(format_error(body, error, status));
|
||||
result = false;
|
||||
})
|
||||
.on_progress([&](Http::Progress progress, bool &cancel) {
|
||||
progress_fn(std::move(progress), cancel);
|
||||
if (cancel) {
|
||||
BOOST_LOG_TRIVIAL(info) << name << ": Upload canceled";
|
||||
result = false;
|
||||
}
|
||||
})
|
||||
#ifdef WIN32
|
||||
.ssl_revoke_best_effort(m_ssl_revoke_best_effort)
|
||||
#endif
|
||||
.perform_sync();
|
||||
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint && !uploaded_path.empty()) {
|
||||
wxString start_msg;
|
||||
if (!start_print(start_msg, uploaded_path)) {
|
||||
error_fn(std::move(start_msg));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef slic3r_Moonraker_hpp_
|
||||
#define slic3r_Moonraker_hpp_
|
||||
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
#include <wx/arrstr.h>
|
||||
|
||||
#include "PrintHost.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class DynamicPrintConfig;
|
||||
class Http;
|
||||
|
||||
// Moonraker is the JSON / WebSocket gateway that ships in front of Klipper
|
||||
// (and on Klipper-API-compatible firmwares like the Prusa-Firmware-Buddy
|
||||
// Buddy-Klipper fork). REST shape differs from OctoPrint: distinct paths,
|
||||
// JSON body for print/start, {"result":...}/{"error":...} envelope.
|
||||
//
|
||||
// Endpoints used:
|
||||
// GET /server/info -- connection test, reads klippy_state
|
||||
// POST /server/files/upload (multipart) -- upload gcode (form fields: file, root)
|
||||
// POST /printer/print/start (json) -- {"filename":"<name>.gcode"} starts print
|
||||
//
|
||||
// Auth: X-Api-Key header if `printhost_apikey` is non-empty; Moonraker accepts
|
||||
// unauthenticated LAN access by default, so the key is optional. HTTP Basic /
|
||||
// Digest are not part of the Moonraker spec and are not sent.
|
||||
class Moonraker : public PrintHost
|
||||
{
|
||||
public:
|
||||
Moonraker(DynamicPrintConfig *config);
|
||||
~Moonraker() override = default;
|
||||
|
||||
const char* get_name() const override;
|
||||
|
||||
bool test(wxString &curl_msg) const override;
|
||||
wxString get_test_ok_msg() const override;
|
||||
wxString get_test_failed_msg(wxString &msg) const override;
|
||||
bool upload(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const override;
|
||||
bool has_auto_discovery() const override { return false; }
|
||||
bool can_test() const override { return true; }
|
||||
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
|
||||
std::string get_host() const override { return m_host; }
|
||||
bool get_storage(wxArrayString &storage_path, wxArrayString &storage_name) const override;
|
||||
const std::string& get_apikey() const { return m_apikey; }
|
||||
const std::string& get_cafile() const { return m_cafile; }
|
||||
|
||||
protected:
|
||||
std::string m_host;
|
||||
std::string m_apikey;
|
||||
std::string m_cafile;
|
||||
bool m_ssl_revoke_best_effort;
|
||||
|
||||
void set_auth(Http &http) const;
|
||||
std::string make_url(const std::string &path) const;
|
||||
bool start_print(wxString &error_msg, const std::string &filename) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "QidiPrinterAgent.hpp"
|
||||
#include "SnapmakerPrinterAgent.hpp"
|
||||
#include "MoonrakerPrinterAgent.hpp"
|
||||
#include "CrealityPrintAgent.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
@@ -133,6 +134,9 @@ void NetworkAgentFactory::register_all_agents()
|
||||
register_agent<OrcaPrinterAgent>();
|
||||
register_agent<QidiPrinterAgent>();
|
||||
register_agent<SnapmakerPrinterAgent>();
|
||||
register_agent<CrealityPrintAgent>(); // Must come BEFORE MoonrakerPrinterAgent —
|
||||
// CrealityPrintAgent extends Moonraker behaviour
|
||||
// for K-series boards with CFS support.
|
||||
register_agent<MoonrakerPrinterAgent>();
|
||||
|
||||
// BBLPrinterAgent takes no constructor args, so register manually
|
||||
|
||||
@@ -33,45 +33,6 @@ namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
#ifdef WIN32
|
||||
std::string get_host_from_url(const std::string& url_in)
|
||||
{
|
||||
std::string url = url_in;
|
||||
// add http:// if there is no scheme
|
||||
size_t double_slash = url.find("//");
|
||||
if (double_slash == std::string::npos)
|
||||
url = "http://" + url;
|
||||
std::string out = url;
|
||||
CURLU* hurl = curl_url();
|
||||
if (hurl) {
|
||||
// Parse the input URL.
|
||||
CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, url.c_str(), 0);
|
||||
if (rc == CURLUE_OK) {
|
||||
// Replace the address.
|
||||
char* host;
|
||||
rc = curl_url_get(hurl, CURLUPART_HOST, &host, 0);
|
||||
if (rc == CURLUE_OK) {
|
||||
char* port;
|
||||
rc = curl_url_get(hurl, CURLUPART_PORT, &port, 0);
|
||||
if (rc == CURLUE_OK && port != nullptr) {
|
||||
out = std::string(host) + ":" + port;
|
||||
curl_free(port);
|
||||
} else {
|
||||
out = host;
|
||||
curl_free(host);
|
||||
}
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to get host form URL " << url;
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to parse URL " << url;
|
||||
curl_url_cleanup(hurl);
|
||||
}
|
||||
else
|
||||
BOOST_LOG_TRIVIAL(error) << "OctoPrint get_host_from_url: failed to allocate curl_url";
|
||||
return out;
|
||||
}
|
||||
|
||||
// Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail.
|
||||
std::string substitute_host(const std::string& orig_addr, std::string sub_addr)
|
||||
{
|
||||
@@ -186,7 +147,6 @@ bool OctoPrint::test_with_resolved_ip(wxString &msg) const
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url;
|
||||
|
||||
std::string host = get_host_from_url(m_host);
|
||||
auto http = Http::get(url);//std::move(url));
|
||||
// "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable.
|
||||
// And when creating Http object above, libcurl automatically includes "Host" header from address it got.
|
||||
@@ -194,7 +154,7 @@ bool OctoPrint::test_with_resolved_ip(wxString &msg) const
|
||||
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
|
||||
// Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
|
||||
http.header("Host", host);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
set_auth(http);
|
||||
http
|
||||
.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
@@ -306,7 +266,7 @@ bool OctoPrint::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Erro
|
||||
#ifndef WIN32
|
||||
return upload_inner_with_host(std::move(upload_data), prorgess_fn, error_fn, info_fn);
|
||||
#else
|
||||
std::string host = get_host_from_url(m_host);
|
||||
std::string host = Http::get_host_from_url(m_host);
|
||||
|
||||
// decide what to do based on m_host - resolve hostname or upload to ip
|
||||
std::vector<boost::asio::ip::address> resolved_addr;
|
||||
@@ -393,14 +353,13 @@ bool OctoPrint::upload_inner_with_resolved_ip(PrintHostUpload upload_data, Progr
|
||||
% upload_parent_path.string()
|
||||
% (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false");
|
||||
|
||||
std::string host = get_host_from_url(m_host);
|
||||
auto http = Http::post(url);//std::move(url));
|
||||
// "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable.
|
||||
// And when creating Http object above, libcurl automatically includes "Host" header from address it got.
|
||||
// Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back.
|
||||
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
|
||||
http.header("Host", host);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
set_auth(http);
|
||||
http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false")
|
||||
.form_add("path", upload_parent_path.string()) // XXX: slashes on windows ???
|
||||
@@ -486,8 +445,7 @@ bool OctoPrint::upload_inner_with_host(PrintHostUpload upload_data, ProgressFn p
|
||||
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
|
||||
// Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
|
||||
std::string host = get_host_from_url(m_host);
|
||||
http.header("Host", host);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
#endif // _WIN32
|
||||
set_auth(http);
|
||||
http.form_add("print", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false")
|
||||
@@ -884,7 +842,6 @@ bool PrusaLink::test_with_resolved_ip_and_method_check(wxString& msg, bool& use_
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url;
|
||||
|
||||
std::string host = get_host_from_url(m_host);
|
||||
auto http = Http::get(url);//std::move(url));
|
||||
// "Host" header is necessary here. We have resolved IP address and subsituted it into "url" variable.
|
||||
// And when creating Http object above, libcurl automatically includes "Host" header from address it got.
|
||||
@@ -892,7 +849,7 @@ bool PrusaLink::test_with_resolved_ip_and_method_check(wxString& msg, bool& use_
|
||||
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
|
||||
// Also when allow_ip_resolve = 0, this is not needed, but it should not break anything if it stays.
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
|
||||
http.header("Host", host);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
set_auth(http);
|
||||
http
|
||||
.on_error([&](std::string body, std::string error, unsigned status) {
|
||||
@@ -1053,8 +1010,7 @@ bool PrusaLink::put_inner(PrintHostUpload upload_data, std::string url, const st
|
||||
// Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back.
|
||||
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
|
||||
std::string host = get_host_from_url(m_host);
|
||||
http.header("Host", host);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
#endif // _WIN32
|
||||
set_auth(http);
|
||||
// This is ugly, but works. There was an error at PrusaLink side that accepts any string at Print-After-Upload as true, thus False was also triggering print after upload.
|
||||
@@ -1103,8 +1059,7 @@ bool PrusaLink::post_inner(PrintHostUpload upload_data, std::string url, const s
|
||||
// Thus "Host" is set to the resolved IP instead of host filled by user. We need to change it back.
|
||||
// Not changing the host would work on the most cases (where there is 1 service on 1 hostname) but would break when f.e. reverse proxy is used (issue #9734).
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-5.4
|
||||
std::string host = get_host_from_url(m_host);
|
||||
http.header("Host", host);
|
||||
http.header("Host", Http::get_host_header_value(m_host));
|
||||
#endif // _WIN32
|
||||
set_auth(http);
|
||||
set_http_post_header_args(http, upload_data.post_action);
|
||||
|
||||
@@ -101,24 +101,6 @@ std::string resolve_display_name(
|
||||
return username;
|
||||
}
|
||||
|
||||
std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "")
|
||||
{
|
||||
if (name.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Mix user_id into the hashed input so two different users generating a setting_id
|
||||
// for an identically-named preset get distinct UUIDs. Without this, the cloud's ID
|
||||
// space collides across accounts and the second user's create gets HTTP 409 with
|
||||
// server_profile=null on every sync (the foreign owner's record is not exposed).
|
||||
static const boost::uuids::uuid orca_namespace =
|
||||
boost::uuids::string_generator()("f47ac10b-58cc-4372-a567-0e02b2c3d479");
|
||||
|
||||
boost::uuids::name_generator_sha1 gen(orca_namespace);
|
||||
boost::uuids::uuid id = user_id.empty() ? gen(name) : gen(user_id + "/" + name);
|
||||
return boost::uuids::to_string(id);
|
||||
}
|
||||
|
||||
std::string base64url_encode(const std::vector<unsigned char>& data)
|
||||
{
|
||||
std::string out;
|
||||
@@ -412,6 +394,24 @@ OrcaCloudServiceAgent::~OrcaCloudServiceAgent()
|
||||
}
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::generate_uuid_for_setting_id(const std::string& name, const std::string& user_id)
|
||||
{
|
||||
if (name.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Mix user_id into the hashed input so two different users generating a setting_id
|
||||
// for an identically-named preset get distinct UUIDs. Without this, the cloud's ID
|
||||
// space collides across accounts and the second user's create gets HTTP 409 with
|
||||
// server_profile=null on every sync (the foreign owner's record is not exposed).
|
||||
static const boost::uuids::uuid orca_namespace =
|
||||
boost::uuids::string_generator()("f47ac10b-58cc-4372-a567-0e02b2c3d479");
|
||||
|
||||
boost::uuids::name_generator_sha1 gen(orca_namespace);
|
||||
boost::uuids::uuid id = user_id.empty() ? gen(name) : gen(user_id + "/" + name);
|
||||
return boost::uuids::to_string(id);
|
||||
}
|
||||
|
||||
void OrcaCloudServiceAgent::configure_urls(AppConfig* app_config)
|
||||
{
|
||||
if (!app_config) return;
|
||||
@@ -1250,8 +1250,10 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
|
||||
|
||||
if (http_code == 409) {
|
||||
// Conflict - parse server version
|
||||
nlohmann::json err_body;
|
||||
try {
|
||||
auto json = nlohmann::json::parse(response);
|
||||
err_body = json;
|
||||
if (json.is_null()) {
|
||||
result.server_deleted = true;
|
||||
} else {
|
||||
@@ -1261,6 +1263,13 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
|
||||
result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0);
|
||||
}
|
||||
} catch (...) {}
|
||||
// Surface the conflict via the http-error callback with the local preset name injected.
|
||||
// The raw server body omits the name for tombstone (-3) conflicts (server_profile is null),
|
||||
// but the GUI needs it to regenerate the deterministic setting_id for a force push.
|
||||
if (!err_body.is_object())
|
||||
err_body = nlohmann::json::object();
|
||||
err_body["name"] = name;
|
||||
invoke_http_error_callback(409, err_body.dump());
|
||||
result.error_message = response;
|
||||
return result;
|
||||
}
|
||||
@@ -1564,21 +1573,24 @@ bool OrcaCloudServiceAgent::decode_jwt_expiry(const std::string& token, std::chr
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const std::string& reason, bool async)
|
||||
RefreshResult OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const std::string& reason, bool async)
|
||||
{
|
||||
if (refresh_token.empty()) return false;
|
||||
if (refresh_token.empty()) return RefreshResult::AuthRejected; // nothing to refresh
|
||||
|
||||
bool expected = false;
|
||||
if (!refresh_running.compare_exchange_strong(expected, true)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: refresh already running, skip (reason=" << reason << ")";
|
||||
return false;
|
||||
// Another refresh is already in flight. Treat as transient so we keep the session
|
||||
// instead of logging out: that in-flight refresh surfaces its own Success/AuthRejected,
|
||||
// so a genuine rejection is only deferred to the next request, never lost.
|
||||
return RefreshResult::Transient;
|
||||
}
|
||||
|
||||
auto worker = [this, refresh_token, reason]() {
|
||||
(void) reason;
|
||||
bool ok = refresh_session_with_token(refresh_token);
|
||||
RefreshResult r = refresh_session_with_token(refresh_token);
|
||||
refresh_running.store(false);
|
||||
return ok;
|
||||
return r;
|
||||
};
|
||||
|
||||
if (async) {
|
||||
@@ -1586,13 +1598,15 @@ bool OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const
|
||||
refresh_thread.join();
|
||||
}
|
||||
refresh_thread = std::thread([worker]() { worker(); });
|
||||
return true;
|
||||
// Fire-and-forget: the outcome isn't known yet and no current caller consumes it.
|
||||
// Return Transient (indeterminate) rather than implying a completed, successful refresh.
|
||||
return RefreshResult::Transient;
|
||||
}
|
||||
|
||||
return worker();
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool async)
|
||||
RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool async)
|
||||
{
|
||||
std::string refresh_token = get_refresh_token();
|
||||
if (refresh_token.empty()) {
|
||||
@@ -1600,7 +1614,7 @@ bool OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool
|
||||
}
|
||||
if (refresh_token.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh token available for refresh (reason=" << reason << ")";
|
||||
return false;
|
||||
return RefreshResult::AuthRejected; // no persisted token: nothing to preserve
|
||||
}
|
||||
|
||||
return refresh_now(refresh_token, reason, async);
|
||||
@@ -1616,37 +1630,55 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const
|
||||
|
||||
if (!needs_refresh) return true;
|
||||
|
||||
if (refresh_from_storage(reason, false)) return true;
|
||||
if (refresh_from_storage(reason, false) == RefreshResult::Success) return true;
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(750));
|
||||
return refresh_from_storage(reason + "_retry", false);
|
||||
return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success;
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token)
|
||||
// Maps a token-refresh HTTP outcome to a RefreshResult. http_code == 0 means the
|
||||
// server could not be reached; session_established is only meaningful for a 2xx body.
|
||||
static RefreshResult classify_refresh_result(unsigned http_code, bool session_established)
|
||||
{
|
||||
if (http_code == 0)
|
||||
return RefreshResult::Transient; // no response: network/transport failure
|
||||
if (http_code == 400 || http_code == 401 || http_code == 403)
|
||||
return RefreshResult::AuthRejected; // refresh token rejected
|
||||
if (http_code >= 400)
|
||||
return RefreshResult::Transient; // rate-limit (429), server error (5xx) or other 4xx: keep the session
|
||||
return session_established ? RefreshResult::Success // 2xx with a usable session
|
||||
: RefreshResult::Transient; // 2xx but unusable body
|
||||
}
|
||||
|
||||
RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token)
|
||||
{
|
||||
std::string body = "{\"refresh_token\":\"" + refresh_token + "\"}";
|
||||
std::string url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=refresh_token";
|
||||
std::string response;
|
||||
unsigned int http_code = 0;
|
||||
if (!http_post_token(body, &response, &http_code, url) || http_code >= 400) {
|
||||
// http_post_token sets http_code to 0 when the server could not be reached.
|
||||
http_post_token(body, &response, &http_code, url);
|
||||
|
||||
bool established = false;
|
||||
if (http_code >= 200 && http_code < 300) {
|
||||
if (session_handler) {
|
||||
established = session_handler(response);
|
||||
} else {
|
||||
// No session handler set - parse the token response directly and establish the
|
||||
// session, so OrcaCloudServiceAgent is self-contained without external setup.
|
||||
try {
|
||||
established = set_user_session(json::parse(response));
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh parse exception - " << e.what();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::string truncated_response = response.size() > 200 ? response.substr(0, 200) + "..." : response;
|
||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh failed - http_code=" << http_code
|
||||
<< ", response_body=" << truncated_response;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (session_handler) {
|
||||
return session_handler(response);
|
||||
}
|
||||
|
||||
// No session handler set - parse the token response directly and establish session
|
||||
// This makes OrcaCloudServiceAgent self-contained without requiring external setup
|
||||
try {
|
||||
return set_user_session(json::parse(response));
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh parse exception - " << e.what();
|
||||
return false;
|
||||
}
|
||||
return classify_refresh_result(http_code, established);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1759,15 +1791,20 @@ void OrcaCloudServiceAgent::clear_session()
|
||||
// HTTP Helpers
|
||||
// ============================================================================
|
||||
|
||||
bool OrcaCloudServiceAgent::attempt_refresh_after_unauthorized(const std::string& reason)
|
||||
RefreshResult OrcaCloudServiceAgent::attempt_refresh_after_unauthorized(const std::string& reason)
|
||||
{
|
||||
if (refresh_from_storage(reason, false)) return true;
|
||||
RefreshResult r = refresh_from_storage(reason, false);
|
||||
if (r != RefreshResult::Transient) return r; // Success or AuthRejected: decided, no retry
|
||||
|
||||
// Only a transient (network/server) failure is worth retrying.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
if (refresh_from_storage(reason + "_retry", false)) return true;
|
||||
r = refresh_from_storage(reason + "_retry", false);
|
||||
|
||||
BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=failure source=" << reason << " action=logout";
|
||||
return false;
|
||||
if (r == RefreshResult::Transient)
|
||||
BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=transient source=" << reason << " action=keep_session";
|
||||
else if (r == RefreshResult::AuthRejected)
|
||||
BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=rejected source=" << reason << " action=logout";
|
||||
return r;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> OrcaCloudServiceAgent::data_headers()
|
||||
@@ -1780,6 +1817,24 @@ std::map<std::string, std::string> OrcaCloudServiceAgent::data_headers()
|
||||
return headers;
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::resolve_unauthorized(HttpResult& res,
|
||||
const std::function<HttpResult()>& perform, const std::string& reason)
|
||||
{
|
||||
if (res.status != 401)
|
||||
return false;
|
||||
|
||||
RefreshResult rr = attempt_refresh_after_unauthorized(reason);
|
||||
if (rr == RefreshResult::Success) {
|
||||
res = perform(); // refreshed: retry the original request with the new token
|
||||
return false;
|
||||
}
|
||||
|
||||
// Transient (no connection / 5xx / 429 / ambiguous): keep the session and token,
|
||||
// suppress the auth error so the GUI does not log the user out.
|
||||
// AuthRejected (refresh token genuinely rejected): let the 401 surface -> logout.
|
||||
return rr == RefreshResult::Transient;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* response_body, unsigned int* http_code)
|
||||
{
|
||||
std::string url = api_base_url + path;
|
||||
@@ -1788,12 +1843,6 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
|
||||
if (!ensure_token_fresh("http_get_" + path))
|
||||
BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false";
|
||||
|
||||
struct HttpResult {
|
||||
bool success{false};
|
||||
unsigned int status{0};
|
||||
std::string body;
|
||||
};
|
||||
|
||||
auto perform = [&]() {
|
||||
HttpResult result;
|
||||
try {
|
||||
@@ -1832,20 +1881,16 @@ int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* respon
|
||||
};
|
||||
|
||||
HttpResult res = perform();
|
||||
|
||||
// Single retry on 401 - no recursion
|
||||
if (res.status == 401 && attempt_refresh_after_unauthorized("http_get_" + path)) {
|
||||
res = perform();
|
||||
}
|
||||
bool suppress = resolve_unauthorized(res, perform, "http_get_" + path);
|
||||
|
||||
if (response_body) *response_body = res.body;
|
||||
if (http_code) *http_code = res.status;
|
||||
|
||||
if (!res.success || res.status >= 400) {
|
||||
if (!suppress && (!res.success || res.status >= 400)) {
|
||||
invoke_http_error_callback(res.status, res.body);
|
||||
}
|
||||
|
||||
return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code)
|
||||
@@ -1855,12 +1900,6 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
|
||||
|
||||
ensure_token_fresh("http_post_" + path);
|
||||
|
||||
struct HttpResult {
|
||||
bool success{false};
|
||||
unsigned int status{0};
|
||||
std::string body;
|
||||
};
|
||||
|
||||
auto perform = [&]() {
|
||||
HttpResult result;
|
||||
try {
|
||||
@@ -1902,20 +1941,19 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
|
||||
};
|
||||
|
||||
HttpResult res = perform();
|
||||
|
||||
// Single retry on 401 - no recursion
|
||||
if (res.status == 401 && attempt_refresh_after_unauthorized("http_post_" + path)) {
|
||||
res = perform();
|
||||
}
|
||||
bool suppress = resolve_unauthorized(res, perform, "http_post_" + path);
|
||||
|
||||
if (response_body) *response_body = res.body;
|
||||
if (http_code) *http_code = res.status;
|
||||
|
||||
if (!res.success || res.status >= 400) {
|
||||
// 409 is a push-only domain conflict; sync_push re-fires the error callback with the
|
||||
// local preset name injected (the raw server body omits it for tombstone conflicts),
|
||||
// so skip the generic nameless auto-fire here to avoid a duplicate, nameless event.
|
||||
if (!suppress && (!res.success || res.status >= 400) && res.status != 409) {
|
||||
invoke_http_error_callback(res.status, res.body);
|
||||
}
|
||||
|
||||
return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code)
|
||||
@@ -1925,12 +1963,6 @@ int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string&
|
||||
|
||||
ensure_token_fresh("http_put_" + path);
|
||||
|
||||
struct HttpResult {
|
||||
bool success{false};
|
||||
unsigned int status{0};
|
||||
std::string body;
|
||||
};
|
||||
|
||||
auto perform = [&]() {
|
||||
HttpResult result;
|
||||
try {
|
||||
@@ -1959,7 +1991,7 @@ int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string&
|
||||
.on_error([&](std::string resp_body, std::string error, unsigned resp_status) {
|
||||
result.success = false;
|
||||
result.status = resp_status == 0 ? 404 : resp_status;
|
||||
result.body = body;
|
||||
result.body = resp_body;
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error;
|
||||
})
|
||||
.timeout_max(30)
|
||||
@@ -1972,20 +2004,16 @@ int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string&
|
||||
};
|
||||
|
||||
HttpResult res = perform();
|
||||
|
||||
// Single retry on 401 - no recursion
|
||||
if (res.status == 401 && attempt_refresh_after_unauthorized("http_put_" + path)) {
|
||||
res = perform();
|
||||
}
|
||||
bool suppress = resolve_unauthorized(res, perform, "http_put_" + path);
|
||||
|
||||
if (response_body) *response_body = res.body;
|
||||
if (http_code) *http_code = res.status;
|
||||
|
||||
if (!res.success || res.status >= 400) {
|
||||
if (!suppress && (!res.success || res.status >= 400)) {
|
||||
invoke_http_error_callback(res.status, res.body);
|
||||
}
|
||||
|
||||
return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* response_body, unsigned int* http_code)
|
||||
@@ -1995,12 +2023,6 @@ int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* res
|
||||
|
||||
ensure_token_fresh("http_delete_" + path);
|
||||
|
||||
struct HttpResult {
|
||||
bool success{false};
|
||||
unsigned int status{0};
|
||||
std::string body;
|
||||
};
|
||||
|
||||
auto perform = [&]() {
|
||||
HttpResult result;
|
||||
try {
|
||||
@@ -2039,20 +2061,16 @@ int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* res
|
||||
};
|
||||
|
||||
HttpResult res = perform();
|
||||
|
||||
// Single retry on 401 - no recursion
|
||||
if (res.status == 401 && attempt_refresh_after_unauthorized("http_delete_" + path)) {
|
||||
res = perform();
|
||||
}
|
||||
bool suppress = resolve_unauthorized(res, perform, "http_delete_" + path);
|
||||
|
||||
if (response_body) *response_body = res.body;
|
||||
if (http_code) *http_code = res.status;
|
||||
|
||||
if (!res.success || res.status >= 400) {
|
||||
if (!suppress && (!res.success || res.status >= 400)) {
|
||||
invoke_http_error_callback(res.status, res.body);
|
||||
}
|
||||
|
||||
return res.success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
bool OrcaCloudServiceAgent::http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& custom_url)
|
||||
@@ -2108,7 +2126,7 @@ bool OrcaCloudServiceAgent::http_post_token(const std::string& body, std::string
|
||||
})
|
||||
.on_error([&](std::string body, std::string error, unsigned resp_status) {
|
||||
success = false;
|
||||
status = resp_status == 0 ? 404 : resp_status;
|
||||
status = resp_status; // keep 0 for "no response" so the refresh classifier sees a transport failure
|
||||
resp_body = body;
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error;
|
||||
})
|
||||
|
||||
@@ -20,6 +20,14 @@ namespace Slic3r {
|
||||
class AppConfig;
|
||||
struct BundleMetadata;
|
||||
|
||||
// Outcome of a token-refresh attempt: decides whether a 401 should log the user
|
||||
// out (AuthRejected) or be treated as a recoverable condition (Transient).
|
||||
enum class RefreshResult {
|
||||
Success, // new tokens obtained
|
||||
AuthRejected, // server definitively rejected the refresh token -> logout is correct
|
||||
Transient // network/server problem -> keep the session and retry later
|
||||
};
|
||||
|
||||
// Constants for OAuth loopback server
|
||||
namespace auth_constants {
|
||||
constexpr int LOOPBACK_PORT = 41172;
|
||||
@@ -276,10 +284,10 @@ public:
|
||||
void clear_refresh_token();
|
||||
|
||||
// Token refresh helpers
|
||||
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
|
||||
bool refresh_from_storage(const std::string& reason, bool async = false);
|
||||
bool refresh_now(const std::string& refresh_token, const std::string& reason, bool async = false);
|
||||
bool refresh_session_with_token(const std::string& refresh_token);
|
||||
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
|
||||
RefreshResult refresh_from_storage(const std::string& reason, bool async = false);
|
||||
RefreshResult refresh_now(const std::string& refresh_token, const std::string& reason, bool async = false);
|
||||
RefreshResult refresh_session_with_token(const std::string& refresh_token);
|
||||
|
||||
// Session state helpers. nickname is the human-facing UI label after provider fallback resolution.
|
||||
bool set_user_session(const std::string& token,
|
||||
@@ -292,6 +300,8 @@ public:
|
||||
bool set_user_session(const nlohmann::json& session_json, bool notify_login = true);
|
||||
void clear_session();
|
||||
|
||||
static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "");
|
||||
|
||||
private:
|
||||
// Sync protocol helpers
|
||||
int sync_pull(
|
||||
@@ -299,13 +309,28 @@ private:
|
||||
std::function<void(int http_code, const std::string& error)> on_error
|
||||
);
|
||||
|
||||
// Shared result of one HTTP attempt by the data methods (get/post/put/delete).
|
||||
struct HttpResult {
|
||||
bool success{false};
|
||||
unsigned int status{0};
|
||||
std::string body;
|
||||
};
|
||||
|
||||
// Applies the "retry once on 401" policy for the data HTTP methods.
|
||||
// `res` holds the first response; `perform` re-issues the request after a
|
||||
// successful refresh. Returns true if the auth error should be SUPPRESSED
|
||||
// (i.e. the session must be kept rather than logged out).
|
||||
bool resolve_unauthorized(HttpResult& res,
|
||||
const std::function<HttpResult()>& perform,
|
||||
const std::string& reason);
|
||||
|
||||
// HTTP request helpers
|
||||
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
|
||||
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
|
||||
int http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);
|
||||
int http_delete(const std::string& path, std::string* response_body, unsigned int* http_code);
|
||||
std::map<std::string, std::string> data_headers();
|
||||
bool attempt_refresh_after_unauthorized(const std::string& reason);
|
||||
RefreshResult attempt_refresh_after_unauthorized(const std::string& reason);
|
||||
|
||||
// Auth HTTP helpers
|
||||
bool http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& url = "");
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "SimplyPrint.hpp"
|
||||
#include "ElegooLink.hpp"
|
||||
#include "3DPrinterOS.hpp"
|
||||
#include "Moonraker.hpp"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
using boost::optional;
|
||||
@@ -69,6 +70,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config)
|
||||
case htSimplyPrint: return new SimplyPrint(config);
|
||||
case htElegooLink: return new ElegooLink(config);
|
||||
case ht3DPrinterOS: return new C3DPrinterOS(config);
|
||||
case htMoonraker: return new Moonraker(config);
|
||||
default: return nullptr;
|
||||
}
|
||||
} else {
|
||||
@@ -90,6 +92,10 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
|
||||
webui_url = ElegooLink::get_print_host_webui(config);
|
||||
break;
|
||||
}
|
||||
case htCrealityPrint: {
|
||||
webui_url = CrealityPrint::get_print_host_webui(config);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user