From dfd3444ae7d713b0f1ebfdbb241fbeeb4b7b0d84 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 19 Aug 2026 19:20:31 +0800 Subject: [PATCH 01/19] feat: initial draft of lifecycle events API for plugins --- src/libslic3r/GCode.cpp | 49 ++++++- src/libslic3r/LifecycleEvents.hpp | 135 ++++++++++++++++++++ src/libslic3r/Preset.cpp | 10 ++ src/libslic3r/Print.cpp | 38 +++++- src/slic3r/GUI/DeviceCore/DevManager.cpp | 27 +++- src/slic3r/GUI/DeviceManager.cpp | 28 +++- src/slic3r/GUI/GLCanvas3D.cpp | 31 ++++- src/slic3r/GUI/GUI_App.cpp | 22 ++++ src/slic3r/GUI/GUI_ObjectList.cpp | 15 +++ src/slic3r/GUI/Jobs/ArrangeJob.cpp | 8 ++ src/slic3r/GUI/Jobs/FillBedJob.cpp | 8 ++ src/slic3r/GUI/Jobs/OrientJob.cpp | 7 + src/slic3r/GUI/Plater.cpp | 58 +++++++++ src/slic3r/GUI/Tab.cpp | 15 +++ src/slic3r/Utils/PrintHost.cpp | 18 +++ src/slic3r/plugin/PluginHooks.cpp | 13 ++ src/slic3r/plugin/PluginManager.cpp | 21 +++ src/slic3r/plugin/PluginManager.hpp | 4 +- src/slic3r/plugin/PyPluginTrampoline.hpp | 6 + src/slic3r/plugin/PythonPluginBridge.cpp | 41 ++++++ src/slic3r/plugin/PythonPluginInterface.hpp | 4 + 21 files changed, 545 insertions(+), 13 deletions(-) create mode 100644 src/libslic3r/LifecycleEvents.hpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 18d805936e..6f5f2ad8e4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7,6 +7,7 @@ #include "I18N.hpp" #include "GCode.hpp" #include "Exception.hpp" +#include "LifecycleEvents.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" #include "Geometry/ConvexHull.hpp" @@ -2446,6 +2447,13 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_writer.set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); + } + // check if any custom gcode contains keywords used by the gcode processor to // produce time estimation and gcode toolpaths std::vector> validation_res = DoExport::validate_custom_gcode(*print); @@ -2481,12 +2489,22 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.set_print(print); GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor); if (! file.is_open()) { - BOOST_LOG_TRIVIAL(error) << std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n" << std::endl; + std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"; + BOOST_LOG_TRIVIAL(error) << err_msg << std::endl; if (!fs::exists(folder)) { //fs::create_directory(folder); - BOOST_LOG_TRIVIAL(error) << "the parent path " + folder.string() +" is not there!!!" << std::endl; + std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!"; + BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl; + err_msg += add_err_msg; } - throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Error; + ctx.msg = err_msg; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } + throw Slic3r::RuntimeError(err_msg); } try { @@ -2497,11 +2515,18 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu boost::nowide::remove(path_tmp.c_str()); throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n"); } - } catch (std::exception & /* ex */) { + } catch (std::exception &ex) { // Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown. // Close and remove the file. file.close(); boost::nowide::remove(path_tmp.c_str()); + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Error; + ctx.msg = ex.what(); + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw; } file.close(); @@ -2607,6 +2632,13 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu std::error_code ret = rename_file(path_tmp, path); if (ret) { + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Error; + ctx.msg = "Failed to rename the output G-code file: " + ret.message(); + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw Slic3r::RuntimeError( std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' + "Is " + path_tmp + " locked?" + '\n'); @@ -2617,7 +2649,14 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info(); print->set_done(psGCodeExport); - + + { + LifecycleEventContext ctx; + ctx.name = path; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } + // Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT / // M486), which is driven by exclude_object for every printer if(result != nullptr) diff --git a/src/libslic3r/LifecycleEvents.hpp b/src/libslic3r/LifecycleEvents.hpp new file mode 100644 index 0000000000..1528e97b46 --- /dev/null +++ b/src/libslic3r/LifecycleEvents.hpp @@ -0,0 +1,135 @@ +#pragma once + +// LifecycleEvents.hpp +// -------------------- +// Application lifecycle events (project, slicing, plate editing, preset, printer connection +// activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to +// observe. Lives in libslic3r rather than the plugin layer because some events fire from inside +// the slicing engine itself; see fire_lifecycle_event() below. + +#include +#include +#include + +namespace Slic3r +{ + enum class LifecycleEvent { + // Project (3mf) + NewProject, + ProjectOpened, + ProjectBeforeSave, + ProjectAfterSave, + ProjectClosed, + + // Slicing pipeline + SliceStarted, + SliceGeometryFinished, + GCodeExportStarted, + GCodeExportFinished, + SlicingJobComplete, + + // Plate/model editing + ObjectAdded, + ObjectDeleted, + ObjectTransformed, + + // Preset + PresetSelected, + PresetSaved, + + // Printer/device + PrintStateChanged, + DeviceOnlineChanged, + DeviceDiscovered, + DeviceSelected, + DeviceConnected, + DeviceDisconnected, + UploadStarted, + UploadFinished, + }; + + // Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with + // Windows macros (wingdi.h) as unqualified names. + enum class LifecycleEvtCode { Ok, Error, Warn }; + + struct LifecycleEventContext + { + // The primary subject identifier for the event. This is event-specific (for example, a + // project/output path, preset name, device id, or object name), must not contain status or + // prose, and may be empty when the event has no single subject. + std::string name; + + // Outcome of the operation represented by the event. For state-change and start events, + // Ok means that the event occurred; it does not imply that a future operation succeeded. + LifecycleEvtCode code = LifecycleEvtCode::Ok; + + // Optional human-readable detail or diagnostic text. It is not a stable parsing contract; + // machine-readable data should be represented by a dedicated field or event instead. + std::string msg; + }; + + inline std::string lifecycle_event_to_string(LifecycleEvent event) + { + switch (event) { + case LifecycleEvent::NewProject: return "NewProject"; + case LifecycleEvent::ProjectOpened: return "ProjectOpened"; + case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave"; + case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave"; + case LifecycleEvent::ProjectClosed: return "ProjectClosed"; + + case LifecycleEvent::SliceStarted: return "SliceStarted"; + case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished"; + case LifecycleEvent::GCodeExportStarted: return "GCodeExportStarted"; + case LifecycleEvent::GCodeExportFinished: return "GCodeExportFinished"; + case LifecycleEvent::SlicingJobComplete: return "SlicingJobComplete"; + + case LifecycleEvent::ObjectAdded: return "ObjectAdded"; + case LifecycleEvent::ObjectDeleted: return "ObjectDeleted"; + case LifecycleEvent::ObjectTransformed: return "ObjectTransformed"; + + case LifecycleEvent::PresetSelected: return "PresetSelected"; + case LifecycleEvent::PresetSaved: return "PresetSaved"; + case LifecycleEvent::PrintStateChanged: return "PrintStateChanged"; + + case LifecycleEvent::DeviceOnlineChanged: return "DeviceOnlineChanged"; + case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered"; + case LifecycleEvent::DeviceSelected: return "DeviceSelected"; + case LifecycleEvent::DeviceConnected: return "DeviceConnected"; + case LifecycleEvent::DeviceDisconnected: return "DeviceDisconnected"; + + case LifecycleEvent::UploadStarted: return "UploadStarted"; + case LifecycleEvent::UploadFinished: return "UploadFinished"; + default: return "Unknown"; + } + } + + inline std::string lifecycle_evt_code_to_string(LifecycleEvtCode code) + { + switch (code) { + case LifecycleEvtCode::Ok: return "Ok"; + case LifecycleEvtCode::Error: return "Error"; + case LifecycleEvtCode::Warn: return "Warn"; + default: return "Unknown"; + } + } + + // Global cross-layer seam (mirrors ConfigBase::set_resolve_capability_fn): any libslic3r code can + // fire a lifecycle event without depending on the plugin layer above it, which installs the + // dispatcher here at startup. Not tied to Print/GCode specifically, since nothing here should + // require callers to hold a Print& just to report an event. + using LifecycleHookFn = std::function; + + inline LifecycleHookFn& lifecycle_hook_fn() + { + static LifecycleHookFn fn; + return fn; + } + + inline void set_lifecycle_hook_fn(LifecycleHookFn fn) { lifecycle_hook_fn() = std::move(fn); } + + inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) + { + if (const LifecycleHookFn& fn = lifecycle_hook_fn(); fn) + fn(event, ctx); + } +} diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9ae8b86fd8..22a0c4f6a7 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -45,6 +45,7 @@ #include #include "libslic3r.h" +#include "LifecycleEvents.hpp" #include "Utils.hpp" #include "Time.hpp" #include "PlaceholderParser.hpp" @@ -2931,6 +2932,7 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det // 1) Find the preset with a new_name or create a new one, // initialize it with the edited config. auto it = this->find_preset_internal(new_name); + const bool preset_existed = (it != m_presets.end() && it->name == new_name); if (it != m_presets.end() && it->name == new_name) { // Preset with the same name found. Preset &preset = *it; @@ -3038,6 +3040,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det this->get_selected_preset().save(&(parent_preset->config)); else this->get_selected_preset().save(nullptr); + + { + LifecycleEventContext ctx; + ctx.name = new_name; + ctx.msg = preset_existed ? "overwrite" : "new"; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::PresetSaved, ctx); + } } bool PresetCollection::delete_current_preset() diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1af28255ee..68bff0cd8e 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -8,6 +8,7 @@ #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" +#include "LifecycleEvents.hpp" #include "ShortestPath.hpp" #include "Thread.hpp" #include "Time.hpp" @@ -2252,6 +2253,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) if (m_objects.empty()) return; + { + LifecycleEventContext ctx; + ctx.name = output_filename(); + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx); + } + for (PrintObject *obj : m_objects) obj->clear_shared_object(); @@ -2818,6 +2826,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info(); + + { + LifecycleEventContext ctx; + ctx.code = LifecycleEvtCode::Ok; + ctx.name = output_filename(); + fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx); + } } // G-code export process, running at a background thread. @@ -4434,6 +4449,13 @@ void Print::set_gcode_file_invalidated() //BBS: add gcode file preload logic void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb) { + { + LifecycleEventContext ctx; + ctx.name = file; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); + } + try { GCodeProcessor processor; GCodeProcessor::s_IsBBLPrinter = is_BBL_printer(); @@ -4453,13 +4475,27 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces *result = std::move(processor.extract_result()); result->filament_change_sequence = filament_seq_loaded; result->nozzle_change_sequence = nozzle_seq_loaded; - } catch (std::exception & /* ex */) { + } catch (std::exception &ex) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str(); + { + LifecycleEventContext ctx; + ctx.name = file; + ctx.code = LifecycleEvtCode::Error; + ctx.msg = ex.what(); + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw Slic3r::RuntimeError( std::string("Failed to process the G-code file ") + file + " from previous 3mf\n"); } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str(); + + { + LifecycleEventContext ctx; + ctx.name = file; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } } std::tuple Print::object_skirt_offset(double margin_height) const diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index edc958ec53..e3a785f430 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -10,6 +10,7 @@ #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libslic3r/Time.hpp" @@ -328,7 +329,10 @@ namespace Slic3r obj->bind_state = "free"; obj->last_alive = Slic3r::Utils::get_current_time_utc(); - obj->m_is_online = true; + // Route through set_online_state() (rather than writing m_is_online directly) so the + // DeviceOnlineChanged lifecycle event fires consistently; same effective value/behavior + // here since the object was already online in the common case. + obj->set_online_state(true); obj->set_dev_name(dev_name); /* if (!obj->dev_ip.empty()) { Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip); @@ -345,6 +349,10 @@ namespace Slic3r obj->bind_sec_link = sec_link; obj->dev_connection_name = connection_name; obj->bind_ssdp_version = ssdp_version; + // Discovery establishes the initial reachability state. Do not report it as an + // online transition; DeviceDiscovered below is the lifecycle event for a new + // device. Subsequent updates route through set_online_state(), so a known device + // still emits DeviceOnlineChanged when its reachability actually changes. obj->m_is_online = true; //load access code @@ -361,6 +369,15 @@ namespace Slic3r BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id << ", ip = " << dev_ip <<", printer_name = " << dev_name << ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state; + + // First discovery of a genuinely new device (not a periodic SSDP/heartbeat update to + // an already-known one, which is handled in the branch above). + { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx); + } } update_local_machine(*obj); } @@ -935,8 +952,14 @@ namespace Slic3r } void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/, - const std::string& /*new_dev_id*/) + const std::string& new_dev_id) { + { + LifecycleEventContext ctx; + ctx.name = new_dev_id; // empty string is a valid deselection + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceSelected, ctx); + } if (MachineObject* obj_ = get_selected_machine()) { GUI::wxGetApp().sidebar().update_sync_status(obj_); if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index f4befe78c1..ba818aefd6 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Time.hpp" #include "libslic3r/Thread.hpp" #include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "GuiColor.hpp" #include "GUI_App.hpp" @@ -2591,7 +2592,15 @@ void MachineObject::reset() void MachineObject::set_print_state(std::string status) { + const bool changed = (print_status != status); print_status = status; + if (changed) { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = print_status; + fire_lifecycle_event(LifecycleEvent::PrintStateChanged, ctx); + } } int MachineObject::connect(bool use_openssl) @@ -2613,6 +2622,10 @@ int MachineObject::connect(bool use_openssl) int MachineObject::disconnect() { if (m_agent) { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); return m_agent->disconnect_printer(); } return -1; @@ -2643,8 +2656,16 @@ bool MachineObject::is_connecting() void MachineObject::set_online_state(bool on_off) { + const bool changed = (m_is_online != on_off); m_is_online = on_off; if (!on_off) m_active_state = NotActive; + if (changed) { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = on_off ? "online" : "offline"; + fire_lifecycle_event(LifecycleEvent::DeviceOnlineChanged, ctx); + } } bool MachineObject::is_info_ready(bool check_version) const @@ -4566,8 +4587,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ try { if (j.contains("event")) { if (j["event"].contains("event")) { - if (j["event"]["event"].get() == "client.disconnected") + if (j["event"]["event"].get() == "client.disconnected") { set_online_state(false); + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); + } else if (j["event"]["event"].get() == "client.connected") set_online_state(true); } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index f6ffb75405..469ab8120e 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -39,6 +39,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" #include "slic3r/Utils/UndoRedo.hpp" #include "slic3r/Utils/MacDarkMode.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include @@ -5061,8 +5062,16 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) //BBS: nofity object list to update wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items(); - if (object_moved) + if (object_moved) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "moved"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED)); + } // BBS: support wipe-tower for multi-plates for (int plate_id = 0; plate_id < wipe_tower_origins.size(); plate_id++) { @@ -5183,8 +5192,16 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) //BBS: nofity object list to update wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items(); - if (!done.empty()) + if (!done.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "rotated"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); + } m_dirty = true; } @@ -5275,8 +5292,16 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) //BBS: notify object info update wxGetApp().plater()->show_object_info(); - if (!done.empty()) + if (!done.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "scaled"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); + } m_dirty = true; } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 0d4e3f35cf..83b2b2303d 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2169,6 +2169,11 @@ void GUI_App::init_networking_callbacks() obj->command_get_access_code(); if (m_agent) m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer()); + + LifecycleEventContext ctx; + ctx.name = obj->get_dev_id(); + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx); } }); }); @@ -2207,6 +2212,11 @@ void GUI_App::init_networking_callbacks() obj->command_get_version(); event.SetInt(0); event.SetString(obj->get_dev_id()); + + LifecycleEventContext ctx; + ctx.name = obj->get_dev_id(); + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx); } else if (state == ConnectStatus::ConnectStatusFailed) { // Orca: only update status if same device id if (m_device_manager->selected_machine != dev_id) return; @@ -2222,10 +2232,22 @@ void GUI_App::init_networking_callbacks() wxGetApp().show_dialog(text); } event.SetInt(-1); + + { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); + } } else if (state == ConnectStatus::ConnectStatusLost) { m_device_manager->set_selected_machine(""); event.SetInt(-1); BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost"; + + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); } else { event.SetInt(-1); BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index dc89f5f9bb..e786b7241b 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -9,6 +9,7 @@ #include "BitmapComboBox.hpp" #include "MainFrame.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "OptionsGroup.hpp" #include "Tab.hpp" @@ -3530,7 +3531,14 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx) obj->delete_connectors(); if (obj->volumes.empty() || !obj->has_solid_mesh()) { + const std::string deleted_obj_name = obj->name; model.delete_object(idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = deleted_obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } m_objects_model->Delete(m_objects_model->GetItemById(idx)); continue; } @@ -4099,6 +4107,13 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed, const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut()); Expand(m_objects_model->GetParent(item)); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = model_object->name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectAdded, ctx); + } + if (!do_info_update) return; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 6b74b71af1..71a5e58344 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -13,6 +13,7 @@ #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libnest2d/common.hpp" @@ -699,6 +700,13 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale(ap.translation(X)) % unscale(ap.translation(Y)) % ap.name; } + { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "arranged"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } + m_plater->update(); // BBS //wxGetApp().obj_manipul()->set_dirty(); diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index a9a66cb96f..502282c307 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -6,6 +6,7 @@ #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libnest2d/common.hpp" #include @@ -347,6 +348,13 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) m_plater->arrange(); } m_plater->update(); + + { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "arranged"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } } m_plater->mark_plate_toolbar_image_dirty(); diff --git a/src/slic3r/GUI/Jobs/OrientJob.cpp b/src/slic3r/GUI/Jobs/OrientJob.cpp index ee8ea875c0..ed9117e8bf 100644 --- a/src/slic3r/GUI/Jobs/OrientJob.cpp +++ b/src/slic3r/GUI/Jobs/OrientJob.cpp @@ -5,6 +5,7 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libslic3r/PresetBundle.hpp" @@ -254,6 +255,12 @@ void OrientJob::finalize(bool canceled, std::exception_ptr &eptr) mesh.apply(); } + if (!m_selected.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "auto_oriented"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } m_plater->update(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2225812ff6..197ece0823 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8375,7 +8375,14 @@ void Plater::priv::remove(size_t obj_idx) view3D->enable_layers_editing(false); m_worker.cancel_all(); + std::string obj_name = (obj_idx < model.objects.size()) ? model.objects[obj_idx]->name : std::to_string(obj_idx); model.delete_object(obj_idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); update(); @@ -8408,7 +8415,14 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia if (obj->is_cut()) sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); + std::string obj_name = obj->name; model.delete_object(obj_idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); @@ -8458,6 +8472,14 @@ void Plater::priv::reset(bool apply_presets_change) clear_warnings(); + const std::string closed_project_name = into_u8(get_project_filename()); + if (!closed_project_name.empty() || !model.objects.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.name = closed_project_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectClosed, ctx); + } + set_project_filename(""); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: empty"; @@ -10868,11 +10890,15 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) notification_manager->set_slicing_progress_export_possible(); // Reset the "export G-code path" name, so that the automatic background processing will be enabled again. + const std::string lifecycle_job_name = this->background_process.fff_print() ? + this->background_process.fff_print()->output_filename() : std::string(); this->background_process.reset_export(); // This bool stops showing export finished notification even when process_completed_with_error is false bool has_error = false; + std::string lifecycle_error_msg; if (evt.error()) { auto message = evt.format_error_message(); + lifecycle_error_msg = message.first; if (evt.critical_error()) { if (q->m_tracking_popup_menu) { // We don't want to pop-up a message box when tracking a pop-up menu. @@ -10910,6 +10936,14 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) is_finished = true; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = lifecycle_job_name; + ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok); + ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string()); + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx); + } + //BBS: set the current plater's slice result to valid if (!this->background_process.empty()) this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success()); @@ -13068,6 +13102,11 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ //get_partplate_list().reinit(); //get_partplate_list().update_slice_context_to_current_plate(p->background_process); //p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + if (!silent) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx); + } reset(transfer_preset_changes); reset_project_dirty_after_save(); reset_project_dirty_initial_presets(); @@ -13199,6 +13238,13 @@ void Plater::load_project(wxString const& filename2, p->set_project_name(_L("Untitled")); } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(load_restore ? originfile : filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectOpened, ctx); + } + } else { if (using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename; @@ -13261,11 +13307,23 @@ int Plater::save_project(bool saveAs) if (full_pathnames) { save_strategy = save_strategy | SaveStrategy::FullPathSources; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectBeforeSave, ctx); + } if (export_3mf(into_path(filename), save_strategy) < 0) { MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."), _L("Save project"), wxOK | wxICON_WARNING).ShowModal(); return wxID_CANCEL; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectAfterSave, ctx); + } Slic3r::remove_backup(model(), false); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1a31355d0e..9d6e2299c0 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -36,6 +36,7 @@ #include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/plugin/PluginConfig.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "Plater.hpp" #include "MainFrame.hpp" #include "format.hpp" @@ -6900,6 +6901,20 @@ bool Tab::select_preset( } load_current_preset(); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = preset_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + switch (m_type) { + case Preset::TYPE_PRINT: ctx.msg = "print"; break; + case Preset::TYPE_SLA_PRINT: ctx.msg = "sla_print"; break; + case Preset::TYPE_FILAMENT: ctx.msg = "filament"; break; + case Preset::TYPE_SLA_MATERIAL: ctx.msg = "sla_material"; break; + case Preset::TYPE_PRINTER: ctx.msg = "printer"; break; + default: break; + } + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::PresetSelected, ctx); + } if (delete_third_printer) { wxGetApp().CallAfter([filament_presets, process_presets]() { diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 7f69d5e087..0952019b10 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -23,6 +23,7 @@ #include "CrealityPrint.hpp" #include "../GUI/PrintHostDialogs.hpp" #include "../GUI/MainFrame.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "Obico.hpp" #include "Flashforge.hpp" #include "SimplyPrint.hpp" @@ -358,12 +359,29 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) { emit_progress(0); // Indicate the upload is starting + // Captured before upload_data is moved into upload() below. + const std::string upload_filename = the_job.upload_data.source_path.filename().string(); + + { + LifecycleEventContext ctx; + ctx.name = upload_filename; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx); + } + bool success = the_job.printhost->upload(std::move(the_job.upload_data), [this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); }, [this](wxString error) { this->error_fn(std::move(error)); }, [this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); } ); + { + LifecycleEventContext ctx; + ctx.name = upload_filename; + ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error; + fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx); + } + if (success) { emit_progress(100); if (the_job.switch_to_device_tab) { diff --git a/src/slic3r/plugin/PluginHooks.cpp b/src/slic3r/plugin/PluginHooks.cpp index c6aaaf803c..2d84df5fdb 100644 --- a/src/slic3r/plugin/PluginHooks.cpp +++ b/src/slic3r/plugin/PluginHooks.cpp @@ -7,6 +7,7 @@ #include "libslic3r/Config.hpp" #include "libslic3r/Exception.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/Print.hpp" #include "libslic3r_version.h" @@ -46,6 +47,16 @@ void install_capability_resolver() }); } +// Global libslic3r-side seam (Slic3r::fire_lifecycle_event, in libslic3r/LifecycleEvents.hpp): +// broadcasts to every loaded, enabled capability regardless of type, unlike the SlicingPipeline +// hook below which only targets picker-selected SlicingPipeline capabilities. +void install_lifecycle_event_hook() +{ + set_lifecycle_hook_fn([](LifecycleEvent event, const LifecycleEventContext& ctx) { + PluginManager::instance().dispatch_lifecycle_event(event, ctx); + }); +} + // Print::process() fires this hook at each pipeline seam on the slicing worker // thread; here we run the picker-selected SlicingPipeline capabilities. Per // capability we acquire the GIL, honor cancellation, and convert a plugin @@ -122,12 +133,14 @@ void install() { install_capability_resolver(); install_slicing_pipeline_hook(); + install_lifecycle_event_hook(); } void uninstall() { ConfigBase::set_resolve_capability_fn(nullptr); Print::set_slicing_pipeline_hook_fn(nullptr); + set_lifecycle_hook_fn(nullptr); } } // namespace Slic3r::plugin_hooks diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index 3587c0a824..3675f0e466 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -1,5 +1,6 @@ #include "PluginManager.hpp" +#include #include #include #include @@ -1987,4 +1988,24 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k return result; } +void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) { + for (const auto& cap : get_plugin_capabilities()) { + if (!cap || !cap->is_enabled()) continue; + try { + cap->on_lifecycle_event(evt, ctx); + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name() + << "' on_lifecycle_event(" << lifecycle_event_to_string(evt) << ") threw: " << ex.what() + << " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code) + << ", msg='" << ctx.msg << "']"; + } catch (...) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name() + << "' on_lifecycle_event(" << lifecycle_event_to_string(evt) + << ") threw a non-standard exception" + << " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code) + << ", msg='" << ctx.msg << "']"; + } + } +} + } // namespace Slic3r diff --git a/src/slic3r/plugin/PluginManager.hpp b/src/slic3r/plugin/PluginManager.hpp index ddb0bf9dce..3d6be9981a 100644 --- a/src/slic3r/plugin/PluginManager.hpp +++ b/src/slic3r/plugin/PluginManager.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -20,7 +21,6 @@ #include #include "CloudPluginService.hpp" -#include "PluginFsUtils.hpp" #include "PluginDescriptor.hpp" #include "PluginLoader.hpp" #include "PluginConfig.hpp" @@ -204,6 +204,8 @@ public: ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error); + void dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx); + private: PluginManager() = default; PluginManager(const PluginManager&) = delete; diff --git a/src/slic3r/plugin/PyPluginTrampoline.hpp b/src/slic3r/plugin/PyPluginTrampoline.hpp index e36c0b375c..a36fd8100a 100644 --- a/src/slic3r/plugin/PyPluginTrampoline.hpp +++ b/src/slic3r/plugin/PyPluginTrampoline.hpp @@ -129,6 +129,12 @@ public: { ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); } + + void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) override + { + ORCA_PY_OVERRIDE_AUDITED( + ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx); + } }; class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 328ebbace1..264b72a24c 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -338,6 +338,44 @@ void bind_python_api(pybind11::module_& m) .value("FatalError", PluginResult::FatalError) .export_values(); + py::enum_(m, "LifecycleEvent", "Application lifecycle moment passed to on_lifecycle_event") + .value("NewProject", LifecycleEvent::NewProject) + .value("ProjectOpened", LifecycleEvent::ProjectOpened) + .value("ProjectBeforeSave", LifecycleEvent::ProjectBeforeSave) + .value("ProjectAfterSave", LifecycleEvent::ProjectAfterSave) + .value("ProjectClosed", LifecycleEvent::ProjectClosed) + .value("SliceStarted", LifecycleEvent::SliceStarted) + .value("SliceGeometryFinished", LifecycleEvent::SliceGeometryFinished) + .value("GCodeExportStarted", LifecycleEvent::GCodeExportStarted) + .value("GCodeExportFinished", LifecycleEvent::GCodeExportFinished) + .value("SlicingJobComplete", LifecycleEvent::SlicingJobComplete) + .value("ObjectAdded", LifecycleEvent::ObjectAdded) + .value("ObjectDeleted", LifecycleEvent::ObjectDeleted) + .value("ObjectTransformed", LifecycleEvent::ObjectTransformed) + .value("PresetSelected", LifecycleEvent::PresetSelected) + .value("PresetSaved", LifecycleEvent::PresetSaved) + .value("PrintStateChanged", LifecycleEvent::PrintStateChanged) + .value("DeviceOnlineChanged", LifecycleEvent::DeviceOnlineChanged) + .value("DeviceDiscovered", LifecycleEvent::DeviceDiscovered) + .value("DeviceSelected", LifecycleEvent::DeviceSelected) + .value("DeviceConnected", LifecycleEvent::DeviceConnected) + .value("DeviceDisconnected", LifecycleEvent::DeviceDisconnected) + .value("UploadStarted", LifecycleEvent::UploadStarted) + .value("UploadFinished", LifecycleEvent::UploadFinished) + .export_values(); + + py::enum_(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext") + .value("Ok", LifecycleEvtCode::Ok) + .value("Error", LifecycleEvtCode::Error) + .value("Warn", LifecycleEvtCode::Warn) + .export_values(); + + py::class_(m, "LifecycleEventContext", "Payload accompanying a LifecycleEvent") + .def(py::init<>()) + .def_readonly("name", &LifecycleEventContext::name) + .def_readonly("code", &LifecycleEventContext::code) + .def_readonly("msg", &LifecycleEventContext::msg); + py::class_(m, "PluginContext", "Context shared with plugin entry points") .def(py::init<>()) .def_readwrite("orca_version", &PluginContext::orca_version); @@ -362,6 +400,9 @@ void bind_python_api(pybind11::module_& m) .def("get_type", &PluginCapabilityInterface::get_type) .def("on_load", &PluginCapabilityInterface::on_load) .def("on_unload", &PluginCapabilityInterface::on_unload) + .def("on_lifecycle_event", &PluginCapabilityInterface::on_lifecycle_event, + "Override to react to an application lifecycle moment (LifecycleEvent) and its\n" + "LifecycleEventContext payload. Available on every capability type.") .def("has_config_ui", &PluginCapabilityInterface::has_config_ui, "Override to return True to replace the host's default JSON editor with your own HTML\n" "UI, returned by get_config_ui(). Every capability is configurable and appears in the\n" diff --git a/src/slic3r/plugin/PythonPluginInterface.hpp b/src/slic3r/plugin/PythonPluginInterface.hpp index 4a6df06441..c1e0b17aac 100644 --- a/src/slic3r/plugin/PythonPluginInterface.hpp +++ b/src/slic3r/plugin/PythonPluginInterface.hpp @@ -10,6 +10,8 @@ #include #include +#include + namespace Slic3r { enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; @@ -167,6 +169,8 @@ public: virtual void on_unload() {} virtual void on_cancelled() {} + virtual void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) { (void) event; (void) ctx; } + // ── C++-only host state, never exposed to Python. Set by the loader at materialization. ── // // The capability owns its own identity and enable flag: they are read once under the GIL, live From 64d04a75f3365fa72a89d22170433e659aaa2be5 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Mon, 24 Aug 2026 11:36:37 +0800 Subject: [PATCH 02/19] feat: additional events --- src/libslic3r/LifecycleEvents.hpp | 50 ++++++++++++++++- src/slic3r/GUI/GUI_ObjectList.cpp | 25 ++++++++- src/slic3r/GUI/Jobs/PrintJob.cpp | 22 ++++++++ src/slic3r/GUI/Jobs/PrintJob.hpp | 3 + src/slic3r/GUI/Jobs/SendJob.cpp | 21 +++++++ src/slic3r/GUI/Jobs/SendJob.hpp | 2 + src/slic3r/GUI/PartPlate.cpp | 35 +++++++++++- src/slic3r/GUI/Plater.cpp | 61 ++++++++++++++++----- src/slic3r/GUI/ProjectDirtyStateManager.cpp | 47 +++++++++++++++- src/slic3r/GUI/ProjectDirtyStateManager.hpp | 23 +++++++- src/slic3r/GUI/TaskManager.cpp | 18 ++++++ src/slic3r/plugin/PythonPluginBridge.cpp | 20 ++++++- 12 files changed, 307 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/LifecycleEvents.hpp b/src/libslic3r/LifecycleEvents.hpp index 1528e97b46..9954836b3e 100644 --- a/src/libslic3r/LifecycleEvents.hpp +++ b/src/libslic3r/LifecycleEvents.hpp @@ -2,8 +2,8 @@ // LifecycleEvents.hpp // -------------------- -// Application lifecycle events (project, slicing, plate editing, preset, printer connection -// activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to +// Application lifecycle events (project, slicing, plate editing, preset, printer connection, and +// job activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to // observe. Lives in libslic3r rather than the plugin layer because some events fire from inside // the slicing engine itself; see fire_lifecycle_event() below. @@ -20,6 +20,7 @@ namespace Slic3r ProjectBeforeSave, ProjectAfterSave, ProjectClosed, + ProjectDirtyChanged, // Slicing pipeline SliceStarted, @@ -32,6 +33,12 @@ namespace Slic3r ObjectAdded, ObjectDeleted, ObjectTransformed, + ObjectChanged, + ObjectRenamed, + PlateCreated, + PlateDeleted, + PlateSelected, + PlateRenamed, // Preset PresetSelected, @@ -46,6 +53,12 @@ namespace Slic3r DeviceDisconnected, UploadStarted, UploadFinished, + + // Print/send jobs + PrintJobStarted, + PrintJobFinished, + SendJobStarted, + SendJobFinished, }; // Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with @@ -66,6 +79,28 @@ namespace Slic3r // Optional human-readable detail or diagnostic text. It is not a stable parsing contract; // machine-readable data should be represented by a dedicated field or event instead. std::string msg; + + // Stable subject/object identifier, when the source model provides one. + std::string id; + + // Previous value for rename and other before/after events. + std::string previous_name; + + // Device identifier for printer and job events. + std::string device_id; + + // Job identifier when the originating queue/task provides one. + std::string job_id; + + // Source subsystem or operation detail, suitable for filtering but not guaranteed to be + // exhaustive across versions. + std::string source; + + // Plate, object, or volume index when the source uses an index rather than a stable id. + int index = -1; + + // Aggregate project dirty state for ProjectDirtyChanged. + bool dirty = false; }; inline std::string lifecycle_event_to_string(LifecycleEvent event) @@ -76,6 +111,7 @@ namespace Slic3r case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave"; case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave"; case LifecycleEvent::ProjectClosed: return "ProjectClosed"; + case LifecycleEvent::ProjectDirtyChanged: return "ProjectDirtyChanged"; case LifecycleEvent::SliceStarted: return "SliceStarted"; case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished"; @@ -86,6 +122,12 @@ namespace Slic3r case LifecycleEvent::ObjectAdded: return "ObjectAdded"; case LifecycleEvent::ObjectDeleted: return "ObjectDeleted"; case LifecycleEvent::ObjectTransformed: return "ObjectTransformed"; + case LifecycleEvent::ObjectChanged: return "ObjectChanged"; + case LifecycleEvent::ObjectRenamed: return "ObjectRenamed"; + case LifecycleEvent::PlateCreated: return "PlateCreated"; + case LifecycleEvent::PlateDeleted: return "PlateDeleted"; + case LifecycleEvent::PlateSelected: return "PlateSelected"; + case LifecycleEvent::PlateRenamed: return "PlateRenamed"; case LifecycleEvent::PresetSelected: return "PresetSelected"; case LifecycleEvent::PresetSaved: return "PresetSaved"; @@ -99,6 +141,10 @@ namespace Slic3r case LifecycleEvent::UploadStarted: return "UploadStarted"; case LifecycleEvent::UploadFinished: return "UploadFinished"; + case LifecycleEvent::PrintJobStarted: return "PrintJobStarted"; + case LifecycleEvent::PrintJobFinished: return "PrintJobFinished"; + case LifecycleEvent::SendJobStarted: return "SendJobStarted"; + case LifecycleEvent::SendJobFinished: return "SendJobFinished"; default: return "Unknown"; } } diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index e786b7241b..2d492be812 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1,5 +1,6 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "GUI_ObjectList.hpp" #include "GUI_Factories.hpp" //#include "GUI_ObjectLayers.hpp" @@ -1200,17 +1201,39 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const if (m_objects_model->GetItemType(item) & itObject) { std::string name = m_objects_model->GetName(item).ToUTF8().data(); if (obj->name != name) { + const std::string previous_name = obj->name; obj->name = name; // if object has just one volume, rename this volume too if (obj->volumes.size() == 1) obj->volumes[0]->name = obj->name; Slic3r::save_object_mesh(*obj); + + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.id = std::to_string(obj->id().id); + ctx.index = obj_idx; + ctx.source = "object"; + fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx); } return; } if (volume_id < 0) return; - obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data(); + std::string name = m_objects_model->GetName(item).ToUTF8().data(); + if (obj->volumes[volume_id]->name == name) + return; + + const std::string previous_name = obj->volumes[volume_id]->name; + obj->volumes[volume_id]->name = name; + + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.id = std::to_string(obj->volumes[volume_id]->id().id); + ctx.index = volume_id; + ctx.source = "volume"; + fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx); } void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index d79deb955b..cd9b851b2a 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -1,4 +1,5 @@ #include "PrintJob.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" @@ -133,6 +134,13 @@ wxString PrintJob::get_http_error_msg(unsigned int status, std::string body) void PrintJob::process(Ctl &ctl) { + LifecycleEventContext start_ctx; + start_ctx.name = m_project_name; + start_ctx.device_id = m_dev_id; + start_ctx.source = "print_job"; + fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); + m_lifecycle_started = true; + /* display info */ std::string msg; wxString error_str; @@ -689,6 +697,7 @@ void PrintJob::process(Ctl &ctl) } wxQueueEvent(m_plater, evt); m_job_finished = true; + m_lifecycle_success = true; } } @@ -701,6 +710,19 @@ void PrintJob::finalize(bool canceled, std::exception_ptr &eptr) { eptr = std::current_exception(); } + if (m_lifecycle_started && !m_lifecycle_finished) { + LifecycleEventContext finish_ctx; + finish_ctx.name = m_project_name; + finish_ctx.device_id = m_dev_id; + finish_ctx.source = "print_job"; + finish_ctx.code = canceled ? LifecycleEvtCode::Warn : + (eptr || !m_lifecycle_success ? LifecycleEvtCode::Error : LifecycleEvtCode::Ok); + finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" : + (m_lifecycle_success ? "" : "failed")); + fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx); + m_lifecycle_finished = true; + } + if (canceled || eptr) return; } diff --git a/src/slic3r/GUI/Jobs/PrintJob.hpp b/src/slic3r/GUI/Jobs/PrintJob.hpp index 9a23b04054..4be68031d9 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.hpp +++ b/src/slic3r/GUI/Jobs/PrintJob.hpp @@ -43,6 +43,9 @@ class PrintJob : public Job std::function m_success_fun{nullptr}; std::string m_dev_id; bool m_job_finished{ false }; + bool m_lifecycle_started{ false }; + bool m_lifecycle_finished{ false }; + bool m_lifecycle_success{ false }; int m_print_job_completed_id = 0; wxString m_completed_evt_data; std::function m_enter_ip_address_fun_fail{ nullptr }; diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index bb52367188..5bf9010201 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -1,4 +1,5 @@ #include "SendJob.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" @@ -146,6 +147,13 @@ void SendJob::process(Ctl &ctl) } } + LifecycleEventContext start_ctx; + start_ctx.name = m_project_name; + start_ctx.device_id = m_dev_id; + start_ctx.source = "send_job"; + fire_lifecycle_event(LifecycleEvent::SendJobStarted, start_ctx); + m_lifecycle_started = true; + int total_plate_num = m_plater->get_partplate_list().get_plate_count(); PartPlate* plate = m_plater->get_partplate_list().get_plate(job_data.plate_idx); @@ -423,6 +431,19 @@ void SendJob::finalize(bool canceled, std::exception_ptr &eptr) eptr = std::current_exception(); } + if (m_lifecycle_started && !m_lifecycle_finished) { + LifecycleEventContext finish_ctx; + finish_ctx.name = m_project_name; + finish_ctx.device_id = m_dev_id; + finish_ctx.source = "send_job"; + finish_ctx.code = canceled ? LifecycleEvtCode::Warn : + (eptr ? LifecycleEvtCode::Error : (m_job_finished ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error)); + finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" : + (m_job_finished ? "" : "failed")); + fire_lifecycle_event(LifecycleEvent::SendJobFinished, finish_ctx); + m_lifecycle_finished = true; + } + if (canceled || eptr) return; } diff --git a/src/slic3r/GUI/Jobs/SendJob.hpp b/src/slic3r/GUI/Jobs/SendJob.hpp index 1d4e025d6e..86d180ac8c 100644 --- a/src/slic3r/GUI/Jobs/SendJob.hpp +++ b/src/slic3r/GUI/Jobs/SendJob.hpp @@ -22,6 +22,8 @@ class SendJob : public Job PrintPrepareData job_data; std::string m_dev_id; bool m_job_finished{ false }; + bool m_lifecycle_started{ false }; + bool m_lifecycle_finished{ false }; int m_print_job_completed_id = 0; bool m_is_check_mode{false}; bool m_check_and_continue{false}; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 910c761c06..a2466bc4a7 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -25,6 +25,7 @@ #include "libslic3r/Tesselate.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "I18N.hpp" #include "GUI_App.hpp" @@ -2527,11 +2528,20 @@ void PartPlate::set_plate_name(const std::string& name) if (boost::equals(m_name, name)) return; + const std::string previous_name = m_name; m_name = name; if (m_print != nullptr) m_print->set_plate_name(name); invalidate_plate_name_texture(); + + if (m_plater != nullptr && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.index = m_plate_index; + fire_lifecycle_event(LifecycleEvent::PlateRenamed, ctx); + } } //get the print's object, result and index @@ -4638,9 +4648,16 @@ int PartPlateList::create_plate(bool adjust_position) if (m_plater) { // In GUI mode - wxGetApp().obj_list()->on_plate_added(plate); + wxGetApp().obj_list()->on_plate_added(plate); } + if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = plate->get_plate_name(); + ctx.index = new_index; + fire_lifecycle_event(LifecycleEvent::PlateCreated, ctx); + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":created a new plate %1%") % new_index; return new_index; } @@ -4739,6 +4756,7 @@ int PartPlateList::delete_plate(int index) BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate %1%, has an invalid index %2%") % index % plate->get_index(); return -1; } + const std::string plate_name = plate->get_plate_name(); if (m_plater) { // In GUI mode @@ -4821,6 +4839,13 @@ int PartPlateList::delete_plate(int index) delete plate; + if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = plate_name; + ctx.index = index; + fire_lifecycle_event(LifecycleEvent::PlateDeleted, ctx); + } + // FIX: context of BackgroundSliceProcess and gcode preview need to be updated before ObjectList::reload_all_plates(). #if 0 if (m_plater != nullptr) { @@ -4929,6 +4954,7 @@ int PartPlateList::select_plate(int index) if (m_plate_list.empty() || index >= m_plate_list.size()) { return -1; } + const int previous_index = m_current_plate; // BBS: erase unnecessary snapshot if (get_curr_plate_index() != index && m_intialized) { @@ -4955,6 +4981,13 @@ int PartPlateList::select_plate(int index) //wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT)); } + if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = m_plate_list[index]->get_plate_name(); + ctx.index = index; + fire_lifecycle_event(LifecycleEvent::PlateSelected, ctx); + } + return 0; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d9ad76b51f..4da3370681 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -61,6 +61,7 @@ #include #include "libslic3r/libslic3r.h" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/Format/STL.hpp" #include "libslic3r/Format/DRC.hpp" #include "libslic3r/Format/STEP.hpp" @@ -8470,6 +8471,11 @@ void Plater::priv::delete_all_objects_from_model() void Plater::priv::reset(bool apply_presets_change) { + // TakeSnapshot below and load_current_presets() further down each re-evaluate the + // aggregate dirty flag against a baseline that hasn't been reset yet, so they can toggle + // is_dirty() back and forth several times before it settles; coalesce those into one event. + ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(dirty_state); + Plater::TakeSnapshot snapshot(q, _u8L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator); clear_warnings(); @@ -13118,21 +13124,28 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ ctx.code = Slic3r::LifecycleEvtCode::Ok; Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx); } - reset(transfer_preset_changes); - reset_project_dirty_after_save(); - reset_project_dirty_initial_presets(); - wxGetApp().update_saved_preset_from_current_preset(); - update_project_dirty_from_presets(); + { + // Same rationale as in Plater::priv::reset(): the whole reset + preset-reload + + // baseline-reset sequence below settles into its final dirty state only once it + // completes, so hold notifications until then to avoid firing on transient flips. + ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(p->dirty_state); - //reset project - p->project.reset(); - //set project name - if (project_name.empty()) - p->set_project_name(_L("Untitled")); - else - p->set_project_name(project_name); + reset(transfer_preset_changes); + reset_project_dirty_after_save(); + reset_project_dirty_initial_presets(); + wxGetApp().update_saved_preset_from_current_preset(); + update_project_dirty_from_presets(); - Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator); + //reset project + p->project.reset(); + //set project name + if (project_name.empty()) + p->set_project_name(_L("Untitled")); + else + p->set_project_name(project_name); + + Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator); + } Model m; model().load_from(m); // new id avoid same path name @@ -18339,6 +18352,14 @@ void Plater::changed_object(ModelObject &object){ // Check outside bed get_current_canvas3D()->requires_check_outside_state(); + + if (!is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = object.name; + ctx.id = std::to_string(object.id().id); + ctx.source = "geometry"; + fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx); + } } void Plater::changed_object(int obj_idx) @@ -18375,6 +18396,20 @@ void Plater::changed_objects(const std::vector& object_idxs) // update print this->p->schedule_background_process(); + + if (!is_loading_project()) { + for (size_t obj_idx : object_idxs) { + if (obj_idx >= p->model.objects.size() || p->model.objects[obj_idx] == nullptr) + continue; + + LifecycleEventContext ctx; + ctx.name = p->model.objects[obj_idx]->name; + ctx.id = std::to_string(p->model.objects[obj_idx]->id().id); + ctx.index = static_cast(obj_idx); + ctx.source = "geometry"; + fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx); + } + } } void Plater::schedule_background_process(bool schedule/* = true*/) diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.cpp b/src/slic3r/GUI/ProjectDirtyStateManager.cpp index c986328f71..53eb3cd353 100644 --- a/src/slic3r/GUI/ProjectDirtyStateManager.cpp +++ b/src/slic3r/GUI/ProjectDirtyStateManager.cpp @@ -6,6 +6,7 @@ #include "MainFrame.hpp" #include "I18N.hpp" #include "Plater.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include @@ -17,13 +18,23 @@ namespace GUI { void ProjectDirtyStateManager::update_from_undo_redo_stack(bool dirty) { + const bool was_dirty = is_dirty(); m_plater_dirty = dirty; + notify_dirty_change(was_dirty, "undo_redo"); if (const Plater *plater = wxGetApp().plater(); plater && wxGetApp().initialized()) wxGetApp().mainframe->update_title(); } +void ProjectDirtyStateManager::set_plater_dirty(bool is_dirty) +{ + const bool was_dirty = this->is_dirty(); + m_plater_dirty = is_dirty; + notify_dirty_change(was_dirty, "plater"); +} + void ProjectDirtyStateManager::update_from_presets() { + const bool was_dirty = is_dirty(); m_presets_dirty = false; // check switching of the presets only for exist/loaded project, but not for new GUI_App &app = wxGetApp(); @@ -45,18 +56,53 @@ void ProjectDirtyStateManager::update_from_presets() } m_presets_dirty |= app.has_unsaved_preset_changes(); m_project_config_dirty = m_initial_project_config != app.preset_bundle->project_config; + notify_dirty_change(was_dirty, "presets"); app.mainframe->update_title(); } void ProjectDirtyStateManager::reset_after_save() { + const bool was_dirty = is_dirty(); this->reset_initial_presets(); m_plater_dirty = false; m_presets_dirty = false; m_project_config_dirty = false; + notify_dirty_change(was_dirty, "save"); wxGetApp().mainframe->update_title(); } +void ProjectDirtyStateManager::notify_dirty_change(bool was_dirty, const char *source) +{ + if (m_suppress_depth > 0) + return; + + const bool dirty = is_dirty(); + if (was_dirty == dirty) + return; + + LifecycleEventContext ctx; + ctx.code = LifecycleEvtCode::Ok; + ctx.dirty = dirty; + ctx.source = source; + fire_lifecycle_event(LifecycleEvent::ProjectDirtyChanged, ctx); +} + +void ProjectDirtyStateManager::begin_suppress_notifications() +{ + if (m_suppress_depth == 0) + m_suppress_entry_dirty = is_dirty(); + ++m_suppress_depth; +} + +void ProjectDirtyStateManager::end_suppress_notifications() +{ + assert(m_suppress_depth > 0); + if (m_suppress_depth > 0) + --m_suppress_depth; + if (m_suppress_depth == 0) + notify_dirty_change(m_suppress_entry_dirty, "batch"); +} + void ProjectDirtyStateManager::reset_initial_presets() { m_initial_presets.fill(std::string{}); @@ -168,4 +214,3 @@ void ProjectDirtyStateManager::render_debug_window() const } // namespace GUI } // namespace Slic3r - diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.hpp b/src/slic3r/GUI/ProjectDirtyStateManager.hpp index 171d19e219..fd121d1d85 100644 --- a/src/slic3r/GUI/ProjectDirtyStateManager.hpp +++ b/src/slic3r/GUI/ProjectDirtyStateManager.hpp @@ -14,21 +14,42 @@ public: void reset_after_save(); void reset_initial_presets(); - void set_plater_dirty(bool is_dirty) { m_plater_dirty = is_dirty; } + void set_plater_dirty(bool is_dirty); bool is_dirty() const { return m_plater_dirty || m_project_config_dirty || m_presets_dirty; } bool is_presets_dirty() const { return m_presets_dirty; } + // RAII guard coalescing dirty-state updates: while any guard is alive, ProjectDirtyChanged + // notifications are held back; when the outermost guard is destroyed, at most one + // notification fires, reflecting only the net change across the whole guarded scope. + class NotificationSuppressor + { + public: + explicit NotificationSuppressor(ProjectDirtyStateManager &owner) : m_owner(owner) { m_owner.begin_suppress_notifications(); } + ~NotificationSuppressor() { m_owner.end_suppress_notifications(); } + NotificationSuppressor(const NotificationSuppressor &) = delete; + NotificationSuppressor &operator=(const NotificationSuppressor &) = delete; + private: + ProjectDirtyStateManager &m_owner; + }; + #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW void render_debug_window() const; #endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW private: + void notify_dirty_change(bool was_dirty, const char *source); + void begin_suppress_notifications(); + void end_suppress_notifications(); + // Does the Undo / Redo stack indicate the project is dirty? bool m_plater_dirty { false }; // Do the presets indicate the project is dirty? bool m_presets_dirty { false }; // Is the project config dirty? bool m_project_config_dirty { false }; + // NotificationSuppressor nesting depth and the dirty state observed when the outermost guard began. + int m_suppress_depth { 0 }; + bool m_suppress_entry_dirty { false }; // Keeps track of preset names selected at the time of last project save. std::array m_initial_presets; DynamicPrintConfig m_initial_project_config; diff --git a/src/slic3r/GUI/TaskManager.cpp b/src/slic3r/GUI/TaskManager.cpp index d294d140c5..b0f6f43f58 100644 --- a/src/slic3r/GUI/TaskManager.cpp +++ b/src/slic3r/GUI/TaskManager.cpp @@ -1,6 +1,7 @@ #include "TaskManager.hpp" #include "libslic3r/Thread.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "nlohmann/json.hpp" #include "MainFrame.hpp" #include "GUI_App.hpp" @@ -210,6 +211,13 @@ int TaskManager::schedule(TaskStateInfo* task) assert(task->state() == TaskState::TS_PENDING); task->set_state(TaskState::TS_SENDING); + LifecycleEventContext start_ctx; + start_ctx.name = task->params().project_name; + start_ctx.device_id = task->params().dev_id; + start_ctx.job_id = std::to_string(task->task_info_id); + start_ctx.source = "task_manager"; + fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); + BOOST_LOG_TRIVIAL(trace) << "task_manager: schedule a task to dev_id = " << task->params().dev_id; boost::thread* new_sending_thread = new boost::thread(); *new_sending_thread = Slic3r::create_thread( @@ -237,6 +245,16 @@ int TaskManager::schedule(TaskStateInfo* task) task->set_state(TaskState::TS_SEND_CANCELED); } } + + LifecycleEventContext finish_ctx; + finish_ctx.name = task->params().project_name; + finish_ctx.device_id = task->params().dev_id; + finish_ctx.job_id = std::to_string(task->task_info_id); + finish_ctx.source = "task_manager"; + finish_ctx.code = result == 0 ? LifecycleEvtCode::Ok : + (task->is_canceled() ? LifecycleEvtCode::Warn : LifecycleEvtCode::Error); + finish_ctx.msg = result == 0 ? "" : (task->is_canceled() ? "cancelled" : "failed"); + fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx); /* remove from sending task list */ m_scedule_mutex.lock(); diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 34a91a543e..2b4ca52fd8 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -345,6 +345,7 @@ void bind_python_api(pybind11::module_& m) .value("ProjectBeforeSave", LifecycleEvent::ProjectBeforeSave) .value("ProjectAfterSave", LifecycleEvent::ProjectAfterSave) .value("ProjectClosed", LifecycleEvent::ProjectClosed) + .value("ProjectDirtyChanged", LifecycleEvent::ProjectDirtyChanged) .value("SliceStarted", LifecycleEvent::SliceStarted) .value("SliceGeometryFinished", LifecycleEvent::SliceGeometryFinished) .value("GCodeExportStarted", LifecycleEvent::GCodeExportStarted) @@ -353,6 +354,12 @@ void bind_python_api(pybind11::module_& m) .value("ObjectAdded", LifecycleEvent::ObjectAdded) .value("ObjectDeleted", LifecycleEvent::ObjectDeleted) .value("ObjectTransformed", LifecycleEvent::ObjectTransformed) + .value("ObjectChanged", LifecycleEvent::ObjectChanged) + .value("ObjectRenamed", LifecycleEvent::ObjectRenamed) + .value("PlateCreated", LifecycleEvent::PlateCreated) + .value("PlateDeleted", LifecycleEvent::PlateDeleted) + .value("PlateSelected", LifecycleEvent::PlateSelected) + .value("PlateRenamed", LifecycleEvent::PlateRenamed) .value("PresetSelected", LifecycleEvent::PresetSelected) .value("PresetSaved", LifecycleEvent::PresetSaved) .value("PrintStateChanged", LifecycleEvent::PrintStateChanged) @@ -363,6 +370,10 @@ void bind_python_api(pybind11::module_& m) .value("DeviceDisconnected", LifecycleEvent::DeviceDisconnected) .value("UploadStarted", LifecycleEvent::UploadStarted) .value("UploadFinished", LifecycleEvent::UploadFinished) + .value("PrintJobStarted", LifecycleEvent::PrintJobStarted) + .value("PrintJobFinished", LifecycleEvent::PrintJobFinished) + .value("SendJobStarted", LifecycleEvent::SendJobStarted) + .value("SendJobFinished", LifecycleEvent::SendJobFinished) .export_values(); py::enum_(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext") @@ -375,7 +386,14 @@ void bind_python_api(pybind11::module_& m) .def(py::init<>()) .def_readonly("name", &LifecycleEventContext::name) .def_readonly("code", &LifecycleEventContext::code) - .def_readonly("msg", &LifecycleEventContext::msg); + .def_readonly("msg", &LifecycleEventContext::msg) + .def_readonly("id", &LifecycleEventContext::id) + .def_readonly("previous_name", &LifecycleEventContext::previous_name) + .def_readonly("device_id", &LifecycleEventContext::device_id) + .def_readonly("job_id", &LifecycleEventContext::job_id) + .def_readonly("source", &LifecycleEventContext::source) + .def_readonly("index", &LifecycleEventContext::index) + .def_readonly("dirty", &LifecycleEventContext::dirty); py::class_(m, "PluginContext", "Context shared with plugin entry points") .def(py::init<>()) From 33a8265a11555043a27c746abcd1670cb82bd3cd Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 25 Aug 2026 15:37:15 +0800 Subject: [PATCH 03/19] fix: slicer regression test --- src/libslic3r/Print.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 2aa9ac8ce9..29086725ea 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2255,7 +2255,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) { LifecycleEventContext ctx; - ctx.name = output_filename(); + ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Ok; fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx); } @@ -2829,8 +2829,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) { LifecycleEventContext ctx; + ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Ok; - ctx.name = output_filename(); fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx); } } @@ -4451,8 +4451,9 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces { { LifecycleEventContext ctx; - ctx.name = file; + ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Ok; + ctx.msg = file; fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); } @@ -4479,9 +4480,9 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str(); { LifecycleEventContext ctx; - ctx.name = file; + ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Error; - ctx.msg = ex.what(); + ctx.msg = file + "\n" + ex.what(); fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } throw Slic3r::RuntimeError( @@ -4492,8 +4493,9 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces { LifecycleEventContext ctx; - ctx.name = file; + ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Ok; + ctx.msg = file; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } } From fabd08af1e4b54733a4d98c111c316d1b42d3db4 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 25 Aug 2026 15:59:17 +0800 Subject: [PATCH 04/19] update GCodeExport to use model id for ctx.name --- src/libslic3r/GCode.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 6f5f2ad8e4..2e16959a71 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2449,8 +2449,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu { LifecycleEventContext ctx; - ctx.name = path; + ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Ok; + ctx.msg = path; fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); } @@ -2499,9 +2500,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu } { LifecycleEventContext ctx; - ctx.name = path; + ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Error; - ctx.msg = err_msg; + ctx.msg = std::string(path) + "\n" + err_msg; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } throw Slic3r::RuntimeError(err_msg); @@ -2522,9 +2523,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu boost::nowide::remove(path_tmp.c_str()); { LifecycleEventContext ctx; - ctx.name = path; + ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Error; - ctx.msg = ex.what(); + ctx.msg = std::string(path) + "\n" + ex.what(); fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } throw; @@ -2634,9 +2635,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu if (ret) { { LifecycleEventContext ctx; - ctx.name = path; + ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Error; - ctx.msg = "Failed to rename the output G-code file: " + ret.message(); + ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message(); fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } throw Slic3r::RuntimeError( @@ -2652,8 +2653,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu { LifecycleEventContext ctx; - ctx.name = path; + ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Ok; + ctx.msg = path; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } From ce41b5a3cb6d3f5eb3db4abc66964d75c4acd78e Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 18 Sep 2026 22:33:46 +0800 Subject: [PATCH 05/19] fix: stale audit mode on_lifecycle_event --- src/slic3r/plugin/PyPluginTrampoline.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/slic3r/plugin/PyPluginTrampoline.hpp b/src/slic3r/plugin/PyPluginTrampoline.hpp index c23bc30e98..4c77f1ba9c 100644 --- a/src/slic3r/plugin/PyPluginTrampoline.hpp +++ b/src/slic3r/plugin/PyPluginTrampoline.hpp @@ -135,8 +135,7 @@ public: void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) override { - ORCA_PY_OVERRIDE_AUDITED( - ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx); + ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx); } }; From 7ca2b9ad9c9a24a9e6740aadbeb808cc450efa48 Mon Sep 17 00:00:00 2001 From: InfimechOfficial <144992637+InfimechOfficial@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:46:05 +0800 Subject: [PATCH 06/19] =?UTF-8?q?update=20FlyingBear=20Ghost7=200.4=20nozz?= =?UTF-8?q?le.json,=20InfiMech=20EX=200.4=20nozzle.json=E2=80=A6=20(#15707?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update FlyingBear Ghost7 0.4 nozzle.json, InfiMech EX 0.4 nozzle.json, InfiMech EX+APS 0.4 nozzle.json Co-authored-by: Flyingbear Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com> --- .../FlyingBear/machine/Ghost7/FlyingBear Ghost7 0.4 nozzle.json | 2 +- .../InfiMech/machine/EX+APS/InfiMech EX+APS 0.4 nozzle.json | 2 +- .../profiles/InfiMech/machine/EX/InfiMech EX 0.4 nozzle.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/profiles/FlyingBear/machine/Ghost7/FlyingBear Ghost7 0.4 nozzle.json b/resources/profiles/FlyingBear/machine/Ghost7/FlyingBear Ghost7 0.4 nozzle.json index fd14f77ebf..9f195a4eca 100644 --- a/resources/profiles/FlyingBear/machine/Ghost7/FlyingBear Ghost7 0.4 nozzle.json +++ b/resources/profiles/FlyingBear/machine/Ghost7/FlyingBear Ghost7 0.4 nozzle.json @@ -198,6 +198,6 @@ "0.4" ], "z_hop_types": [ - "Normal Lift" + "Auto Lift" ] } diff --git a/resources/profiles/InfiMech/machine/EX+APS/InfiMech EX+APS 0.4 nozzle.json b/resources/profiles/InfiMech/machine/EX+APS/InfiMech EX+APS 0.4 nozzle.json index a77186c2fd..9fb5343da9 100644 --- a/resources/profiles/InfiMech/machine/EX+APS/InfiMech EX+APS 0.4 nozzle.json +++ b/resources/profiles/InfiMech/machine/EX+APS/InfiMech EX+APS 0.4 nozzle.json @@ -198,6 +198,6 @@ "0.4" ], "z_hop_types": [ - "Normal Lift" + "Auto Lift" ] } diff --git a/resources/profiles/InfiMech/machine/EX/InfiMech EX 0.4 nozzle.json b/resources/profiles/InfiMech/machine/EX/InfiMech EX 0.4 nozzle.json index a018e3f8a3..ec9bbc6d00 100644 --- a/resources/profiles/InfiMech/machine/EX/InfiMech EX 0.4 nozzle.json +++ b/resources/profiles/InfiMech/machine/EX/InfiMech EX 0.4 nozzle.json @@ -198,6 +198,6 @@ "0.4" ], "z_hop_types": [ - "Normal Lift" + "Auto Lift" ] } From 0fb8f3d48733b34f82f0d325bd8043558885e5ac Mon Sep 17 00:00:00 2001 From: peachismomo Date: Wed, 23 Sep 2026 02:14:56 +0800 Subject: [PATCH 07/19] fix: synchronize lifecycle hook shutdown with active dispatches --- src/libslic3r/LifecycleEvents.hpp | 77 +++++++++++++++++++-- src/slic3r/plugin/PluginHooks.hpp | 5 +- src/slic3r/plugin/PluginManager.cpp | 3 +- tests/slic3rutils/test_plugin_lifecycle.cpp | 70 +++++++++++++++++++ 4 files changed, 146 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/LifecycleEvents.hpp b/src/libslic3r/LifecycleEvents.hpp index 9954836b3e..9ceaa99f30 100644 --- a/src/libslic3r/LifecycleEvents.hpp +++ b/src/libslic3r/LifecycleEvents.hpp @@ -8,6 +8,9 @@ // the slicing engine itself; see fire_lifecycle_event() below. #include +#include +#include +#include #include #include @@ -165,17 +168,79 @@ namespace Slic3r // require callers to hold a Print& just to report an event. using LifecycleHookFn = std::function; - inline LifecycleHookFn& lifecycle_hook_fn() + namespace detail { + + struct LifecycleHookState { - static LifecycleHookFn fn; - return fn; + std::mutex mutex; + std::condition_variable cv; + LifecycleHookFn fn; + std::size_t active_dispatches = 0; + bool accepting = false; + }; + + inline LifecycleHookState& lifecycle_hook_state() + { + static LifecycleHookState state; + return state; } - inline void set_lifecycle_hook_fn(LifecycleHookFn fn) { lifecycle_hook_fn() = std::move(fn); } + class LifecycleDispatchGuard + { + public: + explicit LifecycleDispatchGuard(LifecycleHookState& state) : m_state(state) {} + + ~LifecycleDispatchGuard() + { + std::lock_guard lock(m_state.mutex); + --m_state.active_dispatches; + if (m_state.active_dispatches == 0) + m_state.cv.notify_all(); + } + + LifecycleDispatchGuard(const LifecycleDispatchGuard&) = delete; + LifecycleDispatchGuard& operator=(const LifecycleDispatchGuard&) = delete; + + private: + LifecycleHookState& m_state; + }; + + } // namespace detail + + // Installing a hook starts accepting dispatches. Passing an empty function stops accepting + // new dispatches, detaches the hook, and waits for callbacks already in progress to finish. + // This is used during plugin shutdown so plugin code cannot be unloaded while a lifecycle + // callback is still executing. The empty-function path must not be called from inside the + // lifecycle callback itself. + inline void set_lifecycle_hook_fn(LifecycleHookFn fn) + { + detail::LifecycleHookState& state = detail::lifecycle_hook_state(); + if (fn) { + std::lock_guard lock(state.mutex); + state.fn = std::move(fn); + state.accepting = true; + return; + } + + std::unique_lock lock(state.mutex); + state.accepting = false; + state.fn = nullptr; + state.cv.wait(lock, [&state] { return state.active_dispatches == 0; }); + } inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) { - if (const LifecycleHookFn& fn = lifecycle_hook_fn(); fn) - fn(event, ctx); + detail::LifecycleHookState& state = detail::lifecycle_hook_state(); + LifecycleHookFn fn; + { + std::lock_guard lock(state.mutex); + if (!state.accepting || !state.fn) + return; + fn = state.fn; + ++state.active_dispatches; + } + + detail::LifecycleDispatchGuard guard(state); + fn(event, ctx); } } diff --git a/src/slic3r/plugin/PluginHooks.hpp b/src/slic3r/plugin/PluginHooks.hpp index bff276c5df..5eeacfa449 100644 --- a/src/slic3r/plugin/PluginHooks.hpp +++ b/src/slic3r/plugin/PluginHooks.hpp @@ -14,8 +14,9 @@ namespace Slic3r::plugin_hooks { void install(); // Reset every hook to null so none can enter Python after the interpreter -// finalizes. Called from PluginManager::shutdown(); callers must have stopped -// background slicing first (resetting a hook while process() runs is a race). +// finalizes. The lifecycle-event hook drains callbacks already in progress +// before returning. Other hooks retain their existing caller-side shutdown +// requirements. void uninstall(); } // namespace Slic3r::plugin_hooks diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index dad77aa7a0..2b30841172 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -135,7 +135,8 @@ void PluginManager::shutdown() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown enter"; // Detach the libslic3r hooks first so nothing dispatches into Python while (or after) plugins - // unload. Callers stop background slicing before this. + // unload. The lifecycle-event hook also drains callbacks already in progress before returning; + // the remaining hook seams retain their existing shutdown requirements. plugin_hooks::uninstall(); // Reject new plugin loads before we drain. diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index 63a4e6827b..bac3a19ea9 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -12,8 +13,11 @@ #include #include +#include #include +#include #include +#include #include using namespace Slic3r; @@ -114,6 +118,72 @@ PluginDescriptor descriptor_of(PluginManager& manager, const std::string& plugin } // namespace +TEST_CASE("Lifecycle hook shutdown drains concurrent dispatch", "[PluginLifecycle][LifecycleEvents]") +{ + std::mutex mutex; + std::condition_variable cv; + bool callback_entered = false; + bool release_callback = false; + bool shutdown_started = false; + bool shutdown_finished = false; + + set_lifecycle_hook_fn([&](LifecycleEvent, const LifecycleEventContext&) { + std::unique_lock lock(mutex); + callback_entered = true; + cv.notify_all(); + cv.wait(lock, [&] { return release_callback; }); + }); + + std::thread dispatch_thread([] { + LifecycleEventContext ctx; + fire_lifecycle_event(LifecycleEvent::ProjectOpened, ctx); + }); + + bool callback_was_entered = false; + { + std::unique_lock lock(mutex); + callback_was_entered = cv.wait_for(lock, std::chrono::seconds(5), [&] { return callback_entered; }); + } + if (!callback_was_entered) { + { + std::lock_guard lock(mutex); + release_callback = true; + cv.notify_all(); + } + dispatch_thread.join(); + set_lifecycle_hook_fn(nullptr); + FAIL("Lifecycle callback did not start"); + } + + std::thread shutdown_thread([&] { + { + std::lock_guard lock(mutex); + shutdown_started = true; + cv.notify_all(); + } + set_lifecycle_hook_fn(nullptr); + std::lock_guard lock(mutex); + shutdown_finished = true; + cv.notify_all(); + }); + + bool shutdown_was_started = false; + { + std::unique_lock lock(mutex); + shutdown_was_started = cv.wait_for(lock, std::chrono::seconds(5), [&] { return shutdown_started; }); + if (shutdown_was_started) + CHECK_FALSE(cv.wait_for(lock, std::chrono::milliseconds(100), [&] { return shutdown_finished; })); + release_callback = true; + cv.notify_all(); + } + + dispatch_thread.join(); + shutdown_thread.join(); + if (!shutdown_was_started) + FAIL("Lifecycle hook shutdown did not start"); + CHECK(shutdown_finished); +} + TEST_CASE("A discovered script plugin loads and materializes its capability", "[PluginLifecycle][Python]") { ScopedPluginManager plugin_system; From 24ddd81d7d3cb932cc606be44fa828d7c876d44b Mon Sep 17 00:00:00 2001 From: peachismomo Date: Wed, 23 Sep 2026 02:31:46 +0800 Subject: [PATCH 08/19] feat: stop lifecycle dispatch after slicing cancellation --- src/libslic3r/GCode.cpp | 5 +++++ src/libslic3r/LifecycleEvents.hpp | 5 +++++ src/libslic3r/Print.cpp | 5 +++++ src/slic3r/plugin/PluginManager.cpp | 9 +++++++++ 4 files changed, 24 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 560ed434d5..1a1d1bcbff 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2482,6 +2482,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Ok; ctx.msg = path; + ctx.cancellation_check = [print]() { return print->canceled(); }; fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); } @@ -2533,6 +2534,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Error; ctx.msg = std::string(path) + "\n" + err_msg; + ctx.cancellation_check = [print]() { return print->canceled(); }; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } throw Slic3r::RuntimeError(err_msg); @@ -2556,6 +2558,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Error; ctx.msg = std::string(path) + "\n" + ex.what(); + ctx.cancellation_check = [print]() { return print->canceled(); }; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } throw; @@ -2668,6 +2671,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Error; ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message(); + ctx.cancellation_check = [print]() { return print->canceled(); }; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } throw Slic3r::RuntimeError( @@ -2686,6 +2690,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu ctx.name = std::to_string(print->model().id().id); ctx.code = LifecycleEvtCode::Ok; ctx.msg = path; + ctx.cancellation_check = [print]() { return print->canceled(); }; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } diff --git a/src/libslic3r/LifecycleEvents.hpp b/src/libslic3r/LifecycleEvents.hpp index 9ceaa99f30..e1abc795c1 100644 --- a/src/libslic3r/LifecycleEvents.hpp +++ b/src/libslic3r/LifecycleEvents.hpp @@ -104,6 +104,11 @@ namespace Slic3r // Aggregate project dirty state for ProjectDirtyChanged. bool dirty = false; + + // Optional host-side cancellation probe. Background slicing and G-code export events set + // this to the originating Print's cancellation state so dispatch can stop before calling + // the next capability. It is intentionally not exposed through the Python payload API. + std::function cancellation_check; }; inline std::string lifecycle_event_to_string(LifecycleEvent event) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 3734318c1a..5df69b7335 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2699,6 +2699,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) LifecycleEventContext ctx; ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Ok; + ctx.cancellation_check = [this]() { return canceled(); }; fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx); } @@ -3325,6 +3326,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) LifecycleEventContext ctx; ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Ok; + ctx.cancellation_check = [this]() { return canceled(); }; fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx); } } @@ -4931,6 +4933,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Ok; ctx.msg = file; + ctx.cancellation_check = [this]() { return canceled(); }; fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); } @@ -4960,6 +4963,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Error; ctx.msg = file + "\n" + ex.what(); + ctx.cancellation_check = [this]() { return canceled(); }; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } throw Slic3r::RuntimeError( @@ -4973,6 +4977,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces ctx.name = std::to_string(m_model.id().id); ctx.code = LifecycleEvtCode::Ok; ctx.msg = file; + ctx.cancellation_check = [this]() { return canceled(); }; fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); } } diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index 2b30841172..022f85eb1f 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -2092,8 +2092,17 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k } void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) { + const auto is_canceled = [&ctx]() { + return ctx.cancellation_check && ctx.cancellation_check(); + }; + + if (is_canceled()) + return; + for (const auto& cap : get_plugin_capabilities()) { if (!cap || !cap->is_enabled()) continue; + if (is_canceled()) + break; try { cap->on_lifecycle_event(evt, ctx); } catch (const std::exception& ex) { From f83bfa17ff878a6689b875353df7200ab2d30053 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Tue, 22 Sep 2026 13:41:30 -0500 Subject: [PATCH 09/19] ci: keep older compiler cache entries when the save wrote nothing (#15828) #15668 saves the compiler cache on cancelled and failed builds and then drops the older entries for the leg on the ref. actions/cache/save only warns when its tar fails, so a cancelled build whose ccache directory was still being written saved nothing, the drop ran anyway and deleted the leg's last good entry. The next run on main restored nothing and compiled cold, and so did every PR that restored in the gap. Run 35405244634 (Flatpak x86_64, 2026-09-18) did this to ccache-Flatpak-x86_64-35397824860-1; between 13 and 18 September 9 of 87 cancelled main build jobs did the same. Look the new entry up before deleting anything, and keep the older ones when it is not there. --- .github/workflows/build_all.yml | 8 ++++++++ .github/workflows/build_orca.yml | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index ed106ec65e..24317b0add 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -413,6 +413,14 @@ jobs: GH_TOKEN: ${{ github.token }} run: | api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches" + # The save step reports success even when its tar failed, so keep + # the older entries unless the new one is listed. + if ! curl -sSf -H "Authorization: Bearer $GH_TOKEN" \ + "$api?ref=$GITHUB_REF&key=$CCACHE_ENTRY" \ + | jq -e --arg entry "$CCACHE_ENTRY" 'any(.actions_caches[]; .key == $entry)' > /dev/null; then + echo "$CCACHE_ENTRY was not saved; keeping the older entries." + exit 0 + fi curl -sSf -H "Authorization: Bearer $GH_TOKEN" \ "$api?ref=$GITHUB_REF&key=ccache-$CCACHE_LEG-&per_page=100" \ | jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \ diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 85458615b4..2ea72d0773 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -798,6 +798,13 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + # The save step reports success even when its tar failed, so keep + # the older entries unless the new one is listed. + if ! gh cache list --ref "$GITHUB_REF" --key "$CCACHE_ENTRY" --json key \ + | jq -e --arg entry "$CCACHE_ENTRY" 'any(.[]; .key == $entry)' > /dev/null; then + echo "$CCACHE_ENTRY was not saved; keeping the older entries." + exit 0 + fi gh cache list --ref "$GITHUB_REF" --key "ccache-$CCACHE_LEG-" --limit 100 --json id,key \ | jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \ '.[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \ From fb825acbd57009d9f052299c539a3c51ed2362fb Mon Sep 17 00:00:00 2001 From: peachismomo Date: Wed, 23 Sep 2026 02:46:44 +0800 Subject: [PATCH 10/19] fix: use original project name --- src/slic3r/GUI/Jobs/PrintJob.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index 25535ff1a5..d11756c0e2 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -181,7 +181,7 @@ void PrintJob::process(Ctl &ctl) } } - m_project_name = truncate_string(m_project_name, 100); + const std::string transport_project_name = truncate_string(m_project_name, 100); int curr_plate_idx = 0; if (m_print_type == "from_normal") { @@ -380,11 +380,11 @@ void PrintJob::process(Ctl &ctl) } } - if (params.preset_name.empty() && m_print_type == "from_normal") { params.preset_name = wxString::Format("%s_plate_%d", m_project_name, curr_plate_idx).ToStdString(); } - if (params.project_name.empty()) {params.project_name = m_project_name;} + if (params.preset_name.empty() && m_print_type == "from_normal") { params.preset_name = wxString::Format("%s_plate_%d", transport_project_name, curr_plate_idx).ToStdString(); } + if (params.project_name.empty()) {params.project_name = transport_project_name;} if (m_is_calibration_task) { - params.project_name = m_project_name; + params.project_name = transport_project_name; params.origin_model_id = ""; } From 4a4c649dbb0e36aec8b10a80b3537cb0d64d0d15 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Wed, 23 Sep 2026 02:51:35 +0800 Subject: [PATCH 11/19] fix: dispatch task lifecycle events on one worktre thread --- src/slic3r/GUI/TaskManager.cpp | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/TaskManager.cpp b/src/slic3r/GUI/TaskManager.cpp index b0f6f43f58..64f3e5d6a5 100644 --- a/src/slic3r/GUI/TaskManager.cpp +++ b/src/slic3r/GUI/TaskManager.cpp @@ -6,6 +6,8 @@ #include "MainFrame.hpp" #include "GUI_App.hpp" +#include + using namespace nlohmann; namespace Slic3r { @@ -211,28 +213,38 @@ int TaskManager::schedule(TaskStateInfo* task) assert(task->state() == TaskState::TS_PENDING); task->set_state(TaskState::TS_SENDING); - LifecycleEventContext start_ctx; - start_ctx.name = task->params().project_name; - start_ctx.device_id = task->params().dev_id; - start_ctx.job_id = std::to_string(task->task_info_id); - start_ctx.source = "task_manager"; - fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); - BOOST_LOG_TRIVIAL(trace) << "task_manager: schedule a task to dev_id = " << task->params().dev_id; boost::thread* new_sending_thread = new boost::thread(); *new_sending_thread = Slic3r::create_thread( [this, task] { + // Keep both lifecycle callbacks on this per-task worker thread. Plugin observers can + // therefore associate Started and Finished for one task with a single execution context. + LifecycleEventContext start_ctx; + start_ctx.name = task->params().project_name; + start_ctx.device_id = task->params().dev_id; + start_ctx.job_id = std::to_string(task->task_info_id); + start_ctx.source = "task_manager"; + fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); + + int result = -1; if (!m_agent) { BOOST_LOG_TRIVIAL(trace) << "task_manager: NetworkAgent is nullptr"; - return; } - assert(m_agent); + else { + assert(m_agent); + try { // DEBUG FOR TEST #if 0 - int result = start_print_test(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn); + result = start_print_test(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn); #else - int result = m_agent->start_print(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn); + result = m_agent->start_print(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn); #endif + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << "task_manager: start_print threw: " << ex.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "task_manager: start_print threw an unknown exception"; + } + } if (result == 0) { last_sent_timestamp = std::chrono::system_clock::now(); task->set_sent_time(last_sent_timestamp); From ff96cce7c323b276a63c3ea861df6eb6fb5b30a3 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Wed, 23 Sep 2026 03:09:03 +0800 Subject: [PATCH 12/19] fix: update lifecycle contract for printer connection --- src/libslic3r/LifecycleEvents.hpp | 10 ++--- src/slic3r/GUI/DeviceCore/DevManager.cpp | 4 +- src/slic3r/GUI/DeviceManager.cpp | 57 +++++++++++------------- src/slic3r/GUI/DeviceManager.hpp | 1 + src/slic3r/GUI/GUI_App.cpp | 22 ++------- src/slic3r/plugin/PythonPluginBridge.cpp | 5 +-- 6 files changed, 39 insertions(+), 60 deletions(-) diff --git a/src/libslic3r/LifecycleEvents.hpp b/src/libslic3r/LifecycleEvents.hpp index e1abc795c1..f3e6f32392 100644 --- a/src/libslic3r/LifecycleEvents.hpp +++ b/src/libslic3r/LifecycleEvents.hpp @@ -49,11 +49,10 @@ namespace Slic3r // Printer/device PrintStateChanged, - DeviceOnlineChanged, + DeviceOnline, + DeviceOffline, DeviceDiscovered, DeviceSelected, - DeviceConnected, - DeviceDisconnected, UploadStarted, UploadFinished, @@ -141,11 +140,10 @@ namespace Slic3r case LifecycleEvent::PresetSaved: return "PresetSaved"; case LifecycleEvent::PrintStateChanged: return "PrintStateChanged"; - case LifecycleEvent::DeviceOnlineChanged: return "DeviceOnlineChanged"; + case LifecycleEvent::DeviceOnline: return "DeviceOnline"; + case LifecycleEvent::DeviceOffline: return "DeviceOffline"; case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered"; case LifecycleEvent::DeviceSelected: return "DeviceSelected"; - case LifecycleEvent::DeviceConnected: return "DeviceConnected"; - case LifecycleEvent::DeviceDisconnected: return "DeviceDisconnected"; case LifecycleEvent::UploadStarted: return "UploadStarted"; case LifecycleEvent::UploadFinished: return "UploadFinished"; diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index 0a456a9267..c03f0cfe59 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -357,7 +357,7 @@ namespace Slic3r obj->last_alive = Slic3r::Utils::get_current_time_utc(); // Route through set_online_state() (rather than writing m_is_online directly) so the - // DeviceOnlineChanged lifecycle event fires consistently; same effective value/behavior + // DeviceOnline lifecycle event fires consistently; same effective value/behavior // here since the object was already online in the common case. obj->set_online_state(true); obj->set_dev_name(dev_name); @@ -380,7 +380,7 @@ namespace Slic3r // Discovery establishes the initial reachability state. Do not report it as an // online transition; DeviceDiscovered below is the lifecycle event for a new // device. Subsequent updates route through set_online_state(), so a known device - // still emits DeviceOnlineChanged when its reachability actually changes. + // still emits DeviceOnline/DeviceOffline when its reachability actually changes. obj->m_is_online = true; //load access code diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 7a52c648ef..6bd690e9a6 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -2657,11 +2657,10 @@ int MachineObject::connect(bool use_openssl) int MachineObject::disconnect() { if (m_agent) { - LifecycleEventContext ctx; - ctx.name = dev_id; - ctx.code = LifecycleEvtCode::Ok; - fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); - return m_agent->disconnect_printer(); + const int result = m_agent->disconnect_printer(); + if (result == 0) + set_online_state(false); + return result; } return -1; } @@ -2699,7 +2698,7 @@ void MachineObject::set_online_state(bool on_off) ctx.name = dev_id; ctx.code = LifecycleEvtCode::Ok; ctx.msg = on_off ? "online" : "offline"; - fire_lifecycle_event(LifecycleEvent::DeviceOnlineChanged, ctx); + fire_lifecycle_event(on_off ? LifecycleEvent::DeviceOnline : LifecycleEvent::DeviceOffline, ctx); } } @@ -2827,13 +2826,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ parse_msg_count++; std::chrono::system_clock::time_point clock_start = std::chrono::system_clock::now(); - this->set_online_state(true); - - std::chrono::system_clock::time_point curr_time = std::chrono::system_clock::now(); - auto diff1 = std::chrono::duration_cast(curr_time - last_update_time); - - /* update last received time */ - last_update_time = std::chrono::system_clock::now(); json j_pre; bool parse_ok = false; @@ -2846,8 +2838,29 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ /* post process payload */ sanitizeToUtf8(payload); BOOST_LOG_TRIVIAL(info) << "parse_json: sanitize to utf8"; + try { + j_pre = json::parse(payload); + parse_ok = true; + } + catch (...) {} } + bool client_disconnected = false; + if (parse_ok && j_pre.is_object() && j_pre.contains("event") && j_pre["event"].is_object() && + j_pre["event"].contains("event") && j_pre["event"]["event"].is_string()) { + client_disconnected = j_pre["event"]["event"].get() == "client.disconnected"; + } + + // A disconnect notification is a transport message too, but it must not first mark an + // already-offline device as online through the generic message-received path. + set_online_state(!client_disconnected); + + std::chrono::system_clock::time_point curr_time = std::chrono::system_clock::now(); + auto diff1 = std::chrono::duration_cast(curr_time - last_update_time); + + /* update last received time */ + last_update_time = std::chrono::system_clock::now(); + try { bool restored_json = false; json j; @@ -4618,24 +4631,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ } } - // event info - try { - if (j.contains("event")) { - if (j["event"].contains("event")) { - if (j["event"]["event"].get() == "client.disconnected") { - set_online_state(false); - LifecycleEventContext ctx; - ctx.name = dev_id; - ctx.code = LifecycleEvtCode::Ok; - fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); - } - else if (j["event"]["event"].get() == "client.connected") - set_online_state(true); - } - } - } - catch (...) {} - if (!key_field_only) { BOOST_LOG_TRIVIAL(trace) << "parse_json m_active_state =" << m_active_state; parse_state_changed_event(); diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 914c8f7868..ca3b7c3c44 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -897,6 +897,7 @@ public: bool is_connected(); bool is_connecting(); + // Emits DeviceOnline or DeviceOffline only when the reachability state changes. void set_online_state(bool on_off); bool is_online() { return m_is_online; } bool is_info_ready(bool check_version = true) const; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 5303502b69..29bab18aeb 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2183,10 +2183,7 @@ void GUI_App::init_networking_callbacks() if (m_agent) m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer()); - LifecycleEventContext ctx; - ctx.name = obj->get_dev_id(); - ctx.code = LifecycleEvtCode::Ok; - fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx); + obj->set_online_state(true); } }); }); @@ -2226,10 +2223,7 @@ void GUI_App::init_networking_callbacks() event.SetInt(0); event.SetString(obj->get_dev_id()); - LifecycleEventContext ctx; - ctx.name = obj->get_dev_id(); - ctx.code = LifecycleEvtCode::Ok; - fire_lifecycle_event(LifecycleEvent::DeviceConnected, ctx); + obj->set_online_state(true); } else if (state == ConnectStatus::ConnectStatusFailed) { // Orca: only update status if same device id if (m_device_manager->selected_machine != dev_id) return; @@ -2246,21 +2240,13 @@ void GUI_App::init_networking_callbacks() } event.SetInt(-1); - { - LifecycleEventContext ctx; - ctx.name = dev_id; - ctx.code = LifecycleEvtCode::Ok; - fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); - } + obj->set_online_state(false); } else if (state == ConnectStatus::ConnectStatusLost) { m_device_manager->set_selected_machine(""); event.SetInt(-1); BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost"; - LifecycleEventContext ctx; - ctx.name = dev_id; - ctx.code = LifecycleEvtCode::Ok; - fire_lifecycle_event(LifecycleEvent::DeviceDisconnected, ctx); + obj->set_online_state(false); } else { event.SetInt(-1); BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state; diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index c2587ceb56..a88695005c 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -401,11 +401,10 @@ void bind_python_api(pybind11::module_& m) .value("PresetSelected", LifecycleEvent::PresetSelected) .value("PresetSaved", LifecycleEvent::PresetSaved) .value("PrintStateChanged", LifecycleEvent::PrintStateChanged) - .value("DeviceOnlineChanged", LifecycleEvent::DeviceOnlineChanged) + .value("DeviceOnline", LifecycleEvent::DeviceOnline) + .value("DeviceOffline", LifecycleEvent::DeviceOffline) .value("DeviceDiscovered", LifecycleEvent::DeviceDiscovered) .value("DeviceSelected", LifecycleEvent::DeviceSelected) - .value("DeviceConnected", LifecycleEvent::DeviceConnected) - .value("DeviceDisconnected", LifecycleEvent::DeviceDisconnected) .value("UploadStarted", LifecycleEvent::UploadStarted) .value("UploadFinished", LifecycleEvent::UploadFinished) .value("PrintJobStarted", LifecycleEvent::PrintJobStarted) From 9851874742ee5891be50d3ef932bb2be826102a5 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Wed, 23 Sep 2026 03:12:14 +0800 Subject: [PATCH 13/19] fix: fire PrintJobStarted only after preflight validation --- src/slic3r/GUI/Jobs/PrintJob.cpp | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index d11756c0e2..76e45eb1d7 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -134,13 +134,6 @@ wxString PrintJob::get_http_error_msg(unsigned int status, std::string body) void PrintJob::process(Ctl &ctl) { - LifecycleEventContext start_ctx; - start_ctx.name = m_project_name; - start_ctx.device_id = m_dev_id; - start_ctx.source = "print_job"; - fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); - m_lifecycle_started = true; - /* display info */ std::string msg; int curr_percent = 10; @@ -160,6 +153,18 @@ void PrintJob::process(Ctl &ctl) int result = -1; std::string http_body; + const auto mark_lifecycle_started = [this]() { + if (m_lifecycle_started) + return; + + LifecycleEventContext start_ctx; + start_ctx.name = m_project_name; + start_ctx.device_id = m_dev_id; + start_ctx.source = "print_job"; + fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); + m_lifecycle_started = true; + }; + int total_plate_num = plate_data.plate_count; if (!plate_data.is_valid) { total_plate_num = m_plater->get_partplate_list().get_plate_count(); @@ -551,6 +556,7 @@ void PrintJob::process(Ctl &ctl) if (m_print_type == "from_sdcard_view") { BOOST_LOG_TRIVIAL(info) << "print_job: try to send with cloud, model is sdcard view"; ctl.update_status(curr_percent, _u8L("Sending print job through cloud service")); + mark_lifecycle_started(); result = m_agent->start_sdcard_print(params, update_fn, cancel_fn); } else if (params.connection_type != "lan") { if (params.dev_ip.empty()) @@ -574,6 +580,7 @@ void PrintJob::process(Ctl &ctl) BOOST_LOG_TRIVIAL(info) << "print_job: use ftp send print only"; ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); is_try_lan_mode = true; + mark_lifecycle_started(); result = m_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn); if (result < 0) { error_text = wxString::Format(_L("Access code:%s IP address:%s"), params.password, params.dev_ip); @@ -590,6 +597,7 @@ void PrintJob::process(Ctl &ctl) // try to send local with record BOOST_LOG_TRIVIAL(info) << "print_job: try to start local print with record"; ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); + mark_lifecycle_started(); result = m_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn); if (result == 0) { params.comments = ""; @@ -605,18 +613,21 @@ void PrintJob::process(Ctl &ctl) // try to send with cloud BOOST_LOG_TRIVIAL(warning) << "print_job: try to send with cloud"; ctl.update_status(curr_percent, _u8L("Sending print job through cloud service")); + // Started was already emitted before the local attempt. result = m_agent->start_print(params, update_fn, cancel_fn, wait_fn); } } else { BOOST_LOG_TRIVIAL(info) << "print_job: send with cloud"; ctl.update_status(curr_percent, _u8L("Sending print job through cloud service")); + mark_lifecycle_started(); result = m_agent->start_print(params, update_fn, cancel_fn, wait_fn); } } } else { if (this->could_emmc_print) { ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); + mark_lifecycle_started(); result = m_agent->start_local_print(params, update_fn, cancel_fn); } else { switch(this->sdcard_state) { @@ -627,6 +638,7 @@ void PrintJob::process(Ctl &ctl) if(this->has_sdcard) { // means the storage is abnormal but can be used option is enabled ctl.update_status(curr_percent, _u8L("Sending print job over LAN, but the Storage in the printer is abnormal and print-issues may be caused by this.")); + mark_lifecycle_started(); result = m_agent->start_local_print(params, update_fn, cancel_fn); break; } @@ -637,6 +649,7 @@ void PrintJob::process(Ctl &ctl) return; case DevStorage::SdcardState::HAS_SDCARD_NORMAL: ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); + mark_lifecycle_started(); result = m_agent->start_local_print(params, update_fn, cancel_fn); break; default: From d820303a3f1b4c68b9e59634638e2ca2a3db67a8 Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 22 Sep 2026 20:50:18 -0300 Subject: [PATCH 14/19] Improve preview colors (#15809) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- resources/shaders/110/ssao.fs | 82 +++++++----- resources/shaders/140/ssao.fs | 130 ++++++++++--------- src/libslic3r/AppConfig.cpp | 3 + src/libslic3r/AppConfig.hpp | 1 + src/libvgcode/include/Viewer.hpp | 16 +++ src/libvgcode/src/Shaders.hpp | 68 +++++++++- src/libvgcode/src/ShadersES.hpp | 66 +++++++++- src/libvgcode/src/Viewer.cpp | 15 +++ src/libvgcode/src/ViewerImpl.cpp | 48 ++++++- src/libvgcode/src/ViewerImpl.hpp | 46 +++++++ src/slic3r/GUI/GCodeViewer.cpp | 23 ++++ src/slic3r/GUI/GCodeViewer.hpp | 7 ++ src/slic3r/GUI/GLCanvas3D.cpp | 206 +++++++++++++++---------------- src/slic3r/GUI/GLCanvas3D.hpp | 1 + src/slic3r/GUI/Preferences.cpp | 9 ++ 15 files changed, 512 insertions(+), 209 deletions(-) diff --git a/resources/shaders/110/ssao.fs b/resources/shaders/110/ssao.fs index 5aada05883..db266daba1 100644 --- a/resources/shaders/110/ssao.fs +++ b/resources/shaders/110/ssao.fs @@ -7,18 +7,38 @@ uniform sampler2D color_texture; uniform sampler2D depth_texture; -uniform sampler2D normal_texture; uniform vec2 inv_tex_size; -uniform float z_near; uniform float z_far; uniform bool is_outline; +// The pass has no normal target to read, so the surface normal is reconstructed from the depth +// buffer. inv_projection_matrix unprojects a pixel back into view space and up_view is world +Z +// expressed in view space, which is what tells a top surface from a wall. +uniform mat4 inv_projection_matrix; +uniform vec3 up_view; varying vec2 tex_coord; -float linearize_depth(float depth) +// Position of the given pixel in view space. Valid under both an orthographic and a perspective +// camera, unlike the depth linearization it replaces. +vec3 view_pos(vec2 uv) { - float z = depth * 2.0 - 1.0; - return (2.0 * z_near * z_far) / (z_far + z_near - z * (z_far - z_near)); + vec2 c = clamp(uv, vec2(0.0), vec2(1.0)); + float d = texture2D(depth_texture, c).r; + vec4 ndc = vec4(c * 2.0 - 1.0, d * 2.0 - 1.0, 1.0); + vec4 view = inv_projection_matrix * ndc; + return view.xyz / view.w; +} + +// Surface normal at the given pixel, from the forward differences of the reconstructed view +// position. It rings by a pixel across a depth discontinuity, which is acceptable here: the +// normal only weights the occlusion, nothing is shaded with it. +vec3 view_normal(vec2 uv, vec3 p) +{ + vec3 px = view_pos(uv + vec2(inv_tex_size.x, 0.0)); + vec3 py = view_pos(uv + vec2(0.0, inv_tex_size.y)); + vec3 n = cross(px - p, py - p); + float len = length(n); + return (len > 1e-8) ? n / len : vec3(0.0, 0.0, 1.0); } void main() @@ -28,16 +48,21 @@ void main() return; } vec3 base = texture2D(color_texture, tex_coord).rgb; - float depth_center = linearize_depth(texture2D(depth_texture, tex_coord).r); - // Sample normal at current fragment (range: -1 to 1) - vec3 normal_center = texture2D(normal_texture, tex_coord).rgb * 2.0 - 1.0; + // Nothing was drawn here: occluding the background would only darken the gradient, and its + // reconstructed normal is degenerate anyway. + if (texture2D(depth_texture, tex_coord).r >= 0.9999) { + gl_FragColor = vec4(base, 1.0); + return; + } + + vec3 center_pos = view_pos(tex_coord); + float depth_center = -center_pos.z; + vec3 normal_center = view_normal(tex_coord, center_pos); // Calculate how much the surface faces upward - // up_factor = 1.0 for surfaces pointing straight up (0,0,1) - // up_factor = 0.0 for surfaces pointing down or sideways - float up_factor = max(0.0, normal_center.z); // Assuming Z is up axis - // Alternative: if Y is up, use normal_center.y + // up_factor = 1.0 for surfaces pointing straight up, 0.0 for walls and downward faces + float up_factor = clamp(dot(normal_center, up_view), 0.0, 1.0); // Adaptive sampling radius float radius = mix(2.0, 4.0, depth_center / z_far); @@ -52,39 +77,38 @@ void main() offsets[6] = vec2( 0.0, -1.0); offsets[7] = vec2( 0.707,-0.707); + // Occlusion is a slope, not a depth difference: how far a neighbour rises out of the + // centre's tangent plane over how far away it is. Unlike a raw difference, that sine is + // free of camera distance and zoom, so a crease reads the same from any view. + const float SLOPE_MIN = 0.08; // ~5 degrees, above the depth-buffer noise of a flat surface + const float SLOPE_MAX = 0.60; // ~37 degrees, a full crease + + const float SAMPLE_COUNT = 8.0; float occlusion = 0.0; - int valid_samples = 0; for (int i = 0; i < 8; ++i) { vec2 uv = tex_coord + offsets[i] * inv_tex_size * radius; - uv = clamp(uv, vec2(0.001), vec2(0.999)); - float sample_depth = linearize_depth(texture2D(depth_texture, uv).r); - float depth_diff = max(0.0, depth_center - sample_depth); - - float threshold = 0.015 * (0.5 + depth_center / z_far); - float contribution = smoothstep(0.001, threshold, depth_diff); + vec3 delta = view_pos(uv) - center_pos; + float dist = length(delta); + float rise = (dist > 1e-6) ? dot(delta, normal_center) / dist : 0.0; + float contribution = smoothstep(SLOPE_MIN, SLOPE_MAX, rise); float diagonal_weight = 1.0 - abs(offsets[i].x * offsets[i].y) * 0.5; occlusion += contribution * diagonal_weight; - valid_samples++; } - if (valid_samples > 0) - occlusion /= float(valid_samples); + occlusion /= SAMPLE_COUNT; // flatter/top-like surfaces get less darkening float ao_intensity = 0.55; float ambient_occlusion = 1.0 - occlusion * ao_intensity; - // Different min values for top vs bottom surfaces + // Different min values for top vs bottom surfaces. The boost that used to follow lifted a + // top surface back to within 2% of unoccluded once up_factor became a real normal rather + // than a colour, which is where the AO went; the floors alone shape the effect now. float ao_min = mix(0.45, 0.70, up_factor); // Bottom: 0.45, Top: 0.70 ambient_occlusion = clamp(ambient_occlusion, ao_min, 1.0); - // Boost brightness on top surfaces (optional) - float brightness_boost = 1.0 + up_factor * 0.15; // 15% extra brightness on top - ambient_occlusion = pow(ambient_occlusion, 2.2) * brightness_boost; - ambient_occlusion = clamp(ambient_occlusion, 0.45, 1.05); - gl_FragColor = vec4(base * ambient_occlusion, 1.0); -} \ No newline at end of file +} diff --git a/resources/shaders/140/ssao.fs b/resources/shaders/140/ssao.fs index b3b6df6877..28b561ba2c 100644 --- a/resources/shaders/140/ssao.fs +++ b/resources/shaders/140/ssao.fs @@ -1,24 +1,45 @@ #version 140 /** - * SSAO Shader - GLSL 140 version with sharp depth threshold + * SSAO Shader - GLSL 140 version with a slope-based occlusion test * Only darkens valleys/concave areas, ignores smooth variations */ uniform sampler2D color_texture; uniform sampler2D depth_texture; -uniform sampler2D normal_texture; -uniform float z_near; +uniform vec2 inv_tex_size; uniform float z_far; uniform bool is_outline; +// The pass has no normal target to read, so the surface normal is reconstructed from the depth +// buffer. inv_projection_matrix unprojects a pixel back into view space and up_view is world +Z +// expressed in view space, which is what tells a top surface from a wall. +uniform mat4 inv_projection_matrix; +uniform vec3 up_view; in vec2 tex_coord; out vec4 frag_color; -float linearize_depth(float depth) +// Position of the given pixel in view space. Valid under both an orthographic and a perspective +// camera, unlike the depth linearization it replaces. +vec3 view_pos(ivec2 pixel) { - float z = depth * 2.0 - 1.0; - return (2.0 * z_near * z_far) / (z_far + z_near - z * (z_far - z_near)); + ivec2 p = clamp(pixel, ivec2(0), textureSize(depth_texture, 0) - 1); + float d = texelFetch(depth_texture, p, 0).r; + vec4 ndc = vec4((vec2(p) + 0.5) * inv_tex_size * 2.0 - 1.0, d * 2.0 - 1.0, 1.0); + vec4 view = inv_projection_matrix * ndc; + return view.xyz / view.w; +} + +// Surface normal at the given pixel, from the forward differences of the reconstructed view +// position. It rings by a pixel across a depth discontinuity, which is acceptable here: the +// normal only weights the occlusion, nothing is shaded with it. +vec3 view_normal(ivec2 pixel, vec3 p) +{ + vec3 px = view_pos(pixel + ivec2(1, 0)); + vec3 py = view_pos(pixel + ivec2(0, 1)); + vec3 n = cross(px - p, py - p); + float len = length(n); + return (len > 1e-8) ? n / len : vec3(0.0, 0.0, 1.0); } void main() @@ -28,81 +49,72 @@ void main() return; } ivec2 pixel = ivec2(gl_FragCoord.xy); - float center_depth = linearize_depth(texelFetch(depth_texture, pixel, 0).r); - - // Sample normal buffer (stored as RGB in 0-1 range, convert to -1 to 1) - vec3 normal_center = texelFetch(normal_texture, pixel, 0).rgb * 2.0 - 1.0; - normal_center = normalize(normal_center); - + vec3 color = texture(color_texture, tex_coord).rgb; + + // Nothing was drawn here: occluding the background would only darken the gradient, and its + // reconstructed normal is degenerate anyway. + if (texelFetch(depth_texture, pixel, 0).r >= 0.9999) { + frag_color = vec4(color, 1.0); + return; + } + + vec3 center_pos = view_pos(pixel); + float center_depth = -center_pos.z; + vec3 normal_center = view_normal(pixel, center_pos); + // Calculate upward-facing factor (Z-up coordinate system) - float up_factor = clamp(normal_center.z * 1.5, 0.0, 1.0); - + float up_factor = clamp(dot(normal_center, up_view), 0.0, 1.0); + // Adaptive radius in pixel space int radius = int(mix(2.0, 4.0, center_depth / z_far)); // Optimized sampling pattern - const ivec2 offsets[12] = ivec2[]( + const int SAMPLE_COUNT = 12; + const ivec2 offsets[SAMPLE_COUNT] = ivec2[]( ivec2(1, 0), ivec2(-1, 0), ivec2(0, 1), ivec2(0, -1), ivec2(1, 1), ivec2(-1, 1), ivec2(1, -1), ivec2(-1, -1), ivec2(2, 0), ivec2(-2, 0), ivec2(0, 2), ivec2(0, -2) ); - float occlusion = 0.0; - int valid_samples = 0; + // Occlusion is a slope, not a depth difference: the sine of the angle a neighbour subtends + // above the centre's tangent plane. A raw difference depends on camera distance and zoom, + // so no fixed thresholds suit both a 0.2 mm layer step and a 5 mm overhang. + const float SLOPE_MIN = 0.08; // ~5 degrees, above the depth-buffer noise of a flat surface + const float SLOPE_MAX = 0.60; // ~37 degrees, a full crease - for (int i = 0; i < 12; i++) { + float occlusion = 0.0; + + for (int i = 0; i < SAMPLE_COUNT; i++) { + // No edge rejection: view_pos clamps, giving a near-zero delta and no occlusion. + // Rejecting one side only would bias the denominator against the other. ivec2 sample_pixel = pixel + offsets[i] * radius; - if (sample_pixel.x < 0 || sample_pixel.y < 0) - continue; - - float sample_depth = linearize_depth(texelFetch(depth_texture, sample_pixel, 0).r); - - // Sample normal at neighbor - vec3 normal_sample = texelFetch(normal_texture, sample_pixel, 0).rgb * 2.0 - 1.0; - - // Depth difference (positive if neighbor is closer to camera) - float depth_diff = center_depth - sample_depth; - - // Sharp depth threshold === - // Minimum depth difference to consider occlusion (ignores small variations) - float threshold_min = 0.008; // Higher = only deep valleys get darkened - float threshold_max = 0.04; // Transition range for full occlusion + vec3 delta = view_pos(sample_pixel) - center_pos; + float dist = length(delta); + // How far the neighbour rises towards the viewer out of the centre's tangent plane. A + // flat surface gives ~0 whatever its orientation, so this also subsumes the separate + // planar test the normals were compared for. + float rise = (dist > 1e-6) ? dot(delta, normal_center) / dist : 0.0; float contribution = 0.0; - if (depth_diff > threshold_min) { + if (rise > SLOPE_MIN) { // Abrupt mapping with power curve - contribution = (depth_diff - threshold_min) / (threshold_max - threshold_min); + contribution = (rise - SLOPE_MIN) / (SLOPE_MAX - SLOPE_MIN); contribution = clamp(contribution, 0.0, 1.0); contribution = pow(contribution, 2.0); // Steeper curve for sharper transition } - // Reduce occlusion on planar surfaces (similar normals) - float normal_similarity = dot(normal_center, normal_sample); - float planar_factor = smoothstep(0.75, 0.95, normal_similarity); - contribution *= (1.0 - planar_factor * 0.6); - occlusion += contribution; - valid_samples++; } - if (valid_samples > 0) { - // Calculate ambient occlusion factor with higher base intensity - float ao_factor = 1.0 - (occlusion / float(valid_samples)) * 0.6; - - // Keep bright areas clean (higher minimum for upward-facing surfaces) - float ao_min = mix(0.55, 0.85, up_factor); - ao_factor = clamp(ao_factor, ao_min, 1.0); - - // Slight brightness boost for upward-facing surfaces - float brightness_boost = 1.0 + up_factor * 0.15; - ao_factor = ao_factor * brightness_boost; - - occlusion = ao_factor; - } else { - occlusion = 1.0; - } + // Calculate ambient occlusion factor with higher base intensity + float ao_factor = 1.0 - (occlusion / float(SAMPLE_COUNT)) * 0.6; + + // Keep bright areas clean (higher minimum for upward-facing surfaces). The old 0.85 floor + // and 1.15 boost were set when up_factor came from the colour buffer and read ~0; with a + // real normal they capped a top surface at 2% darkening, which hid the AO entirely. + float ao_min = mix(0.45, 0.70, up_factor); + occlusion = clamp(ao_factor, ao_min, 1.0); - vec3 color = texture(color_texture, tex_coord).rgb; frag_color = vec4(color * occlusion, 1.0); -} \ No newline at end of file +} diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index c8a2c4984d..ff6444d3ef 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -303,6 +303,9 @@ void AppConfig::set_defaults() if (get(SETTING_OPENGL_REALISTIC_PHONG).empty()) set_bool(SETTING_OPENGL_REALISTIC_PHONG, true); + if (get(SETTING_OPENGL_REALISTIC_PREVIEW).empty()) + set_bool(SETTING_OPENGL_REALISTIC_PREVIEW, false); + if (get(SETTING_OPENGL_SHADING_MODEL).empty()) set(SETTING_OPENGL_SHADING_MODEL, "gouraud"); diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 24a4c2069b..2eac9f5fc6 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -42,6 +42,7 @@ using namespace nlohmann; #define SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS "opengl_phong_basic_plate_shadows" #define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao" #define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals" +#define SETTING_OPENGL_REALISTIC_PREVIEW "opengl_realistic_preview" #define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count" #define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1 diff --git a/src/libvgcode/include/Viewer.hpp b/src/libvgcode/include/Viewer.hpp index 5141245d96..179105032d 100644 --- a/src/libvgcode/include/Viewer.hpp +++ b/src/libvgcode/include/Viewer.hpp @@ -60,6 +60,22 @@ public: // using the given camera matrices. // void render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix); + // + // ORCA: realistic view. Render the toolpaths as seen from the light, to fill the caller's + // shadow map. Depth only - the caller masks colour writes and owns the framebuffer. + // + void render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position); + // + // ORCA: realistic view. The shadow map the toolpaths sample, in the given texture unit. + // intensity == 0, the default, turns the lookup off and restores the plain shading. + // + void set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size); + // + // ORCA: tone applied to the shaded toolpaths, to pay back the light the lighting term, + // the shadow and the SSAO pass each take off. 1.0/1.0, the default, is a no-op; the + // caller decides which of the two it varies with the realistic view setting. + // + void set_tone(float exposure, float saturation); // // ************************************************************************ diff --git a/src/libvgcode/src/Shaders.hpp b/src/libvgcode/src/Shaders.hpp index 1450d1a6df..596b42df5c 100644 --- a/src/libvgcode/src/Shaders.hpp +++ b/src/libvgcode/src/Shaders.hpp @@ -16,7 +16,8 @@ static const char* Segments_Vertex_Shader = "#define FIX_TWISTING\n" "const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);\n" "const float light_top_diffuse = 0.6 * 0.8;\n" -"const float light_top_specular = 0.6 * 0.125;\n" +// ORCA: the specular was 0.6 * 0.125, too faint to give the filament any sheen. +"const float light_top_specular = 0.6 * 0.25;\n" "const float light_top_shininess = 20.0;\n" "const vec3 light_front_dir = vec3(0.6985074, 0.1397015, 0.6985074);\n" "const float light_front_diffuse = 0.6 * 0.2;\n" @@ -30,8 +31,19 @@ static const char* Segments_Vertex_Shader = "uniform samplerBuffer height_width_angle_tex;\n" "uniform samplerBuffer color_tex;\n" "uniform usamplerBuffer segment_index_tex;\n" +// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not +// world_position, so the caster would write a depth the receiver never looks up. +"uniform float bias_scale;\n" "in int vertex_id;\n" "out vec3 color;\n" +"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n" +"// ambient and emissive terms in color, which a shadow does not occlude. Their sum is the\n" +"// single lighting term this replaces, so shading is unchanged while shadows are off.\n" +"out vec3 color_direct;\n" +"// ORCA: realistic view - the fragment shader looks the fragment up in the shadow map, which\n" +"// needs its world position and, for the depth bias, its eye space normal.\n" +"out vec3 world_position;\n" +"out vec3 shadow_normal;\n" "vec3 decode_color(float color) {\n" " int c = int(round(color));\n" " int r = (c >> 16) & 0xFF;\n" @@ -40,11 +52,11 @@ static const char* Segments_Vertex_Shader = " float f = 1.0 / 255.0f;\n" " return f * vec3(r, g, b);\n" "}\n" -"float lighting(vec3 eye_position, vec3 eye_normal) {\n" +"float direct_lighting(vec3 eye_position, vec3 eye_normal) {\n" " float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n" " float front_diffuse = light_front_diffuse * max(dot(eye_normal, light_front_dir), 0.0);\n" " float top_specular = light_top_specular * pow(max(dot(-normalize(eye_position), reflect(-light_top_dir, eye_normal)), 0.0), light_top_shininess);\n" -" return ambient + top_diffuse + front_diffuse + top_specular + emission;\n" +" return top_diffuse + front_diffuse + top_specular;\n" "}\n" "void main() {\n" " int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n" @@ -135,19 +147,63 @@ static const char* Segments_Vertex_Shader = " }\n" " vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n" " // ORCA: Apply bias to z-position to avoid z-fighting\n" -" eye_position.z += bias;\n" +" eye_position.z += bias * bias_scale;\n" " vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n" " vec3 color_base = decode_color(texelFetch(color_tex, id).r);\n" -" color = color_base * lighting(eye_position, eye_normal);\n" +" color = color_base * (ambient + emission);\n" +" color_direct = color_base * direct_lighting(eye_position, eye_normal);\n" +" world_position = pos;\n" +" shadow_normal = eye_normal;\n" " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; static const char* Segments_Fragment_Shader = "#version 150\n" +"// ORCA: realistic view - object-on-object and self shadows, read from the same depth map the\n" +"// rest of the 3D scene samples. shadow_intensity == 0, the default, short-circuits the lookup,\n" +"// so the toolpaths shade exactly as before whenever realistic view is off.\n" +"const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);\n" +"uniform sampler2D shadow_map;\n" +"uniform mat4 shadow_light_vp;\n" +"uniform float shadow_intensity;\n" +"uniform float shadow_map_texel;\n" +// ORCA: the lighting term peaks near 0.9 and every later multiplier - the shadow, then the SSAO +// post pass - only takes more light away, so the print reads dimmer and duller than the legend +// colours. These pay that back. Both are 1.0 for an untouched image; what the caller actually +// passes in each mode is decided in GLCanvas3D::_render_gcode, not here. +"uniform float exposure;\n" +"uniform float saturation;\n" +"const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n" "in vec3 color;\n" +"in vec3 color_direct;\n" +"in vec3 world_position;\n" +"in vec3 shadow_normal;\n" "out vec4 fragment_color;\n" +"float shadow_shade() {\n" +" if (shadow_intensity <= 0.0)\n" +" return 1.0;\n" +" vec4 lp = shadow_light_vp * vec4(world_position, 1.0);\n" +" vec3 proj = lp.xyz / lp.w;\n" +" proj = proj * 0.5 + 0.5;\n" +" if (proj.z > 1.0)\n" +" return 1.0;\n" +" // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n" +" // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n" +" float NdotL = dot(normalize(shadow_normal), SHADOW_LIGHT_DIR);\n" +" float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n" +" float sum = 0.0;\n" +" for (int x = -2; x <= 2; ++x) {\n" +" for (int y = -2; y <= 2; ++y) {\n" +" float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;\n" +" sum += (proj.z - bias > closest) ? 1.0 : 0.0;\n" +" }\n" +" }\n" +" return 1.0 - shadow_intensity * (sum / 25.0);\n" +"}\n" "void main() {\n" -" fragment_color = vec4(color, 1.0);\n" +" vec3 c = (color + color_direct * shadow_shade()) * exposure;\n" +" c = mix(vec3(dot(c, LUMA)), c, saturation);\n" +" fragment_color = vec4(clamp(c, 0.0, 1.0), 1.0);\n" "}\n"; static const char* Options_Vertex_Shader = diff --git a/src/libvgcode/src/ShadersES.hpp b/src/libvgcode/src/ShadersES.hpp index e688fa7b4c..4194b836e3 100644 --- a/src/libvgcode/src/ShadersES.hpp +++ b/src/libvgcode/src/ShadersES.hpp @@ -17,7 +17,8 @@ static const char* Segments_Vertex_Shader_ES = "#define FIX_TWISTING\n" "const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);\n" "const float light_top_diffuse = 0.6 * 0.8;\n" -"const float light_top_specular = 0.6 * 0.125;\n" +// ORCA: the specular was 0.6 * 0.125, too faint to give the filament any sheen. +"const float light_top_specular = 0.6 * 0.25;\n" "const float light_top_shininess = 20.0;\n" "const vec3 light_front_dir = vec3(0.6985074, 0.1397015, 0.6985074);\n" "const float light_front_diffuse = 0.6 * 0.3;\n" @@ -33,6 +34,14 @@ static const char* Segments_Vertex_Shader_ES = "uniform usampler2D segment_index_tex;\n" "in float vertex_id_float;\n" "out vec3 color;\n" +"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n" +"// ambient and emissive terms in color, which a shadow does not occlude. Their sum is the\n" +"// single lighting term this replaces, so shading is unchanged while shadows are off.\n" +"out vec3 color_direct;\n" +"// ORCA: realistic view - the fragment shader looks the fragment up in the shadow map, which\n" +"// needs its world position and, for the depth bias, its eye space normal.\n" +"out vec3 world_position;\n" +"out vec3 shadow_normal;\n" "vec3 decode_color(float color) {\n" " int c = int(round(color));\n" " int r = (c >> 16) & 0xFF;\n" @@ -41,11 +50,11 @@ static const char* Segments_Vertex_Shader_ES = " float f = 1.0 / 255.0f;\n" " return f * vec3(r, g, b);\n" "}\n" -"float lighting(vec3 eye_position, vec3 eye_normal) {\n" +"float direct_lighting(vec3 eye_position, vec3 eye_normal) {\n" " float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n" " float front_diffuse = light_front_diffuse * max(dot(eye_normal, light_front_dir), 0.0);\n" " float top_specular = light_top_specular * pow(max(dot(-normalize(eye_position), reflect(-light_top_dir, eye_normal)), 0.0), light_top_shininess);\n" -" return ambient + top_diffuse + front_diffuse + top_specular + emission;\n" +" return top_diffuse + front_diffuse + top_specular;\n" "}\n" "ivec2 tex_coord(sampler2D sampler, int id) {\n" " ivec2 tex_size = textureSize(sampler, 0);\n" @@ -143,17 +152,64 @@ static const char* Segments_Vertex_Shader_ES = " vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n" " vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n" " vec3 color_base = decode_color(texelFetch(color_tex, tex_coord(color_tex, id), 0).r);\n" -" color = color_base * lighting(eye_position, eye_normal);\n" +" color = color_base * (ambient + emission);\n" +" color_direct = color_base * direct_lighting(eye_position, eye_normal);\n" +" world_position = pos;\n" +" shadow_normal = eye_normal;\n" " gl_Position = projection_matrix * vec4(eye_position, 1.0);\n" "}\n"; static const char* Segments_Fragment_Shader_ES = "#version 300 es\n" "precision highp float;\n" +"// ORCA: sampler2D defaults to lowp in an ES fragment shader, far too coarse to compare\n" +"// shadow map depths against.\n" +"precision highp sampler2D;\n" +"// ORCA: realistic view - object-on-object and self shadows, read from the same depth map the\n" +"// rest of the 3D scene samples. shadow_intensity == 0, the default, short-circuits the lookup,\n" +"// so the toolpaths shade exactly as before whenever realistic view is off.\n" +"const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);\n" +"uniform sampler2D shadow_map;\n" +"uniform mat4 shadow_light_vp;\n" +"uniform float shadow_intensity;\n" +"uniform float shadow_map_texel;\n" +// ORCA: the lighting term peaks near 0.9 and every later multiplier - the shadow, then the SSAO +// post pass - only takes more light away, so the print reads dimmer and duller than the legend +// colours. These pay that back. Both are 1.0 for an untouched image; what the caller actually +// passes in each mode is decided in GLCanvas3D::_render_gcode, not here. +"uniform float exposure;\n" +"uniform float saturation;\n" +"const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n" "in vec3 color;\n" +"in vec3 color_direct;\n" +"in vec3 world_position;\n" +"in vec3 shadow_normal;\n" "out vec4 fragment_color;\n" +"float shadow_shade() {\n" +" if (shadow_intensity <= 0.0)\n" +" return 1.0;\n" +" vec4 lp = shadow_light_vp * vec4(world_position, 1.0);\n" +" vec3 proj = lp.xyz / lp.w;\n" +" proj = proj * 0.5 + 0.5;\n" +" if (proj.z > 1.0)\n" +" return 1.0;\n" +" // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n" +" // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n" +" float NdotL = dot(normalize(shadow_normal), SHADOW_LIGHT_DIR);\n" +" float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n" +" float sum = 0.0;\n" +" for (int x = -2; x <= 2; ++x) {\n" +" for (int y = -2; y <= 2; ++y) {\n" +" float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;\n" +" sum += (proj.z - bias > closest) ? 1.0 : 0.0;\n" +" }\n" +" }\n" +" return 1.0 - shadow_intensity * (sum / 25.0);\n" +"}\n" "void main() {\n" -" fragment_color = vec4(color, 1.0);\n" +" vec3 c = (color + color_direct * shadow_shade()) * exposure;\n" +" c = mix(vec3(dot(c, LUMA)), c, saturation);\n" +" fragment_color = vec4(clamp(c, 0.0, 1.0), 1.0);\n" "}\n"; static const char* Options_Vertex_Shader_ES = diff --git a/src/libvgcode/src/Viewer.cpp b/src/libvgcode/src/Viewer.cpp index eb606598e9..61d7310508 100644 --- a/src/libvgcode/src/Viewer.cpp +++ b/src/libvgcode/src/Viewer.cpp @@ -42,6 +42,21 @@ void Viewer::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix) m_impl->render(view_matrix, projection_matrix); } +void Viewer::render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position) +{ + m_impl->render_shadow_casters(view_matrix, projection_matrix, light_position); +} + +void Viewer::set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size) +{ + m_impl->set_shadow_map(texture_unit, light_view_projection, intensity, texel_size); +} + +void Viewer::set_tone(float exposure, float saturation) +{ + m_impl->set_tone(exposure, saturation); +} + EViewType Viewer::get_view_type() const { return m_impl->get_view_type(); diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index da1a601149..10fda297d4 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -763,6 +763,14 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex"); m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex"); m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex"); + // ORCA: realistic view + m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map"); + m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp"); + m_uni_segments_shadow_intensity_id = glGetUniformLocation(m_segments_shader_id, "shadow_intensity"); + m_uni_segments_shadow_map_texel_id = glGetUniformLocation(m_segments_shader_id, "shadow_map_texel"); + m_uni_segments_exposure_id = glGetUniformLocation(m_segments_shader_id, "exposure"); + m_uni_segments_saturation_id = glGetUniformLocation(m_segments_shader_id, "saturation"); + m_uni_segments_bias_scale_id = glGetUniformLocation(m_segments_shader_id, "bias_scale"); glcheck(); assert(m_uni_segments_view_matrix_id != -1 && m_uni_segments_projection_matrix_id != -1 && @@ -1321,7 +1329,7 @@ void ViewerImpl::update_colors() m_settings.update_colors = false; } -void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix) +void ViewerImpl::apply_pending_updates() { if (m_settings.update_view_full_range) update_view_full_range(); @@ -1331,6 +1339,11 @@ void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matr if (m_settings.update_colors) update_colors(); +} + +void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix) +{ + apply_pending_updates(); const Mat4x4 inv_view_matrix = inverse(view_matrix); const Vec3 camera_position = { inv_view_matrix[12], inv_view_matrix[13], inv_view_matrix[14] }; @@ -1345,6 +1358,30 @@ void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matr #endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS } +void ViewerImpl::render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position) +{ + apply_pending_updates(); + + // Only the extrusions and travels cast: the option markers are indicators, not material. + m_rendering_shadow_casters = true; + render_segments(view_matrix, projection_matrix, light_position); + m_rendering_shadow_casters = false; +} + +void ViewerImpl::set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size) +{ + m_shadow_map_texture_unit = texture_unit; + m_shadow_light_vp = light_view_projection; + m_shadow_intensity = intensity; + m_shadow_map_texel = texel_size; +} + +void ViewerImpl::set_tone(float exposure, float saturation) +{ + m_exposure = exposure; + m_saturation = saturation; +} + void ViewerImpl::set_view_type(EViewType type) { m_settings.view_type = type; @@ -1994,6 +2031,15 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data())); glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data())); glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data())); + // ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades + // with the lookup off. + glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit)); + glsafe(glUniformMatrix4fv(m_uni_segments_shadow_light_vp_id, 1, GL_FALSE, m_shadow_light_vp.data())); + glsafe(glUniform1f(m_uni_segments_shadow_intensity_id, m_rendering_shadow_casters ? 0.0f : m_shadow_intensity)); + glsafe(glUniform1f(m_uni_segments_shadow_map_texel_id, m_shadow_map_texel)); + glsafe(glUniform1f(m_uni_segments_exposure_id, m_exposure)); + glsafe(glUniform1f(m_uni_segments_saturation_id, m_saturation)); + glsafe(glUniform1f(m_uni_segments_bias_scale_id, m_rendering_shadow_casters ? 0.0f : 1.0f)); glsafe(glDisable(GL_CULL_FACE)); diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index 4da312fc0e..9231cbdefc 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -71,6 +71,24 @@ public: // Render the toolpaths // void render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix); + // + // ORCA: realistic view. Render the toolpaths as seen from the light, to fill the caller's + // shadow map. Only depth matters here, so the caller masks colour writes; light_position + // takes the place of the camera when the segment boxes are expanded, which gives their + // silhouette as the light sees it. + // + void render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position); + // + // ORCA: realistic view. The shadow map the toolpaths sample, in the given texture unit. + // intensity == 0, the default, turns the lookup off and restores the plain shading. + // + void set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size); + // + // ORCA: tone applied to the shaded toolpaths, to pay back the light the lighting term, + // the shadow and the SSAO pass each take off. 1.0/1.0, the default, is a no-op; the + // caller decides which of the two it varies with the realistic view setting. + // + void set_tone(float exposure, float saturation); EViewType get_view_type() const { return m_settings.view_type; } void set_view_type(EViewType type); @@ -330,6 +348,13 @@ private: int m_uni_segments_height_width_angle_tex_id{ -1 }; int m_uni_segments_colors_tex_id{ -1 }; int m_uni_segments_segment_index_tex_id{ -1 }; + int m_uni_segments_shadow_map_id{ -1 }; + int m_uni_segments_shadow_light_vp_id{ -1 }; + int m_uni_segments_shadow_intensity_id{ -1 }; + int m_uni_segments_shadow_map_texel_id{ -1 }; + int m_uni_segments_exposure_id{ -1 }; + int m_uni_segments_saturation_id{ -1 }; + int m_uni_segments_bias_scale_id{ -1 }; // // Caches for OpenGL uniforms id for options shader // @@ -469,6 +494,27 @@ private: size_t m_enabled_options_tex_size{ 0 }; #endif // ENABLE_OPENGL_ES + // + // ORCA: realistic view. Shadow map state set by set_shadow_map(), consumed by the segments + // shader. m_rendering_shadow_casters forces the intensity to 0 for the depth pass, which + // must not sample the very map it is writing. + // + // Defaults past the four texture units render_segments() binds itself, so the sampler never + // aliases one of the buffer textures before the owner of the map has said where it lives. + int m_shadow_map_texture_unit{ 4 }; + Mat4x4 m_shadow_light_vp{ 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; + float m_shadow_intensity{ 0.0f }; + float m_shadow_map_texel{ 0.0f }; + bool m_rendering_shadow_casters{ false }; + + // + // ORCA: realistic view. Tone set by set_tone(), consumed by the segments shader. + // The identity values leave the shading as it is outside realistic view. + // + float m_exposure{ 1.0f }; + float m_saturation{ 1.0f }; + + void apply_pending_updates(); void update_view_full_range(); void update_color_ranges(); void update_heights_widths(); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 3b3354d86f..2070b988d5 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1641,6 +1641,29 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height) m_sequential_view.render_marker(!m_no_render_path, canvas_width, sequential_view_height(canvas_height), m_viewer.get_view_type()); } +void GCodeViewer::render_shadow_casters(const Transform3d& light_view_matrix, const Transform3d& light_projection_matrix, const Vec3d& light_position) +{ + if (!has_data()) + return; + + m_viewer.render_shadow_casters( + libvgcode::convert(static_cast(light_view_matrix.matrix().cast())), + libvgcode::convert(static_cast(light_projection_matrix.matrix().cast())), + libvgcode::convert(static_cast(light_position.cast()))); +} + +void GCodeViewer::set_shadow_map(int texture_unit, const Transform3d& light_view_projection, float intensity, float texel_size) +{ + m_viewer.set_shadow_map(texture_unit, + libvgcode::convert(static_cast(light_view_projection.matrix().cast())), + intensity, texel_size); +} + +void GCodeViewer::set_tone(float exposure, float saturation) +{ + m_viewer.set_tone(exposure, saturation); +} + void GCodeViewer::render_overlay(int canvas_width, int canvas_height, int right_margin) { if (m_viewer.get_extrusion_roles().empty()) diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 9b82a81b2f..2b4d4e2334 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -279,6 +279,13 @@ public: void render_scene(int canvas_width, int canvas_height); // Legend, sliders, the marker's position window and the G-code window, all ImGui. void render_overlay(int canvas_width, int canvas_height, int right_margin); + // ORCA: realistic view. Depth-only pass drawing the toolpaths as the light sees them, into + // the shadow map the caller has bound, and the map they sample back in render_scene. + void render_shadow_casters(const Transform3d& light_view_matrix, const Transform3d& light_projection_matrix, const Vec3d& light_position); + void set_shadow_map(int texture_unit, const Transform3d& light_view_projection, float intensity, float texel_size); + // ORCA: tone applied to the shaded toolpaths, paying back the light the lighting term, + // the shadow and the SSAO pass each take off. 1.0/1.0 is a no-op. + void set_tone(float exposure, float saturation); //BBS // void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); // void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 190eeedbb4..b76c7f346c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2199,8 +2199,8 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) // Recorded by PartPlate::render_icons() below, when it runs. wxGetApp().plater()->get_partplate_list().clear_hover_tooltip(); glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); - // Invalidate the shadow map each frame; only the View3D path below rebuilds it. This keeps - // the Preview / Assemble canvases from sampling a stale map with an outdated light matrix. + // Invalidate the shadow map each frame; the View3D and Preview paths below rebuild it. This + // keeps the Assemble canvas from sampling a stale map with an outdated light matrix. m_shadow_map_valid = false; _render_background(); @@ -2251,6 +2251,8 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) _render_selection(); _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes); _render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, true, hover_id); + // Realistic view: the print casts a shadow onto the plate here as it does in View3D. + _render_shadows(camera.get_view_matrix(), camera.get_projection_matrix()); // BBS: GUI refactor: add canvas size as parameters _render_gcode(cnv_size.get_width(), cnv_size.get_height()); } @@ -7644,11 +7646,20 @@ bool GLCanvas3D::_is_fxaa_enabled() const return wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_FXAA_ENABLED); } +bool GLCanvas3D::_is_realistic_view_enabled() const +{ + const AppConfig* cfg = wxGetApp().app_config; + if (cfg == nullptr || !cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE)) + return false; + // Prepare and Assemble follow the umbrella toggle alone; Preview needs its own opt-in. + return m_canvas_type != ECanvasType::CanvasPreview || cfg->get_bool(SETTING_OPENGL_REALISTIC_PREVIEW); +} + bool GLCanvas3D::_is_ssao_enabled() const { if (wxGetApp().app_config == nullptr) return false; - return wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE) && + return _is_realistic_view_enabled() && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SSAO); } @@ -7803,95 +7814,23 @@ void GLCanvas3D::_render_ssao_pass(unsigned int width, unsigned int height) const Camera& camera = wxGetApp().plater()->get_camera(); - GLint prev_stencil_mask = 0xFF; - glsafe(::glGetIntegerv(GL_STENCIL_WRITEMASK, &prev_stencil_mask)); - GLboolean prev_stencil_test = GL_FALSE; - glsafe(::glGetBooleanv(GL_STENCIL_TEST, &prev_stencil_test)); - GLboolean prev_depth_mask = GL_TRUE; - glsafe(::glGetBooleanv(GL_DEPTH_WRITEMASK, &prev_depth_mask)); - GLint prev_depth_func = GL_LESS; - glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &prev_depth_func)); - glsafe(::glDisable(GL_DEPTH_TEST)); glsafe(::glDisable(GL_BLEND)); - // Build stencil mask for bed/plate and apply SSAO only outside this mask. - glsafe(::glEnable(GL_STENCIL_TEST)); - glsafe(::glStencilMask(0xFF)); - glsafe(::glClearStencil(0)); - glsafe(::glClear(GL_STENCIL_BUFFER_BIT)); - glsafe(::glStencilFunc(GL_ALWAYS, 1, 0xFF)); - glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)); - // Mark only visible plate pixels (do not exclude objects in front of plate). - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glDepthMask(GL_FALSE)); - glsafe(::glDepthFunc(GL_LEQUAL)); - - GLboolean prev_color_mask[4] = { GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE }; - glsafe(::glGetBooleanv(GL_COLOR_WRITEMASK, prev_color_mask)); - glsafe(::glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE)); - - if (const BuildVolume& build_volume = m_bed.build_volume(); build_volume.valid()) { - GLShaderProgram* flat = wxGetApp().get_shader("flat"); - if (flat != nullptr) { - flat->start_using(); - flat->set_uniform("projection_matrix", camera.get_projection_matrix()); - - GLModel plate_mask; - GLModel::Geometry mask; - mask.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - - if (build_volume.type() == BuildVolume_Type::Rectangle) { - const BoundingBox3Base bb = build_volume.bounding_volume(); - mask.reserve_vertices(4); - mask.reserve_indices(6); - mask.add_vertex(Vec3f((float)bb.min.x(), (float)bb.min.y(), 0.0f)); - mask.add_vertex(Vec3f((float)bb.max.x(), (float)bb.min.y(), 0.0f)); - mask.add_vertex(Vec3f((float)bb.max.x(), (float)bb.max.y(), 0.0f)); - mask.add_vertex(Vec3f((float)bb.min.x(), (float)bb.max.y(), 0.0f)); - mask.add_triangle(0, 1, 2); - mask.add_triangle(0, 2, 3); - } else if (build_volume.type() == BuildVolume_Type::Circle) { - const Vec2f c = Vec2f(unscaled(build_volume.circle().center.x()), unscaled(build_volume.circle().center.y())); - const float r = unscaled(build_volume.circle().radius); - const int segments = 64; - mask.reserve_vertices(segments + 1); - mask.reserve_indices(segments * 3); - mask.add_vertex(Vec3f(c.x(), c.y(), 0.0f)); - for (int i = 0; i < segments; ++i) { - const float a = (2.0f * float(PI) * float(i)) / float(segments); - mask.add_vertex(Vec3f(c.x() + r * std::cos(a), c.y() + r * std::sin(a), 0.0f)); - } - for (int i = 0; i < segments; ++i) { - const unsigned int i1 = 1 + i; - const unsigned int i2 = 1 + ((i + 1) % segments); - mask.add_triangle(0, i1, i2); - } - } - - if (mask.vertices_count() > 0 && mask.indices_count() > 0) { - plate_mask.init_from(std::move(mask)); - flat->set_uniform("view_model_matrix", camera.get_view_matrix()); - plate_mask.render(flat); - } - flat->stop_using(); - } - } - - glsafe(::glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE)); - glsafe(::glDisable(GL_DEPTH_TEST)); - glsafe(::glStencilMask(0x00)); - glsafe(::glStencilFunc(GL_NOTEQUAL, 1, 0xFF)); - glsafe(::glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP)); - shader->start_using(); shader->set_uniform("view_model_matrix", Transform3d::Identity()); shader->set_uniform("projection_matrix", Transform3d::Identity()); shader->set_uniform("color_texture", 0); shader->set_uniform("depth_texture", 1); shader->set_uniform("inv_tex_size", Vec2f(1.0f / static_cast(width), 1.0f / static_cast(height))); - shader->set_uniform("z_near", camera.get_near_z()); shader->set_uniform("z_far", camera.get_far_z()); + // The shader reconstructs the surface normal from the depth buffer, there being no normal + // target to read: it unprojects a pixel back into view space, then measures the result + // against world +Z expressed in view space to tell a top surface from a wall. + const Matrix4d inv_projection_matrix = camera.get_projection_matrix().matrix().inverse(); + shader->set_uniform("inv_projection_matrix", inv_projection_matrix); + const Vec3d up_view = (camera.get_view_matrix().matrix().block<3, 3>(0, 0) * Vec3d::UnitZ()).normalized(); + shader->set_uniform("up_view", up_view); glsafe(::glActiveTexture(GL_TEXTURE0)); glsafe(::glBindTexture(GL_TEXTURE_2D, m_ssao_color_texture_id)); @@ -7903,13 +7842,6 @@ void GLCanvas3D::_render_ssao_pass(unsigned int width, unsigned int height) glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); shader->stop_using(); - if (!prev_stencil_test) - glsafe(::glDisable(GL_STENCIL_TEST)); - glsafe(::glStencilMask(prev_stencil_mask)); - glsafe(::glColorMask(prev_color_mask[0], prev_color_mask[1], prev_color_mask[2], prev_color_mask[3])); - - glsafe(::glDepthMask(prev_depth_mask)); - glsafe(::glDepthFunc(prev_depth_func)); glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); @@ -8154,15 +8086,19 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform { if (wxGetApp().app_config == nullptr) return; - if (!wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE)) + if (!_is_realistic_view_enabled()) return; if (!wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS)) return; - if (m_volumes.empty()) - return; - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) + // The preview canvas holds no volumes of its own for FFF. Once slicing has run its printed + // geometry is the G-code toolpaths, which both cast into the map here and sample it back in + // _render_gcode; before slicing there are only shells, and nothing casts at all. View3D and + // SLA preview use m_volumes. The shells are deliberately never casters: they are a + // translucent ghost of the whole object, so they would drop the solid shadow of a print that + // has not been sliced, and at any layer below the last, one that is not there yet. + const bool toolpath_casters = m_canvas_type == ECanvasType::CanvasPreview && m_gcode_viewer.has_data(); + if (!toolpath_casters && m_volumes.empty()) return; if (OpenGLManager::get_framebuffers_type() == OpenGLManager::EFramebufferType::Arb) { @@ -8174,10 +8110,30 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform // Bounding box of the printable objects (the shadow casters). BoundingBoxf3 obj_bb; - for (const GLVolume* volume : m_volumes.volumes) { - if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) - continue; - obj_bb.merge(volume->transformed_bounding_box()); + if (toolpath_casters) { + // Merged corner by corner: BoundingBoxf3(min, max) marks itself undefined at zero + // Z extent, which a single layer print gives, and the check below would then drop + // every shadow in the frame. + const BoundingBoxf3& paths_bb = m_gcode_viewer.get_paths_bounding_box(); + if ((paths_bb.min.array() <= paths_bb.max.array()).all()) { + obj_bb.merge(paths_bb.min); + obj_bb.merge(paths_bb.max); + } + // Only the enabled layers are drawn, so fitting the map to the whole print wastes + // its depth range and makes contact shadows shift as the slider moves. The z = 0 + // shadow is enclosed separately below, so the plate shadow is unaffected. + const std::vector layer_zs = m_gcode_viewer.get_layers_zs(); + if (!layer_zs.empty()) { + const size_t top = std::min(m_gcode_viewer.get_layers_z_range()[1], layer_zs.size() - 1); + obj_bb.max.z() = std::max(obj_bb.min.z(), std::min(obj_bb.max.z(), layer_zs[top])); + } + } + else { + for (const GLVolume* volume : m_volumes.volumes) { + if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) + continue; + obj_bb.merge(volume->transformed_bounding_box()); + } } if (!obj_bb.defined) return; // no objects to cast shadows @@ -8299,16 +8255,21 @@ void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform glsafe(::glPolygonOffset(4.0f, 4.0f)); glsafe(::glDisable(GL_CULL_FACE)); - shader->start_using(); - shader->set_uniform("projection_matrix", Transform3d(light_proj)); - for (GLVolume* volume : m_volumes.volumes) { - if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) - continue; - const Transform3d view_model = Transform3d(light_view) * volume->world_matrix(); - shader->set_uniform("view_model_matrix", view_model); - volume->model.render(shader); + if (toolpath_casters) + m_gcode_viewer.render_shadow_casters(Transform3d(light_view), Transform3d(light_proj), eye); + // Only this branch draws through "flat"; the toolpaths bring their own program. + else if (GLShaderProgram* shader = wxGetApp().get_shader("flat"); shader != nullptr) { + shader->start_using(); + shader->set_uniform("projection_matrix", Transform3d(light_proj)); + for (GLVolume* volume : m_volumes.volumes) { + if (volume == nullptr || !volume->is_active || !volume->printable || volume->is_modifier || volume->is_wipe_tower) + continue; + const Transform3d view_model = Transform3d(light_view) * volume->world_matrix(); + shader->set_uniform("view_model_matrix", view_model); + volume->model.render(shader); + } + shader->stop_using(); } - shader->stop_using(); // Restore state glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); @@ -8517,7 +8478,7 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with return; } - const bool realistic_mode = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_MODE); + const bool realistic_mode = _is_realistic_view_enabled(); const bool realistic_phong = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_REALISTIC_PHONG); const std::string shader_name = (realistic_mode && realistic_phong) ? "phong" : "gouraud"; GLShaderProgram* shader = wxGetApp().get_shader(shader_name); @@ -8741,7 +8702,34 @@ void GLCanvas3D::_render_wireframe_overlay() //BBS: GUI refactor: add canvas size as parameters void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height) { + // Realistic view: the toolpaths receive the same depth map they were rendered into by + // _render_shadows, which is what gives them object-on-object and self shadows. Intensity 0 + // short-circuits the lookup in the shader, so this is inert whenever the map is missing. + const bool receive_shadows = m_shadow_map_valid && m_shadow_map_texture_id != 0 && m_shadow_map_size != 0; + if (receive_shadows) { + glsafe(::glActiveTexture(GL_TEXTURE4)); + glsafe(::glBindTexture(GL_TEXTURE_2D, m_shadow_map_texture_id)); + glsafe(::glActiveTexture(GL_TEXTURE0)); + m_gcode_viewer.set_shadow_map(4, m_shadow_light_vp, 0.35f, 1.0f / static_cast(m_shadow_map_size)); + } + else + m_gcode_viewer.set_shadow_map(4, Transform3d::Identity(), 0.0f, 0.0f); + + // The lighting term leaves the print dimmer and duller than the legend colours. Saturation + // pays back the duller half in both modes; brightness only where something takes light off + // again - realistic view with at least one lossy pass on - else the lift would just clip. + const AppConfig* cfg = wxGetApp().app_config; + const bool lossy_passes = cfg != nullptr && _is_realistic_view_enabled() && + (cfg->get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS) || cfg->get_bool(SETTING_OPENGL_PHONG_SSAO)); + m_gcode_viewer.set_tone(lossy_passes ? 1.1f : 1.0f, 1.15f); + m_gcode_viewer.render_scene(canvas_width, canvas_height); + + if (receive_shadows) { + glsafe(::glActiveTexture(GL_TEXTURE4)); + glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); + glsafe(::glActiveTexture(GL_TEXTURE0)); + } } void GLCanvas3D::_render_gcode_overlay(int canvas_width, int canvas_height) @@ -9831,7 +9819,7 @@ void GLCanvas3D::_render_canvas_toolbar() ); create_menu_item( _utf8(L("Realistic View")), - m_canvas_type != ECanvasType::CanvasPreview, // not work on preview + true, // work on all cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE), [&cfg]{ cfg->set_bool(SETTING_OPENGL_REALISTIC_MODE, !cfg->get_bool(SETTING_OPENGL_REALISTIC_MODE)); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index ad5188f2f3..75820694b0 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -1324,6 +1324,7 @@ private: void _picking_pass(); void _rectangular_selection_picking_pass(); bool _is_fxaa_enabled() const; + bool _is_realistic_view_enabled() const; bool _is_ssao_enabled() const; int _get_effective_fps_cap() const; bool _is_fps_overlay_enabled() const; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index be4bfcfa63..3a7f37e9cb 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1932,6 +1932,15 @@ void PreferencesDialog::create_items() ); g_sizer->Add(item_realistic_phong); + auto item_realistic_preview = create_item_checkbox( + _L("Enable in Preview"), + _L("Also applies realistic view to the Preview canvas, not just Prepare.\n" + "Preview draws the full toolpath geometry, so shadows and SSAO cost considerably" + " more there than on a plain model."), + SETTING_OPENGL_REALISTIC_PREVIEW + ); + g_sizer->Add(item_realistic_preview); + auto item_realistic_ssao = create_item_checkbox( _L("SSAO ambient occlusion"), _L("Applies SSAO in realistic view."), From e71497738f4cb4f48f3b0ae88bcb1b5566964a9f Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 22 Sep 2026 20:50:59 -0300 Subject: [PATCH 15/19] Configurable default G-code preview view type (#15769) --- src/libslic3r/AppConfig.cpp | 6 ++ src/slic3r/GUI/GCodeViewer.cpp | 123 ++++++++++++++++++++++++++------- src/slic3r/GUI/GCodeViewer.hpp | 13 +++- src/slic3r/GUI/Preferences.cpp | 17 +++++ 4 files changed, 134 insertions(+), 25 deletions(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index ff6444d3ef..cdcd9b4dec 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -224,6 +224,12 @@ void AppConfig::set_defaults() set("preview_dim_previous_layers_brightness", std::to_string(std::max(0, std::min(brightness, 99)))); } + // ORCA: view type the G-code preview opens with. "auto" keeps the automatic choice (Filament for + // multi material prints, Line Type for single material ones), "last" restores the view type the user + // picked last, any other value is a fixed view type name, see GCodeViewer::view_type_to_config_name(). + if (get("preview_default_view_type").empty()) + set("preview_default_view_type", "auto"); + if (get("filaments_area_preferred_count").empty()) set("filaments_area_preferred_count", "10"); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 2070b988d5..b86af5c259 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -104,6 +104,31 @@ static std::string get_view_type_string(libvgcode::EViewType view_type) return ""; } +// ORCA: Stable, locale independent names used to persist a view type in the application config. +// Keep these in sync with the entries of libvgcode::EViewType exposed in the preview combo box. +static const std::vector>& view_type_config_map() +{ + static const std::vector> map = { + { "summary", libvgcode::EViewType::Summary }, + { "feature_type", libvgcode::EViewType::FeatureType }, + { "color_print", libvgcode::EViewType::ColorPrint }, + { "speed", libvgcode::EViewType::Speed }, + { "actual_speed", libvgcode::EViewType::ActualSpeed }, + { "acceleration", libvgcode::EViewType::Acceleration }, + { "jerk", libvgcode::EViewType::Jerk }, + { "height", libvgcode::EViewType::Height }, + { "width", libvgcode::EViewType::Width }, + { "volumetric_flow_rate", libvgcode::EViewType::VolumetricFlowRate }, + { "actual_volumetric_flow_rate", libvgcode::EViewType::ActualVolumetricFlowRate }, + { "layer_time_linear", libvgcode::EViewType::LayerTimeLinear }, + { "layer_time_logarithmic", libvgcode::EViewType::LayerTimeLogarithmic }, + { "fan_speed", libvgcode::EViewType::FanSpeed }, + { "temperature", libvgcode::EViewType::Temperature }, + { "pressure_advance", libvgcode::EViewType::PressureAdvance }, + }; + return map; +} + // Find an index of a value in a sorted vector, which is in . // Returns -1 if there is no such member. static int find_close_layer_idx(const std::vector &zs, double &z, double eps) @@ -1091,9 +1116,7 @@ void GCodeViewer::init(ConfigOptionMode mode, PresetBundle* preset_bundle) // Default view type at first slice. // May be overridden in load() once we know how many tools are actually used in the G-code. m_nozzle_nums = preset_bundle ? preset_bundle->get_printer_extruder_count() : 1; - auto it = std::find(view_type_items.begin(), view_type_items.end(), libvgcode::EViewType::FeatureType); - m_view_type_sel = (it != view_type_items.end()) ? std::distance(view_type_items.begin(), it) : 0; - set_view_type(libvgcode::EViewType::FeatureType); + apply_default_view_type(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished"); } @@ -1114,6 +1137,73 @@ void GCodeViewer::set_scale(float scale) } } +// ORCA: Preview default view type preference, see "preview_default_view_type" in the application config. +std::string GCodeViewer::view_type_to_config_name(libvgcode::EViewType type) +{ + for (const auto& [name, value] : view_type_config_map()) { + if (value == type) + return name; + } + return std::string(); +} + +bool GCodeViewer::view_type_from_config_name(const std::string& name, libvgcode::EViewType& type) +{ + for (const auto& [config_name, value] : view_type_config_map()) { + if (config_name == name) { + type = value; + return true; + } + } + return false; +} + +std::vector> GCodeViewer::default_view_type_choices() +{ + std::vector> choices = { + { "auto", _u8L("Automatic") }, + { "last", _u8L("Last used") }, + }; + for (const auto& [name, type] : view_type_config_map()) + choices.push_back({ name, get_view_type_string(type) }); + return choices; +} + +void GCodeViewer::select_view_type(libvgcode::EViewType type) +{ + auto it = std::find(view_type_items.begin(), view_type_items.end(), type); + m_view_type_sel = (it != view_type_items.end()) ? static_cast(std::distance(view_type_items.begin(), it)) : 0; + set_view_type(type); +} + +// ORCA: Pick the view type the preview opens with, following the "preview_default_view_type" preference: +// a fixed view type, the one the user picked last ("last"), or the automatic choice ("auto", the default) +// which shows Filament for multi material prints and Line Type for single material ones. +// The default is only (re)applied when it actually changes, so a view type picked by hand survives a reslice. +void GCodeViewer::apply_default_view_type() +{ + const std::string preference = wxGetApp().app_config->get("preview_default_view_type"); + + std::string key = preference; + libvgcode::EViewType type = libvgcode::EViewType::FeatureType; + if (preference == "last") { + if (!view_type_from_config_name(wxGetApp().app_config->get("preview_last_view_type"), type)) + type = libvgcode::EViewType::FeatureType; + } + else if (!view_type_from_config_name(preference, type)) { + // "auto", or an unknown value written by a newer version + const bool multi_material = m_viewer.get_used_extruders_count() > 1; + type = multi_material ? libvgcode::EViewType::ColorPrint : libvgcode::EViewType::FeatureType; + key = multi_material ? "auto_multi_material" : "auto_single_material"; + } + + if (m_applied_default_view_type_key == key) + return; + + m_applied_default_view_type_key = key; + select_view_type(type); +} + void GCodeViewer::update_by_mode(ConfigOptionMode mode) { view_type_items.clear(); @@ -1395,27 +1485,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // load_toolpaths(gcode_result, build_volume, exclude_bounding_box); - // ORCA: Apply smart default view type when extruder count changes. - // Multi-color: ColorPrint (Filament), Single-color: FeatureType (Line Type). - // User selections persist within same extruder count, defaults reapply on count change. - int current_count = m_viewer.get_used_extruders_count(); - if (current_count > 1) { - if (m_last_extruder_count_default_applied != 2) { - auto it = std::find(view_type_items.begin(), view_type_items.end(), libvgcode::EViewType::ColorPrint); - if (it != view_type_items.end()) - m_view_type_sel = std::distance(view_type_items.begin(), it); - set_view_type(libvgcode::EViewType::ColorPrint); - m_last_extruder_count_default_applied = 2; - } - } else { - if (m_last_extruder_count_default_applied != 1) { - auto it = std::find(view_type_items.begin(), view_type_items.end(), libvgcode::EViewType::FeatureType); - if (it != view_type_items.end()) - m_view_type_sel = std::distance(view_type_items.begin(), it); - set_view_type(libvgcode::EViewType::FeatureType); - m_last_extruder_count_default_applied = 1; - } - } + // ORCA: Apply the default view type now that we know how many tools the G-code actually uses. + apply_default_view_type(); // BBS: data for rendering color arrangement recommendation m_nozzle_nums = print.config().option("nozzle_diameter")->values.size(); @@ -3581,6 +3652,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv m_view_type_sel = i; set_view_type(view_type_items[m_view_type_sel]); reset_visible(view_type_items[m_view_type_sel]); + // ORCA: remember the pick so the "Last used" preview default can restore it + const std::string view_type_name = view_type_to_config_name(view_type_items[m_view_type_sel]); + if (!view_type_name.empty()) + wxGetApp().app_config->set("preview_last_view_type", view_type_name); update_moves_slider(); #if ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT imgui.set_requires_extra_frame(); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 2b4d4e2334..f4ebe2e81d 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -228,7 +228,8 @@ private: std::vector view_type_items; std::vector view_type_items_str; int m_view_type_sel = 0; - int m_last_extruder_count_default_applied{0}; // 0=unset, 1=single, 2+=multi + // ORCA: which default view type was last applied, see apply_default_view_type(). Empty until the first one is applied. + std::string m_applied_default_view_type_key; std::vector options_items; bool m_legend_visible{ true }; @@ -331,6 +332,16 @@ public: void set_view_type(libvgcode::EViewType type) { m_viewer.set_view_type(type); } + // ORCA: select a view type in the preview combo box and apply it + void select_view_type(libvgcode::EViewType type); + // ORCA: apply the "preview_default_view_type" preference, see the definition for the supported values + void apply_default_view_type(); + + // ORCA: stable, locale independent name of a view type, as stored in the application config + static std::string view_type_to_config_name(libvgcode::EViewType type); + static bool view_type_from_config_name(const std::string& name, libvgcode::EViewType& type); + // ORCA: (config value, translated label) pairs for the "preview_default_view_type" preference combo box + static std::vector> default_view_type_choices(); void reset_visible(libvgcode::EViewType type) { if (type == libvgcode::EViewType::FeatureType) { auto roles = m_viewer.get_extrusion_roles(); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 3a7f37e9cb..001866bfd9 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -2028,6 +2028,23 @@ void PreferencesDialog::create_items() //// GRAPHICS > G-code Preview g_sizer->Add(create_item_title(_L("G-code Preview")), 1, wxEXPAND); + // ORCA: view type the preview opens with + std::vector PreviewViewTypeLabels; + std::vector PreviewViewTypeValues; + for (const auto& [value, label] : GCodeViewer::default_view_type_choices()) { + PreviewViewTypeValues.push_back(value); + PreviewViewTypeLabels.push_back(from_u8(label)); + } + auto item_preview_view_type = create_item_combobox( + _L("Default view type"), + _L("The color scheme the sliced preview opens with.\n" + "Automatic: Filament for multi material prints, Line Type for single material ones.\n" + "Last used: the view type you selected last.\n" + "Any other value always opens that view type.\n" + "You can still switch the view type in the preview afterwards."), + "preview_default_view_type", PreviewViewTypeLabels, PreviewViewTypeValues); + g_sizer->Add(item_preview_view_type); + auto item_dim_previous_layers = create_item_checkbox( _L("Dim lower layers"), _L("When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness."), From 9454ca7454a03cddd80e4c4fb2334a978993cb35 Mon Sep 17 00:00:00 2001 From: Eric McCann Date: Tue, 22 Sep 2026 23:50:17 -0400 Subject: [PATCH 16/19] Add Prusa CORE One MMU3 profiles (#15757) * Add Prusa CORE One MMU3 profiles Dedicated MMU3 CoreOne profiles (like those used on prusaslicer 3). Tested with latest coreone and mmu3 firmware, and works as well as prusaslicer. Correct model default materials to reference compatible MMU3 filaments. Co-authored-by: Codex * Consolidate the CORE One MMU3 generic filaments The bundle shipped two families covering the same four MMU3 variants for each generic material: a standalone `Generic X @MMU3` and a `Prusa Generic X @CORE One MMU3`. The `Prusa Generic` spelling was renamed away from the rest of the tree, so re-name the CORE One-tuned family to `Generic X @Prusa CORE One MMU3` (they inherit the CORE One tune and now the shared generic product id) and drop the standalone files, which only carried raw material-base values. Repoint the model default_materials and re-register the index. * Fix the CORE One MMU3 0.4 default process default_print_profile named `0.20mm Speed @COREONE0.4 + MMU3`, but the preset is `0.20mm SPEED @COREONE0.4 + MMU3`. Preset lookup is case-sensitive, so the intended default never resolved and compatibility selection silently picked another tier. * Normalise the CORE One MMU3 process names Match the bundle's all-caps quality ladder: `Fast Detail` -> `FAST DETAIL`, `Speed` -> `SPEED`, `Structural` -> `STRUCTURAL`, `Balanced` -> `BALANCED`. Filenames now equal their preset name, as every pre-existing Prusa process file does, and the index is rebuilt for the renamed entries. --------- Co-authored-by: Codex Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com> Co-authored-by: SoftFever --- resources/profiles/Prusa.json | 198 +++++++++++++++++- .../Prusa/Prusa CORE One MMU3_cover.png | Bin 0 -> 43464 bytes .../Generic ABS @Prusa CORE One MMU3.json | 53 +++++ .../Generic ASA @Prusa CORE One MMU3.json | 53 +++++ .../Generic PETG @Prusa CORE One MMU3.json | 53 +++++ .../Generic PLA @Prusa CORE One MMU3.json | 53 +++++ ...Generic PLA Silk @Prusa CORE One MMU3.json | 53 +++++ .../Generic TPU @Prusa CORE One MMU3.json | 53 +++++ .../Prusament ASA @CORE One MMU3.json | 54 +++++ .../Prusament PA-CF @CORE One MMU3.json | 54 +++++ .../Prusament PC Blend @CORE One MMU3.json | 54 +++++ .../Prusament PC-CF @CORE One MMU3.json | 54 +++++ .../Prusament PETG @CORE One MMU3.json | 54 +++++ .../Prusament PLA @CORE One MMU3.json | 54 +++++ .../Prusament PVB @CORE One MMU3.json | 57 +++++ .../Prusament rPLA @CORE One MMU3.json | 54 +++++ .../Prusa CORE One MMU3 0.25 nozzle.json | 18 ++ .../Prusa CORE One MMU3 0.3 nozzle.json | 18 ++ .../Prusa CORE One MMU3 0.4 nozzle.json | 40 ++++ .../Prusa CORE One MMU3 0.6 nozzle.json | 19 ++ .../Prusa/machine/Prusa CORE One MMU3.json | 12 ++ .../0.05mm DETAIL @COREONE0.25 + MMU3.json | 23 ++ .../0.07mm DETAIL @COREONE0.25 + MMU3.json | 23 ++ ...0.10mm FAST DETAIL @COREONE0.4 + MMU3.json | 23 ++ .../0.12mm SPEED @COREONE0.25 + MMU3.json | 23 ++ ...0.12mm STRUCTURAL @COREONE0.25 + MMU3.json | 23 ++ .../0.12mm STRUCTURAL @COREONE0.3 + MMU3.json | 23 ++ .../0.15mm BALANCED @COREONE0.4 + MMU3.json | 70 +++++++ .../0.15mm SPEED @COREONE0.25 + MMU3.json | 23 ++ .../0.15mm SPEED @COREONE0.4 + MMU3.json | 23 ++ ...0.15mm STRUCTURAL @COREONE0.25 + MMU3.json | 23 ++ .../0.15mm STRUCTURAL @COREONE0.4 + MMU3.json | 23 ++ .../0.15mm STRUCTURAL @COREONE0.6 + MMU3.json | 23 ++ .../0.16mm SPEED @COREONE0.3 + MMU3.json | 23 ++ .../0.16mm STRUCTURAL @COREONE0.3 + MMU3.json | 23 ++ .../0.20mm BALANCED @COREONE0.4 + MMU3.json | 66 ++++++ ....20mm SOLUBLE FULL @COREONE0.4 + MMU3.json | 23 ++ ... SOLUBLE INTERFACE @COREONE0.4 + MMU3.json | 23 ++ .../0.20mm SPEED @COREONE0.3 + MMU3.json | 23 ++ .../0.20mm SPEED @COREONE0.4 + MMU3.json | 23 ++ .../0.20mm SPEED @COREONE0.6 + MMU3.json | 23 ++ .../0.20mm STRUCTURAL @COREONE0.3 + MMU3.json | 23 ++ .../0.20mm STRUCTURAL @COREONE0.4 + MMU3.json | 23 ++ .../0.20mm STRUCTURAL @COREONE0.6 + MMU3.json | 23 ++ .../0.25mm BALANCED @COREONE0.4 + MMU3.json | 68 ++++++ .../0.25mm SPEED @COREONE0.4 + MMU3.json | 65 ++++++ .../0.25mm SPEED @COREONE0.6 + MMU3.json | 23 ++ .../0.25mm STRUCTURAL @COREONE0.4 + MMU3.json | 72 +++++++ .../0.25mm STRUCTURAL @COREONE0.6 + MMU3.json | 23 ++ .../0.32mm SPEED @COREONE0.6 + MMU3.json | 23 ++ .../0.32mm STRUCTURAL @COREONE0.6 + MMU3.json | 23 ++ 51 files changed, 1973 insertions(+), 1 deletion(-) create mode 100644 resources/profiles/Prusa/Prusa CORE One MMU3_cover.png create mode 100644 resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Generic TPU @Prusa CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Prusament ASA @CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Prusament PA-CF @CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Prusament PC Blend @CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Prusament PC-CF @CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Prusament PETG @CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Prusament PLA @CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Prusament PVB @CORE One MMU3.json create mode 100644 resources/profiles/Prusa/filament/Prusament rPLA @CORE One MMU3.json create mode 100644 resources/profiles/Prusa/machine/Prusa CORE One MMU3 0.25 nozzle.json create mode 100644 resources/profiles/Prusa/machine/Prusa CORE One MMU3 0.3 nozzle.json create mode 100644 resources/profiles/Prusa/machine/Prusa CORE One MMU3 0.4 nozzle.json create mode 100644 resources/profiles/Prusa/machine/Prusa CORE One MMU3 0.6 nozzle.json create mode 100644 resources/profiles/Prusa/machine/Prusa CORE One MMU3.json create mode 100644 resources/profiles/Prusa/process/0.05mm DETAIL @COREONE0.25 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.07mm DETAIL @COREONE0.25 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.10mm FAST DETAIL @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.12mm SPEED @COREONE0.25 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.12mm STRUCTURAL @COREONE0.25 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.12mm STRUCTURAL @COREONE0.3 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.15mm BALANCED @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.15mm SPEED @COREONE0.25 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.15mm SPEED @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.25 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.6 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.16mm SPEED @COREONE0.3 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.16mm STRUCTURAL @COREONE0.3 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm BALANCED @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm SOLUBLE FULL @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm SOLUBLE INTERFACE @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.3 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.6 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.3 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.6 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.25mm BALANCED @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.25mm SPEED @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.25mm SPEED @COREONE0.6 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.25mm STRUCTURAL @COREONE0.4 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.25mm STRUCTURAL @COREONE0.6 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.32mm SPEED @COREONE0.6 + MMU3.json create mode 100644 resources/profiles/Prusa/process/0.32mm STRUCTURAL @COREONE0.6 + MMU3.json diff --git a/resources/profiles/Prusa.json b/resources/profiles/Prusa.json index dc69bbe57a..e16e2d3be3 100644 --- a/resources/profiles/Prusa.json +++ b/resources/profiles/Prusa.json @@ -1,6 +1,6 @@ { "name": "Prusa", - "version": "02.04.00.07", + "version": "02.04.00.08", "force_update": "0", "description": "Prusa configurations", "machine_model_list": [ @@ -20,6 +20,10 @@ "name": "Prusa CORE One L HF", "sub_path": "machine/Prusa CORE One L HF.json" }, + { + "name": "Prusa CORE One MMU3", + "sub_path": "machine/Prusa CORE One MMU3.json" + }, { "name": "Prusa MINI", "sub_path": "machine/Prusa MINI.json" @@ -702,6 +706,26 @@ "name": "0.25mm Speed @MINIIS", "sub_path": "process/0.25mm Speed @MINIIS.json" }, + { + "name": "0.15mm BALANCED @COREONE0.4 + MMU3", + "sub_path": "process/0.15mm BALANCED @COREONE0.4 + MMU3.json" + }, + { + "name": "0.20mm BALANCED @COREONE0.4 + MMU3", + "sub_path": "process/0.20mm BALANCED @COREONE0.4 + MMU3.json" + }, + { + "name": "0.25mm BALANCED @COREONE0.4 + MMU3", + "sub_path": "process/0.25mm BALANCED @COREONE0.4 + MMU3.json" + }, + { + "name": "0.25mm SPEED @COREONE0.4 + MMU3", + "sub_path": "process/0.25mm SPEED @COREONE0.4 + MMU3.json" + }, + { + "name": "0.25mm STRUCTURAL @COREONE0.4 + MMU3", + "sub_path": "process/0.25mm STRUCTURAL @COREONE0.4 + MMU3.json" + }, { "name": "0.05mm DETAIL @MK4S 0.25", "sub_path": "process/0.05mm DETAIL @MK4S 0.25.json" @@ -1114,6 +1138,14 @@ "name": "0.05mm DETAIL @CORE One L 0.25", "sub_path": "process/0.05mm DETAIL @CORE One L 0.25.json" }, + { + "name": "0.07mm DETAIL @COREONE0.25 + MMU3", + "sub_path": "process/0.07mm DETAIL @COREONE0.25 + MMU3.json" + }, + { + "name": "0.12mm SPEED @COREONE0.25 + MMU3", + "sub_path": "process/0.12mm SPEED @COREONE0.25 + MMU3.json" + }, { "name": "0.12mm STRUCTURAL @CORE One 0.25", "sub_path": "process/0.12mm STRUCTURAL @CORE One 0.25.json" @@ -1134,6 +1166,10 @@ "name": "0.15mm STRUCTURAL @MK4S 0.25", "sub_path": "process/0.15mm STRUCTURAL @MK4S 0.25.json" }, + { + "name": "0.12mm STRUCTURAL @COREONE0.3 + MMU3", + "sub_path": "process/0.12mm STRUCTURAL @COREONE0.3 + MMU3.json" + }, { "name": "0.16mm SPEED @MK4S 0.3", "sub_path": "process/0.16mm SPEED @MK4S 0.3.json" @@ -1158,6 +1194,10 @@ "name": "0.10mm FAST DETAIL @CORE One L 0.4", "sub_path": "process/0.10mm FAST DETAIL @CORE One L 0.4.json" }, + { + "name": "0.15mm SPEED @COREONE0.4 + MMU3", + "sub_path": "process/0.15mm SPEED @COREONE0.4 + MMU3.json" + }, { "name": "0.15mm SPEED @CORE One HF 0.4", "sub_path": "process/0.15mm SPEED @CORE One HF 0.4.json" @@ -1166,6 +1206,22 @@ "name": "0.15mm SPEED @CORE One L HF 0.4", "sub_path": "process/0.15mm SPEED @CORE One L HF 0.4.json" }, + { + "name": "0.15mm STRUCTURAL @COREONE0.6 + MMU3", + "sub_path": "process/0.15mm STRUCTURAL @COREONE0.6 + MMU3.json" + }, + { + "name": "0.20mm SOLUBLE FULL @COREONE0.4 + MMU3", + "sub_path": "process/0.20mm SOLUBLE FULL @COREONE0.4 + MMU3.json" + }, + { + "name": "0.20mm SOLUBLE INTERFACE @COREONE0.4 + MMU3", + "sub_path": "process/0.20mm SOLUBLE INTERFACE @COREONE0.4 + MMU3.json" + }, + { + "name": "0.20mm SPEED @COREONE0.4 + MMU3", + "sub_path": "process/0.20mm SPEED @COREONE0.4 + MMU3.json" + }, { "name": "0.20mm SPEED @CORE One HF 0.5", "sub_path": "process/0.20mm SPEED @CORE One HF 0.5.json" @@ -1198,6 +1254,10 @@ "name": "0.25mm SPEED @MK4S HF0.5", "sub_path": "process/0.25mm SPEED @MK4S HF0.5.json" }, + { + "name": "0.20mm SPEED @COREONE0.6 + MMU3", + "sub_path": "process/0.20mm SPEED @COREONE0.6 + MMU3.json" + }, { "name": "0.20mm SPEED @CORE One HF 0.6", "sub_path": "process/0.20mm SPEED @CORE One HF 0.6.json" @@ -1222,6 +1282,10 @@ "name": "0.15mm STRUCTURAL @CORE One L 0.4", "sub_path": "process/0.15mm STRUCTURAL @CORE One L 0.4.json" }, + { + "name": "0.20mm STRUCTURAL @COREONE0.4 + MMU3", + "sub_path": "process/0.20mm STRUCTURAL @COREONE0.4 + MMU3.json" + }, { "name": "0.25mm STRUCTURAL @CORE One HF 0.4", "sub_path": "process/0.25mm STRUCTURAL @CORE One HF 0.4.json" @@ -1230,6 +1294,10 @@ "name": "0.25mm STRUCTURAL @CORE One L HF 0.4", "sub_path": "process/0.25mm STRUCTURAL @CORE One L HF 0.4.json" }, + { + "name": "0.25mm SPEED @COREONE0.6 + MMU3", + "sub_path": "process/0.25mm SPEED @COREONE0.6 + MMU3.json" + }, { "name": "0.25mm SPEED @CORE One HF 0.6", "sub_path": "process/0.25mm SPEED @CORE One HF 0.6.json" @@ -1262,6 +1330,10 @@ "name": "0.30mm STRUCTURAL @CORE One L HF 0.8", "sub_path": "process/0.30mm STRUCTURAL @CORE One L HF 0.8.json" }, + { + "name": "0.32mm SPEED @COREONE0.6 + MMU3", + "sub_path": "process/0.32mm SPEED @COREONE0.6 + MMU3.json" + }, { "name": "0.32mm SPEED @CORE One HF 0.6", "sub_path": "process/0.32mm SPEED @CORE One HF 0.6.json" @@ -1318,6 +1390,18 @@ "name": "0.55mm SPEED @CORE One L HF 0.8", "sub_path": "process/0.55mm SPEED @CORE One L HF 0.8.json" }, + { + "name": "0.05mm DETAIL @COREONE0.25 + MMU3", + "sub_path": "process/0.05mm DETAIL @COREONE0.25 + MMU3.json" + }, + { + "name": "0.12mm STRUCTURAL @COREONE0.25 + MMU3", + "sub_path": "process/0.12mm STRUCTURAL @COREONE0.25 + MMU3.json" + }, + { + "name": "0.15mm SPEED @COREONE0.25 + MMU3", + "sub_path": "process/0.15mm SPEED @COREONE0.25 + MMU3.json" + }, { "name": "0.15mm STRUCTURAL @CORE One 0.25", "sub_path": "process/0.15mm STRUCTURAL @CORE One 0.25.json" @@ -1338,6 +1422,10 @@ "name": "0.20mm SPEED @MK4S 0.3", "sub_path": "process/0.20mm SPEED @MK4S 0.3.json" }, + { + "name": "0.16mm STRUCTURAL @COREONE0.3 + MMU3", + "sub_path": "process/0.16mm STRUCTURAL @COREONE0.3 + MMU3.json" + }, { "name": "0.20mm STRUCTURAL @CORE One 0.3", "sub_path": "process/0.20mm STRUCTURAL @CORE One 0.3.json" @@ -1346,6 +1434,10 @@ "name": "0.20mm STRUCTURAL @CORE One L 0.3", "sub_path": "process/0.20mm STRUCTURAL @CORE One L 0.3.json" }, + { + "name": "0.10mm FAST DETAIL @COREONE0.4 + MMU3", + "sub_path": "process/0.10mm FAST DETAIL @COREONE0.4 + MMU3.json" + }, { "name": "0.25mm STRUCTURAL @CORE One 0.5", "sub_path": "process/0.25mm STRUCTURAL @CORE One 0.5.json" @@ -1370,6 +1462,30 @@ "name": "0.32mm SPEED @MK4S HF0.5", "sub_path": "process/0.32mm SPEED @MK4S HF0.5.json" }, + { + "name": "0.20mm STRUCTURAL @COREONE0.6 + MMU3", + "sub_path": "process/0.20mm STRUCTURAL @COREONE0.6 + MMU3.json" + }, + { + "name": "0.15mm STRUCTURAL @COREONE0.4 + MMU3", + "sub_path": "process/0.15mm STRUCTURAL @COREONE0.4 + MMU3.json" + }, + { + "name": "0.25mm STRUCTURAL @COREONE0.6 + MMU3", + "sub_path": "process/0.25mm STRUCTURAL @COREONE0.6 + MMU3.json" + }, + { + "name": "0.32mm STRUCTURAL @COREONE0.6 + MMU3", + "sub_path": "process/0.32mm STRUCTURAL @COREONE0.6 + MMU3.json" + }, + { + "name": "0.15mm STRUCTURAL @COREONE0.25 + MMU3", + "sub_path": "process/0.15mm STRUCTURAL @COREONE0.25 + MMU3.json" + }, + { + "name": "0.16mm SPEED @COREONE0.3 + MMU3", + "sub_path": "process/0.16mm SPEED @COREONE0.3 + MMU3.json" + }, { "name": "0.20mm SPEED @CORE One 0.3", "sub_path": "process/0.20mm SPEED @CORE One 0.3.json" @@ -1378,6 +1494,10 @@ "name": "0.20mm SPEED @CORE One L 0.3", "sub_path": "process/0.20mm SPEED @CORE One L 0.3.json" }, + { + "name": "0.20mm STRUCTURAL @COREONE0.3 + MMU3", + "sub_path": "process/0.20mm STRUCTURAL @COREONE0.3 + MMU3.json" + }, { "name": "0.32mm STRUCTURAL @CORE One HF 0.5", "sub_path": "process/0.32mm STRUCTURAL @CORE One HF 0.5.json" @@ -1393,6 +1513,10 @@ { "name": "0.32mm SPEED @CORE One L HF 0.5", "sub_path": "process/0.32mm SPEED @CORE One L HF 0.5.json" + }, + { + "name": "0.20mm SPEED @COREONE0.3 + MMU3", + "sub_path": "process/0.20mm SPEED @COREONE0.3 + MMU3.json" } ], "filament_list": [ @@ -2032,6 +2156,10 @@ "name": "Generic ABS @Prusa CORE One HF 0.8", "sub_path": "filament/Generic ABS @Prusa CORE One HF 0.8.json" }, + { + "name": "Generic ABS @Prusa CORE One MMU3", + "sub_path": "filament/Generic ABS @Prusa CORE One MMU3.json" + }, { "name": "Generic ABS @Prusa MK4S 0.6", "sub_path": "filament/Generic ABS @Prusa MK4S 0.6.json" @@ -2112,6 +2240,10 @@ "name": "Generic ASA @Prusa CORE One HF 0.8", "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.8.json" }, + { + "name": "Generic ASA @Prusa CORE One MMU3", + "sub_path": "filament/Generic ASA @Prusa CORE One MMU3.json" + }, { "name": "Generic ASA @Prusa MK4S 0.6", "sub_path": "filament/Generic ASA @Prusa MK4S 0.6.json" @@ -2160,6 +2292,10 @@ "name": "Prusament ASA @CORE One HF 0.8", "sub_path": "filament/Prusament ASA @CORE One HF 0.8.json" }, + { + "name": "Prusament ASA @CORE One MMU3", + "sub_path": "filament/Prusament ASA @CORE One MMU3.json" + }, { "name": "Prusament ASA @MK4S 0.6", "sub_path": "filament/Prusament ASA @MK4S 0.6.json" @@ -2192,6 +2328,10 @@ "name": "Generic TPU @Prusa CORE One 0.8", "sub_path": "filament/Generic TPU @Prusa CORE One 0.8.json" }, + { + "name": "Generic TPU @Prusa CORE One MMU3", + "sub_path": "filament/Generic TPU @Prusa CORE One MMU3.json" + }, { "name": "Generic PA @Prusa", "sub_path": "filament/Generic PA @Prusa.json" @@ -2240,6 +2380,10 @@ "name": "Prusament PA-CF @CORE One 0.8", "sub_path": "filament/Prusament PA-CF @CORE One 0.8.json" }, + { + "name": "Prusament PA-CF @CORE One MMU3", + "sub_path": "filament/Prusament PA-CF @CORE One MMU3.json" + }, { "name": "Prusament PA11 Carbon Fiber @MK4S 0.6", "sub_path": "filament/Prusament PA11 Carbon Fiber @MK4S 0.6.json" @@ -2300,6 +2444,10 @@ "name": "Prusament PC Blend @CORE One HF 0.8", "sub_path": "filament/Prusament PC Blend @CORE One HF 0.8.json" }, + { + "name": "Prusament PC Blend @CORE One MMU3", + "sub_path": "filament/Prusament PC Blend @CORE One MMU3.json" + }, { "name": "Prusament PC Blend @MK4S 0.6", "sub_path": "filament/Prusament PC Blend @MK4S 0.6.json" @@ -2332,6 +2480,10 @@ "name": "Prusament PC-CF @CORE One 0.8", "sub_path": "filament/Prusament PC-CF @CORE One 0.8.json" }, + { + "name": "Prusament PC-CF @CORE One MMU3", + "sub_path": "filament/Prusament PC-CF @CORE One MMU3.json" + }, { "name": "Prusament PC Blend Carbon Fiber @MK4S 0.6", "sub_path": "filament/Prusament PC Blend Carbon Fiber @MK4S 0.6.json" @@ -2380,6 +2532,10 @@ "name": "Generic PETG @Prusa CORE One HF 0.8", "sub_path": "filament/Generic PETG @Prusa CORE One HF 0.8.json" }, + { + "name": "Generic PETG @Prusa CORE One MMU3", + "sub_path": "filament/Generic PETG @Prusa CORE One MMU3.json" + }, { "name": "Generic PETG @Prusa MK4S 0.6", "sub_path": "filament/Generic PETG @Prusa MK4S 0.6.json" @@ -2460,6 +2616,10 @@ "name": "Prusament PETG @CORE One HF 0.8", "sub_path": "filament/Prusament PETG @CORE One HF 0.8.json" }, + { + "name": "Prusament PETG @CORE One MMU3", + "sub_path": "filament/Prusament PETG @CORE One MMU3.json" + }, { "name": "Prusament PETG @MK4S 0.6", "sub_path": "filament/Prusament PETG @MK4S 0.6.json" @@ -2516,6 +2676,10 @@ "name": "Generic PLA @Prusa CORE One HF 0.8", "sub_path": "filament/Generic PLA @Prusa CORE One HF 0.8.json" }, + { + "name": "Generic PLA @Prusa CORE One MMU3", + "sub_path": "filament/Generic PLA @Prusa CORE One MMU3.json" + }, { "name": "Generic PLA @Prusa MK4S 0.6", "sub_path": "filament/Generic PLA @Prusa MK4S 0.6.json" @@ -2584,6 +2748,10 @@ "name": "Generic PLA Silk @Prusa CORE One 0.8", "sub_path": "filament/Generic PLA Silk @Prusa CORE One 0.8.json" }, + { + "name": "Generic PLA Silk @Prusa CORE One MMU3", + "sub_path": "filament/Generic PLA Silk @Prusa CORE One MMU3.json" + }, { "name": "Generic PLA-CF @Prusa", "sub_path": "filament/Generic PLA-CF @Prusa.json" @@ -2628,6 +2796,10 @@ "name": "Prusament PLA @CORE One HF 0.8", "sub_path": "filament/Prusament PLA @CORE One HF 0.8.json" }, + { + "name": "Prusament PLA @CORE One MMU3", + "sub_path": "filament/Prusament PLA @CORE One MMU3.json" + }, { "name": "Prusament PLA @MK4S 0.6", "sub_path": "filament/Prusament PLA @MK4S 0.6.json" @@ -2668,6 +2840,10 @@ "name": "Prusament rPLA @CORE One 0.8", "sub_path": "filament/Prusament rPLA @CORE One 0.8.json" }, + { + "name": "Prusament rPLA @CORE One MMU3", + "sub_path": "filament/Prusament rPLA @CORE One MMU3.json" + }, { "name": "Prusament rPLA @MK4S 0.6", "sub_path": "filament/Prusament rPLA @MK4S 0.6.json" @@ -2740,6 +2916,10 @@ "name": "Prusament PVB @CORE One 0.8", "sub_path": "filament/Prusament PVB @CORE One 0.8.json" }, + { + "name": "Prusament PVB @CORE One MMU3", + "sub_path": "filament/Prusament PVB @CORE One MMU3.json" + }, { "name": "Prusament PVB @MK4S 0.6", "sub_path": "filament/Prusament PVB @MK4S 0.6.json" @@ -3018,6 +3198,10 @@ "name": "fdm_machine_common_xl_5t", "sub_path": "machine/fdm_machine_common_xl_5t.json" }, + { + "name": "Prusa CORE One MMU3 0.4 nozzle", + "sub_path": "machine/Prusa CORE One MMU3 0.4 nozzle.json" + }, { "name": "Prusa CORE One 0.5 nozzle", "sub_path": "machine/Prusa CORE One 0.5 nozzle.json" @@ -3090,6 +3274,18 @@ "name": "Prusa XL 5T 0.8 nozzle", "sub_path": "machine/Prusa XL 5T 0.8 nozzle.json" }, + { + "name": "Prusa CORE One MMU3 0.25 nozzle", + "sub_path": "machine/Prusa CORE One MMU3 0.25 nozzle.json" + }, + { + "name": "Prusa CORE One MMU3 0.3 nozzle", + "sub_path": "machine/Prusa CORE One MMU3 0.3 nozzle.json" + }, + { + "name": "Prusa CORE One MMU3 0.6 nozzle", + "sub_path": "machine/Prusa CORE One MMU3 0.6 nozzle.json" + }, { "name": "Prusa MK4S HF0.5 nozzle", "sub_path": "machine/Prusa MK4S HF0.5 nozzle.json" diff --git a/resources/profiles/Prusa/Prusa CORE One MMU3_cover.png b/resources/profiles/Prusa/Prusa CORE One MMU3_cover.png new file mode 100644 index 0000000000000000000000000000000000000000..be841d654bb2fde379c65b660cd659fd2aa063a1 GIT binary patch literal 43464 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 35} \n; we need to preheat the chamber\nM140 S115 ; set bed temp for chamber heating\n{else} \n; just set the selected bed temp otherwise\nM140 S[first_layer_bed_temperature] ; set bed temp\n{endif}\nM109 R{((filament_notes[initial_tool]=~/.*HT_MBL10.*/) ? (first_layer_temperature[initial_tool] - 10) : (filament_type[initial_tool] == \"PC\" or filament_type[initial_tool] == \"PA\") ? (first_layer_temperature[initial_tool] - 25) : (filament_type[initial_tool] == \"FLEX\") ? 210 : (filament_type[initial_tool]=~/.*PET.*/) ? 175 : 170)} ; wait for temp\n\nM84 E ; turn off E motor\n\nG28 ; home all without mesh bed level\n\n; -- Chamber Temperature Control --\n{if chamber_temperature[initial_tool] > 35} ; if we need to heat the chamber\n; min chamber temp section\nM104 T{initial_tool} S{if idle_temperature[initial_tool] == 0}100{else}{idle_temperature[initial_tool]}{endif} ; set idle temp\nG1 Z10 F720 ; set bed position\nG1 X242 Y-9 F4800 ; set print head position\nM191 S{chamber_temperature[initial_tool]} ; wait for minimal chamber temp\nM141 S0 ; set nominal chamber temp\nM107\nM140 S[first_layer_bed_temperature] ; set bed temp\n\n{else}\nM141 S{if chamber_temperature[initial_tool] == 0}20{else}{chamber_temperature[initial_tool]}{endif} ; set nominal chamber temp\n{endif}\n\n{if first_layer_bed_temperature[initial_tool]<=60}M106 S70{endif}\nG0 Z40 F10000\nM104 T{initial_tool} S{if idle_temperature[initial_tool] == 0}100{else}{idle_temperature[initial_tool]}{endif}\nM190 R[first_layer_bed_temperature] ; wait for bed temp\nM107\n\nG29 G ; absorb heat\n\nM109 R{((filament_notes[0]=~/.*HT_MBL10.*/) ? (first_layer_temperature[0] - 10) : (filament_type[0] == \"PC\" or filament_type[0] == \"PA\") ? (first_layer_temperature[0] - 25) : (filament_type[0] == \"FLEX\") ? 210 : 170)} ; wait for MBL temp\n\nM302 S160 ; lower cold extrusion limit to 160C\n\n{if filament_type[initial_tool]==\"FLEX\"}\nG1 E-4 F2400 ; retraction\n{else}\nG1 E-2 F2400 ; retraction\n{endif}\n\nM84 E ; turn off E motor\n\n; --- Mesh Bed Leveling (MBL) ---\n\nG29 P9 X208 Y-2.5 W32 H4 ; limited MBL\nM84 E ; turn off E motor\n\nG29 P1 ; invalidate MBL and probe print area\nG29 P1 X150 Y0 W100 H20 C ; probe near purge area\nG29 P3.2 ; MBL interpolation\nG29 P3.13 ; MBL extrapolation outside probe area\nG29 A ; activate MBL\n\n; prepare for purge\nM104 S{first_layer_temperature[initial_tool]}\nG0 X249 Y-2.5 Z15 F4800 ; move away and ready for the purge\nM109 S{first_layer_temperature[initial_tool]}\n\nG92 E0\nM569 S0 E ; set spreadcycle mode for extruder\n\nM591 S0 ; disable stuck detection\n\nT[initial_tool]\nG1 E{parking_pos_retraction + extra_loading_move - 15} F1000 ; load to the nozzle\n\n;\n; Extrude purge line\n;\nG92 E0 ; reset extruder position\nG1 E{(filament_type[initial_tool] == \"FLEX\" ? 4 : 2)} F2400 ; deretraction after the initial one before nozzle cleaning\nG0 E5 X235 Z0.2 F500 ; purge\nG0 X145 E36 F500 ; purge\nG0 X135 E4 F500 ; purge\nG0 X125 E4 F650 ; purge\nG0 X122 Z0.05 F8000 ; wipe, move close to the bed\nG0 X119 Z0.2 F8000 ; wipe, move quickly away from the bed\n\nM591 R ; restore stuck detection\n\nG92 E0\nM221 S100 ; set flow to 100%", + "machine_unload_filament_time": "8.9", + "nozzle_diameter": [ + "0.4" + ], + "nozzle_height": "4", + "parking_pos_retraction": "84", + "printer_extruder_id": [ + "1" + ], + "printer_extruder_variant": [ + "Direct Drive Standard" + ], + "printer_model": "Prusa CORE One MMU3", + "printer_notes": "Don't remove the following keywords! These keywords are used in the \"compatible printer\" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_MODEL_COREONEMMU3\nPRINTER_MODEL_MK4\nCOREMMU\nRAMMING_EXTRA\nPG\nNO_TEMPLATES\nThe MK4 marker enables WipeTower2 cold ramming and filament monitoring control for the CORE One MMU3.", + "printer_variant": "0.4", + "single_extruder_multi_material": "1", + "wipe_tower_type": "type2" +} diff --git a/resources/profiles/Prusa/machine/Prusa CORE One MMU3 0.6 nozzle.json b/resources/profiles/Prusa/machine/Prusa CORE One MMU3 0.6 nozzle.json new file mode 100644 index 0000000000..11de2e2eac --- /dev/null +++ b/resources/profiles/Prusa/machine/Prusa CORE One MMU3 0.6 nozzle.json @@ -0,0 +1,19 @@ +{ + "type": "machine", + "name": "Prusa CORE One MMU3 0.6 nozzle", + "inherits": "Prusa CORE One MMU3 0.4 nozzle", + "from": "system", + "setting_id": "rFDxiYXLDZVYHqe8", + "instantiation": "true", + "default_filament_profile": "Prusament PLA @CORE One MMU3", + "default_print_profile": "0.20mm SPEED @COREONE0.6 + MMU3", + "max_layer_height": "0.40", + "min_layer_height": "0.15", + "nozzle_diameter": [ + "0.6" + ], + "printer_model": "Prusa CORE One MMU3", + "printer_variant": "0.6", + "retraction_length": "0.7", + "wipe": "0" +} diff --git a/resources/profiles/Prusa/machine/Prusa CORE One MMU3.json b/resources/profiles/Prusa/machine/Prusa CORE One MMU3.json new file mode 100644 index 0000000000..a0e766e818 --- /dev/null +++ b/resources/profiles/Prusa/machine/Prusa CORE One MMU3.json @@ -0,0 +1,12 @@ +{ + "type": "machine_model", + "name": "Prusa CORE One MMU3", + "bed_model": "coreone_bed.stl", + "bed_texture": "coreone.svg", + "default_materials": "Generic ABS @Prusa CORE One MMU3;Generic ASA @Prusa CORE One MMU3;Generic PETG @Prusa CORE One MMU3;Generic PLA @Prusa CORE One MMU3;Generic PLA Silk @Prusa CORE One MMU3;Generic TPU @Prusa CORE One MMU3", + "family": "Prusa", + "hotend_model": "", + "machine_tech": "FFF", + "model_id": "Prusa_CORE_One_MMU3", + "nozzle_diameter": "0.25;0.3;0.4;0.6" +} diff --git a/resources/profiles/Prusa/process/0.05mm DETAIL @COREONE0.25 + MMU3.json b/resources/profiles/Prusa/process/0.05mm DETAIL @COREONE0.25 + MMU3.json new file mode 100644 index 0000000000..123a51ea99 --- /dev/null +++ b/resources/profiles/Prusa/process/0.05mm DETAIL @COREONE0.25 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.05mm DETAIL @COREONE0.25 + MMU3", + "inherits": "0.05mm DETAIL @CORE One 0.25", + "from": "system", + "setting_id": "GZ9p7JXzPU3Widdg", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.25 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.07mm DETAIL @COREONE0.25 + MMU3.json b/resources/profiles/Prusa/process/0.07mm DETAIL @COREONE0.25 + MMU3.json new file mode 100644 index 0000000000..97b0973c5c --- /dev/null +++ b/resources/profiles/Prusa/process/0.07mm DETAIL @COREONE0.25 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.07mm DETAIL @COREONE0.25 + MMU3", + "inherits": "0.07mm DETAIL @CORE One 0.25", + "from": "system", + "setting_id": "9ffM9tc7XMSNHnGB", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.25 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.10mm FAST DETAIL @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.10mm FAST DETAIL @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..2e7b0c7edb --- /dev/null +++ b/resources/profiles/Prusa/process/0.10mm FAST DETAIL @COREONE0.4 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.10mm FAST DETAIL @COREONE0.4 + MMU3", + "inherits": "0.10mm FAST DETAIL @CORE One 0.4", + "from": "system", + "setting_id": "5l28K1bE8lfuaXF1", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.12mm SPEED @COREONE0.25 + MMU3.json b/resources/profiles/Prusa/process/0.12mm SPEED @COREONE0.25 + MMU3.json new file mode 100644 index 0000000000..e2d4ba94ca --- /dev/null +++ b/resources/profiles/Prusa/process/0.12mm SPEED @COREONE0.25 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.12mm SPEED @COREONE0.25 + MMU3", + "inherits": "0.12mm SPEED @CORE One 0.25", + "from": "system", + "setting_id": "YjbMEvZ8ALzK2qHf", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.25 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.12mm STRUCTURAL @COREONE0.25 + MMU3.json b/resources/profiles/Prusa/process/0.12mm STRUCTURAL @COREONE0.25 + MMU3.json new file mode 100644 index 0000000000..551f6248f5 --- /dev/null +++ b/resources/profiles/Prusa/process/0.12mm STRUCTURAL @COREONE0.25 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.12mm STRUCTURAL @COREONE0.25 + MMU3", + "inherits": "0.12mm STRUCTURAL @CORE One 0.25", + "from": "system", + "setting_id": "g8Bp93DCAwTo5G0s", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.25 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.12mm STRUCTURAL @COREONE0.3 + MMU3.json b/resources/profiles/Prusa/process/0.12mm STRUCTURAL @COREONE0.3 + MMU3.json new file mode 100644 index 0000000000..25efb09730 --- /dev/null +++ b/resources/profiles/Prusa/process/0.12mm STRUCTURAL @COREONE0.3 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.12mm STRUCTURAL @COREONE0.3 + MMU3", + "inherits": "0.12mm STRUCTURAL @CORE One 0.3", + "from": "system", + "setting_id": "XmhM0qI0mXLbBMwv", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.3 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.15mm BALANCED @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.15mm BALANCED @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..dd86f29945 --- /dev/null +++ b/resources/profiles/Prusa/process/0.15mm BALANCED @COREONE0.4 + MMU3.json @@ -0,0 +1,70 @@ +{ + "type": "process", + "name": "0.15mm BALANCED @COREONE0.4 + MMU3", + "inherits": "0.20mm Standard @MK4", + "from": "system", + "setting_id": "gU24xfFrPejGfV1D", + "instantiation": "true", + "bridge_acceleration": "1500", + "brim_type": "no_brim", + "default_acceleration": "3000", + "elefant_foot_compensation": "0.2", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "initial_layer_acceleration": "500", + "initial_layer_infill_speed": "100", + "initial_layer_line_width": "0.5", + "inner_wall_line_width": "0.45", + "internal_solid_infill_line_width": "0.45", + "line_width": "0.45", + "outer_wall_line_width": "0.45", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "raft_first_layer_expansion": "3", + "resolution": "0", + "seam_gap": "15%", + "sparse_infill_line_width": "0.45", + "sparse_infill_pattern": "grid", + "support_base_pattern_spacing": "2", + "support_bottom_interface_spacing": "0.2", + "support_interface_bottom_layers": "0", + "support_interface_spacing": "0.2", + "support_interface_top_layers": "3", + "support_line_width": "0.4", + "support_speed": "120", + "top_surface_acceleration": "2000", + "top_surface_line_width": "0.42", + "travel_acceleration": "7000", + "travel_speed": "350", + "wall_loops": "2", + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "bridge_speed": "45", + "bottom_shell_layers": "4", + "inner_wall_acceleration": "3000", + "inner_wall_speed": "150", + "internal_bridge_speed": "45", + "internal_solid_infill_acceleration": "6000", + "internal_solid_infill_speed": "150", + "layer_height": "0.15", + "outer_wall_acceleration": "2500", + "overhang_2_4_speed": "25", + "overhang_3_4_speed": "50", + "overhang_4_4_speed": "80%", + "small_perimeter_speed": "170", + "sparse_infill_acceleration": "5000", + "sparse_infill_speed": "150", + "support_bottom_z_distance": "0.17", + "support_interface_speed": "50", + "support_threshold_angle": "35", + "support_top_z_distance": "0.17", + "top_shell_layers": "6", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ] +} diff --git a/resources/profiles/Prusa/process/0.15mm SPEED @COREONE0.25 + MMU3.json b/resources/profiles/Prusa/process/0.15mm SPEED @COREONE0.25 + MMU3.json new file mode 100644 index 0000000000..287579505b --- /dev/null +++ b/resources/profiles/Prusa/process/0.15mm SPEED @COREONE0.25 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.15mm SPEED @COREONE0.25 + MMU3", + "inherits": "0.15mm SPEED @CORE One 0.25", + "from": "system", + "setting_id": "mYSeGZQ0MykDNtXP", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.25 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.15mm SPEED @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.15mm SPEED @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..d9b2e4b903 --- /dev/null +++ b/resources/profiles/Prusa/process/0.15mm SPEED @COREONE0.4 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.15mm SPEED @COREONE0.4 + MMU3", + "inherits": "0.15mm SPEED @CORE One 0.4", + "from": "system", + "setting_id": "CdehEFlvnacxhU2H", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.25 + MMU3.json b/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.25 + MMU3.json new file mode 100644 index 0000000000..b9082c332c --- /dev/null +++ b/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.25 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.15mm STRUCTURAL @COREONE0.25 + MMU3", + "inherits": "0.15mm STRUCTURAL @CORE One 0.25", + "from": "system", + "setting_id": "sUWaBUUy033BXEzk", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.25 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..dd1e847ee3 --- /dev/null +++ b/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.4 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.15mm STRUCTURAL @COREONE0.4 + MMU3", + "inherits": "0.15mm STRUCTURAL @CORE One 0.4", + "from": "system", + "setting_id": "kM9rtB6iLqcDNZZU", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.6 + MMU3.json b/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.6 + MMU3.json new file mode 100644 index 0000000000..6e7adf72c0 --- /dev/null +++ b/resources/profiles/Prusa/process/0.15mm STRUCTURAL @COREONE0.6 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.15mm STRUCTURAL @COREONE0.6 + MMU3", + "inherits": "0.15mm STRUCTURAL @CORE One 0.6", + "from": "system", + "setting_id": "PTOVGYLNah28dKRr", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.6 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.16mm SPEED @COREONE0.3 + MMU3.json b/resources/profiles/Prusa/process/0.16mm SPEED @COREONE0.3 + MMU3.json new file mode 100644 index 0000000000..30c6902ee7 --- /dev/null +++ b/resources/profiles/Prusa/process/0.16mm SPEED @COREONE0.3 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.16mm SPEED @COREONE0.3 + MMU3", + "inherits": "0.16mm SPEED @CORE One 0.3", + "from": "system", + "setting_id": "ByXT7JeCV0SWiSnt", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.3 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.16mm STRUCTURAL @COREONE0.3 + MMU3.json b/resources/profiles/Prusa/process/0.16mm STRUCTURAL @COREONE0.3 + MMU3.json new file mode 100644 index 0000000000..946d22c3a5 --- /dev/null +++ b/resources/profiles/Prusa/process/0.16mm STRUCTURAL @COREONE0.3 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.16mm STRUCTURAL @COREONE0.3 + MMU3", + "inherits": "0.16mm STRUCTURAL @CORE One 0.3", + "from": "system", + "setting_id": "3XaKemYMM9AMLAIE", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.3 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.20mm BALANCED @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.20mm BALANCED @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..eba4b17a0e --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm BALANCED @COREONE0.4 + MMU3.json @@ -0,0 +1,66 @@ +{ + "type": "process", + "name": "0.20mm BALANCED @COREONE0.4 + MMU3", + "inherits": "0.20mm Standard @MK4", + "from": "system", + "setting_id": "ZhSjwXMEOWyeWIDd", + "instantiation": "true", + "bridge_acceleration": "1500", + "brim_type": "no_brim", + "default_acceleration": "3000", + "elefant_foot_compensation": "0.2", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "initial_layer_acceleration": "500", + "initial_layer_infill_speed": "100", + "initial_layer_line_width": "0.5", + "inner_wall_line_width": "0.45", + "internal_solid_infill_line_width": "0.45", + "line_width": "0.45", + "outer_wall_line_width": "0.45", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "raft_first_layer_expansion": "3", + "resolution": "0", + "seam_gap": "15%", + "sparse_infill_line_width": "0.45", + "sparse_infill_pattern": "grid", + "support_base_pattern_spacing": "2", + "support_bottom_interface_spacing": "0.2", + "support_interface_bottom_layers": "0", + "support_interface_spacing": "0.2", + "support_interface_top_layers": "3", + "support_line_width": "0.4", + "support_speed": "120", + "top_surface_acceleration": "2000", + "top_surface_line_width": "0.42", + "travel_acceleration": "7000", + "travel_speed": "350", + "wall_loops": "2", + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "bridge_speed": "50", + "inner_wall_acceleration": "3000", + "inner_wall_speed": "150", + "internal_solid_infill_acceleration": "6000", + "internal_solid_infill_speed": "150", + "outer_wall_acceleration": "2500", + "overhang_2_4_speed": "25", + "overhang_3_4_speed": "50", + "overhang_4_4_speed": "80%", + "small_perimeter_speed": "170", + "sparse_infill_acceleration": "5000", + "sparse_infill_speed": "150", + "support_interface_speed": "60", + "support_style": "snug", + "support_threshold_angle": "35", + "top_shell_layers": "5", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ] +} diff --git a/resources/profiles/Prusa/process/0.20mm SOLUBLE FULL @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.20mm SOLUBLE FULL @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..88860432a2 --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm SOLUBLE FULL @COREONE0.4 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.20mm SOLUBLE FULL @COREONE0.4 + MMU3", + "inherits": "0.20mm SOLUBLE FULL @CORE One 0.4", + "from": "system", + "setting_id": "F4GvezeeqYGgETx5", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.20mm SOLUBLE INTERFACE @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.20mm SOLUBLE INTERFACE @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..c74ace1163 --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm SOLUBLE INTERFACE @COREONE0.4 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.20mm SOLUBLE INTERFACE @COREONE0.4 + MMU3", + "inherits": "0.20mm SOLUBLE INTERFACE @CORE One 0.4", + "from": "system", + "setting_id": "hrh6ZS8cMrp6Fput", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.3 + MMU3.json b/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.3 + MMU3.json new file mode 100644 index 0000000000..660170520b --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.3 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.20mm SPEED @COREONE0.3 + MMU3", + "inherits": "0.20mm SPEED @CORE One 0.3", + "from": "system", + "setting_id": "a8MnMQB4PuzlCBkv", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.3 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..66ac63af73 --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.4 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.20mm SPEED @COREONE0.4 + MMU3", + "inherits": "0.20mm SPEED @CORE One 0.4", + "from": "system", + "setting_id": "gFegVTUTnp12IurN", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.6 + MMU3.json b/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.6 + MMU3.json new file mode 100644 index 0000000000..fa6fcb0a90 --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm SPEED @COREONE0.6 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.20mm SPEED @COREONE0.6 + MMU3", + "inherits": "0.20mm SPEED @CORE One 0.6", + "from": "system", + "setting_id": "b5DuLDBbPOrc3QUB", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.6 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.3 + MMU3.json b/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.3 + MMU3.json new file mode 100644 index 0000000000..d1fe101fb0 --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.3 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.20mm STRUCTURAL @COREONE0.3 + MMU3", + "inherits": "0.20mm STRUCTURAL @CORE One 0.3", + "from": "system", + "setting_id": "3GXAe0t8hNMloSsA", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.3 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..cdb41c0ac6 --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.4 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.20mm STRUCTURAL @COREONE0.4 + MMU3", + "inherits": "0.20mm STRUCTURAL @CORE One 0.4", + "from": "system", + "setting_id": "elSJkPZlFi4sWveY", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.6 + MMU3.json b/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.6 + MMU3.json new file mode 100644 index 0000000000..1a5cad323a --- /dev/null +++ b/resources/profiles/Prusa/process/0.20mm STRUCTURAL @COREONE0.6 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.20mm STRUCTURAL @COREONE0.6 + MMU3", + "inherits": "0.20mm STRUCTURAL @CORE One 0.6", + "from": "system", + "setting_id": "uvQ25wBWKEFdqMVl", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.6 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.25mm BALANCED @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.25mm BALANCED @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..4a3343351a --- /dev/null +++ b/resources/profiles/Prusa/process/0.25mm BALANCED @COREONE0.4 + MMU3.json @@ -0,0 +1,68 @@ +{ + "type": "process", + "name": "0.25mm BALANCED @COREONE0.4 + MMU3", + "inherits": "0.20mm Standard @MK4", + "from": "system", + "setting_id": "cEccdq6KND9xa5HS", + "instantiation": "true", + "bridge_acceleration": "1500", + "brim_type": "no_brim", + "default_acceleration": "3000", + "elefant_foot_compensation": "0.2", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "initial_layer_acceleration": "500", + "initial_layer_infill_speed": "100", + "initial_layer_line_width": "0.5", + "inner_wall_line_width": "0.45", + "internal_solid_infill_line_width": "0.45", + "line_width": "0.45", + "outer_wall_line_width": "0.45", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "raft_first_layer_expansion": "3", + "resolution": "0", + "seam_gap": "15%", + "sparse_infill_line_width": "0.45", + "sparse_infill_pattern": "grid", + "support_base_pattern_spacing": "2", + "support_bottom_interface_spacing": "0.2", + "support_interface_bottom_layers": "0", + "support_interface_spacing": "0.2", + "support_interface_top_layers": "3", + "support_line_width": "0.4", + "support_speed": "120", + "top_surface_acceleration": "2000", + "top_surface_line_width": "0.42", + "travel_acceleration": "7000", + "travel_speed": "350", + "wall_loops": "2", + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "bridge_speed": "50", + "inner_wall_acceleration": "3000", + "inner_wall_speed": "150", + "internal_solid_infill_acceleration": "5000", + "internal_solid_infill_speed": "140", + "layer_height": "0.25", + "outer_wall_acceleration": "2500", + "overhang_2_4_speed": "15", + "overhang_3_4_speed": "25", + "overhang_4_4_speed": "70%", + "small_perimeter_speed": "150", + "sparse_infill_acceleration": "5000", + "sparse_infill_speed": "150", + "support_base_pattern": "rectilinear", + "support_bottom_z_distance": "0.25", + "support_style": "snug", + "support_threshold_angle": "35", + "support_top_z_distance": "0.25", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ] +} diff --git a/resources/profiles/Prusa/process/0.25mm SPEED @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.25mm SPEED @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..869de56781 --- /dev/null +++ b/resources/profiles/Prusa/process/0.25mm SPEED @COREONE0.4 + MMU3.json @@ -0,0 +1,65 @@ +{ + "type": "process", + "name": "0.25mm SPEED @COREONE0.4 + MMU3", + "inherits": "0.20mm Standard @MK4", + "from": "system", + "setting_id": "oVLlJtwPINArAb0Y", + "instantiation": "true", + "bridge_acceleration": "1500", + "brim_type": "no_brim", + "default_acceleration": "3000", + "elefant_foot_compensation": "0.2", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "initial_layer_acceleration": "500", + "initial_layer_infill_speed": "100", + "initial_layer_line_width": "0.5", + "inner_wall_line_width": "0.45", + "internal_solid_infill_line_width": "0.45", + "line_width": "0.45", + "outer_wall_line_width": "0.45", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "raft_first_layer_expansion": "3", + "resolution": "0", + "seam_gap": "15%", + "sparse_infill_line_width": "0.45", + "sparse_infill_pattern": "grid", + "support_base_pattern_spacing": "2", + "support_bottom_interface_spacing": "0.2", + "support_interface_bottom_layers": "0", + "support_interface_spacing": "0.2", + "support_interface_top_layers": "3", + "support_line_width": "0.4", + "support_speed": "120", + "top_surface_acceleration": "2000", + "top_surface_line_width": "0.42", + "travel_acceleration": "7000", + "travel_speed": "350", + "wall_loops": "2", + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "bridge_speed": "50", + "inner_wall_acceleration": "6000", + "internal_solid_infill_acceleration": "5000", + "layer_height": "0.25", + "overhang_2_4_speed": "15", + "overhang_3_4_speed": "25", + "overhang_4_4_speed": "70%", + "small_perimeter_speed": "170", + "sparse_infill_acceleration": "7000", + "support_base_pattern": "rectilinear", + "support_bottom_z_distance": "0.25", + "support_interface_speed": "60", + "support_style": "snug", + "support_threshold_angle": "35", + "support_top_z_distance": "0.25", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ] +} diff --git a/resources/profiles/Prusa/process/0.25mm SPEED @COREONE0.6 + MMU3.json b/resources/profiles/Prusa/process/0.25mm SPEED @COREONE0.6 + MMU3.json new file mode 100644 index 0000000000..ebfaa5bad5 --- /dev/null +++ b/resources/profiles/Prusa/process/0.25mm SPEED @COREONE0.6 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.25mm SPEED @COREONE0.6 + MMU3", + "inherits": "0.25mm SPEED @CORE One 0.6", + "from": "system", + "setting_id": "CRAV58WCDIn2bZdK", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.6 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.25mm STRUCTURAL @COREONE0.4 + MMU3.json b/resources/profiles/Prusa/process/0.25mm STRUCTURAL @COREONE0.4 + MMU3.json new file mode 100644 index 0000000000..87aa0f0c52 --- /dev/null +++ b/resources/profiles/Prusa/process/0.25mm STRUCTURAL @COREONE0.4 + MMU3.json @@ -0,0 +1,72 @@ +{ + "type": "process", + "name": "0.25mm STRUCTURAL @COREONE0.4 + MMU3", + "inherits": "0.20mm Standard @MK4", + "from": "system", + "setting_id": "lBGry26X4a9IihAD", + "instantiation": "true", + "bridge_acceleration": "1500", + "brim_type": "no_brim", + "default_acceleration": "3000", + "elefant_foot_compensation": "0.2", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "initial_layer_acceleration": "500", + "initial_layer_infill_speed": "100", + "initial_layer_line_width": "0.5", + "inner_wall_line_width": "0.45", + "internal_solid_infill_line_width": "0.45", + "line_width": "0.45", + "outer_wall_line_width": "0.45", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "raft_first_layer_expansion": "3", + "resolution": "0", + "seam_gap": "15%", + "sparse_infill_line_width": "0.45", + "sparse_infill_pattern": "grid", + "support_base_pattern_spacing": "2", + "support_bottom_interface_spacing": "0.2", + "support_interface_bottom_layers": "0", + "support_interface_spacing": "0.2", + "support_interface_top_layers": "3", + "support_line_width": "0.4", + "support_speed": "120", + "top_surface_acceleration": "2000", + "top_surface_line_width": "0.42", + "travel_acceleration": "7000", + "travel_speed": "350", + "wall_loops": "2", + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "bridge_speed": "50", + "gap_infill_speed": "60", + "inner_wall_acceleration": "2500", + "inner_wall_speed": "70", + "internal_solid_infill_acceleration": "4000", + "internal_solid_infill_speed": "140", + "layer_height": "0.25", + "outer_wall_acceleration": "1500", + "outer_wall_speed": "50", + "overhang_1_4_speed": "50%", + "overhang_2_4_speed": "15", + "overhang_3_4_speed": "45", + "overhang_4_4_speed": "90%", + "small_perimeter_speed": "50", + "sparse_infill_acceleration": "6000", + "sparse_infill_speed": "120", + "support_base_pattern": "rectilinear", + "support_bottom_z_distance": "0.25", + "support_style": "snug", + "support_threshold_angle": "40", + "support_top_z_distance": "0.25", + "top_surface_speed": "80", + "compatible_printers": [ + "Prusa CORE One MMU3 0.4 nozzle" + ] +} diff --git a/resources/profiles/Prusa/process/0.25mm STRUCTURAL @COREONE0.6 + MMU3.json b/resources/profiles/Prusa/process/0.25mm STRUCTURAL @COREONE0.6 + MMU3.json new file mode 100644 index 0000000000..3ade06d990 --- /dev/null +++ b/resources/profiles/Prusa/process/0.25mm STRUCTURAL @COREONE0.6 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.25mm STRUCTURAL @COREONE0.6 + MMU3", + "inherits": "0.25mm STRUCTURAL @CORE One 0.6", + "from": "system", + "setting_id": "95m4AA4YRwcBt63r", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.6 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} diff --git a/resources/profiles/Prusa/process/0.32mm SPEED @COREONE0.6 + MMU3.json b/resources/profiles/Prusa/process/0.32mm SPEED @COREONE0.6 + MMU3.json new file mode 100644 index 0000000000..1f39814702 --- /dev/null +++ b/resources/profiles/Prusa/process/0.32mm SPEED @COREONE0.6 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.32mm SPEED @COREONE0.6 + MMU3", + "inherits": "0.32mm SPEED @CORE One 0.6", + "from": "system", + "setting_id": "nqrmLvULVOKW9bSm", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.6 nozzle" + ], + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%", + "compatible_printers_condition": "" +} diff --git a/resources/profiles/Prusa/process/0.32mm STRUCTURAL @COREONE0.6 + MMU3.json b/resources/profiles/Prusa/process/0.32mm STRUCTURAL @COREONE0.6 + MMU3.json new file mode 100644 index 0000000000..8d69f5c4bc --- /dev/null +++ b/resources/profiles/Prusa/process/0.32mm STRUCTURAL @COREONE0.6 + MMU3.json @@ -0,0 +1,23 @@ +{ + "type": "process", + "name": "0.32mm STRUCTURAL @COREONE0.6 + MMU3", + "inherits": "0.32mm STRUCTURAL @CORE One 0.6", + "from": "system", + "setting_id": "rYhCYIq1vvY40IYf", + "instantiation": "true", + "compatible_printers": [ + "Prusa CORE One MMU3 0.6 nozzle" + ], + "compatible_printers_condition": "", + "filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}-{filament_type[1]}-{filament_type[2]}-{filament_type[3]}-{filament_type[4]}_COREONE_MMU3_{print_time}.gcode", + "prime_tower_brim_width": "2", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "wipe_tower_cone_angle": "25", + "wipe_tower_extra_flow": "250%", + "wipe_tower_extra_spacing": "110%" +} From 12670a6e407d5f8e9f35d30937cfedb229166c3b Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:06:02 +0800 Subject: [PATCH 17/19] Answer the Preview's Per-Frame Lookups From Cached Sums and Draw Segments From an Index Buffer (#15833) * Answer the Preview's Per-Frame Time Query From a Cached Sum The G-code preview's cost is linear in the number of toolpath vertices, and on a tall multi-filament print the wipe tower dominates that count: it emits a roughly constant 160-180 moves on every layer whatever the object is, measured at 57-61% of all moves on a three-filament print. Four places scanned or allocated across the whole vertex array. None of them needed to. get_estimated_time_at re-accumulated the estimated time from vertex 0 on every call, and its caller is the tool marker tooltip, which ImGui re-renders every frame while the properties panel is unfolded. It now starts from a running sum kept at each layer's first vertex, built at load in vertex order, and adds only that layer's vertices: the same additions in the same order, so the float result is unchanged, at a cost of one float per layer and time mode rather than per vertex. At the 351k vertices of a 636-layer test print the call scanned the whole print (238us); it now scans one layer. update_view_full_range walked from vertex 0 to find where the layer range starts, on every slider tick. It now starts at the first vertex of that layer. The index is derived from the vertices rather than from Layers::Item::range, because Layers::update folds a vertex whose layer_id arrives out of order into whichever bucket is open, which makes that range the wrong answer in general; the index costs four bytes per layer, not per vertex. update_colors_texture allocated one float per vertex of the whole print on every slider tick. It now reuses a buffer. render_legend fetched the layer Zs and the per-layer times from inside loops over the custom G-code items, and built whole vectors only to test them for emptiness. The times are hoisted, the Zs are built lazily so a print with no colour change does not pay for them at all, and the emptiness tests use the existing counters. No rendering behaviour changes. * Draw the Preview's Toolpath Segments From an Index Buffer The preview's frame cost is dominated by one call: a single instanced draw of every visible toolpath segment. On a tall multi-filament print the wipe tower supplies most of those segments, which is why the preview of a large tower is slow and why shrinking the layer range speeds it up again. That draw is not fill bound. Shrinking the model to about a fortieth of its screen area moved the frame from 419 ms to 401 ms, so the cost is per segment, not per pixel, and it is paid in the vertex shader: five texelFetch calls plus several cross/normalize per invocation. Each segment is a box of eight corners, but it was submitted with glDrawArraysInstanced over a 24 entry array, so every corner was transformed once per triangle that touches it and the shader ran 24 times per segment. The same 24 entries are now an element buffer over the eight distinct corners, which lets the post-transform cache reuse them and drops the shader to 8 runs per segment. The triangles, their winding and the vertex_id each corner receives are unchanged. Measured over 100 frames on the 636-layer, 351k-vertex three-filament fixture, the segment draw goes from 381 ms to 322 ms per frame. That is a software rasterizer, where triangle setup dominates and understates the win; the drop in shader invocations is the transferable part. Verified by loading the same project in this build and in a build of the parent commit and comparing the canvas across three states - the default view, a rotated camera, and a reduced layer range: pixel identical in all three. The rotated case matters because the shader picks its corner offsets from the camera direction. The only pixels that differ anywhere on screen are in the G-code text panel, which prints a per-process object id that varies between any two runs. --- src/libvgcode/src/SegmentTemplate.cpp | 24 ++++++++-- src/libvgcode/src/SegmentTemplate.hpp | 1 + src/libvgcode/src/ViewerImpl.cpp | 65 +++++++++++++++++++++++++-- src/libvgcode/src/ViewerImpl.hpp | 14 ++++++ src/slic3r/GUI/GCodeViewer.cpp | 18 ++++++-- 5 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/libvgcode/src/SegmentTemplate.cpp b/src/libvgcode/src/SegmentTemplate.cpp index 4e692d5b10..9d9872aa3b 100644 --- a/src/libvgcode/src/SegmentTemplate.cpp +++ b/src/libvgcode/src/SegmentTemplate.cpp @@ -15,7 +15,12 @@ namespace libvgcode { //| 2--0-------5--7 | //| \ | | / | //| 3-------4 | -static constexpr const std::array VERTEX_DATA = { +// The eight corners the vertex shader knows how to place. Each is sent once and +// referenced by INDEX_DATA below, so the post-transform cache can reuse it across +// the triangles that share it: the shader runs 8 times per segment instead of 24. +static constexpr const std::array VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 }; + +static constexpr const std::array INDEX_DATA = { 0, 1, 2, // front spike 0, 2, 3, // front spike 0, 3, 4, // right/bottom body @@ -31,7 +36,7 @@ void SegmentTemplate::init() if (m_vao_id != 0) return; - m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t); + m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t); int curr_vertex_array; glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); @@ -51,12 +56,22 @@ void SegmentTemplate::init() glsafe(glVertexAttribIPointer(0, 1, GL_UNSIGNED_BYTE, 0, (const void*)0)); #endif // ENABLE_OPENGL_ES + // The element buffer binding is part of the vao state, so it is left bound here + // and restored together with the vao. + glsafe(glGenBuffers(1, &m_ibo_id)); + glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id)); + glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW)); + glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer)); glsafe(glBindVertexArray(curr_vertex_array)); } void SegmentTemplate::shutdown() { + if (m_ibo_id != 0) { + glsafe(glDeleteBuffers(1, &m_ibo_id)); + m_ibo_id = 0; + } if (m_vbo_id != 0) { glsafe(glDeleteBuffers(1, &m_vbo_id)); m_vbo_id = 0; @@ -71,14 +86,15 @@ void SegmentTemplate::shutdown() void SegmentTemplate::render(size_t count) { - if (m_vao_id == 0 || m_vbo_id == 0 || count == 0) + if (m_vao_id == 0 || m_vbo_id == 0 || m_ibo_id == 0 || count == 0) return; int curr_vertex_array; glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); glsafe(glBindVertexArray(m_vao_id)); - glsafe(glDrawArraysInstanced(GL_TRIANGLES, 0, static_cast(VERTEX_DATA.size()), static_cast(count))); + glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast(INDEX_DATA.size()), GL_UNSIGNED_BYTE, + nullptr, static_cast(count))); glsafe(glBindVertexArray(curr_vertex_array)); } diff --git a/src/libvgcode/src/SegmentTemplate.hpp b/src/libvgcode/src/SegmentTemplate.hpp index f104eef5d2..117ed2b678 100644 --- a/src/libvgcode/src/SegmentTemplate.hpp +++ b/src/libvgcode/src/SegmentTemplate.hpp @@ -40,6 +40,7 @@ private: // unsigned int m_vao_id{ 0 }; unsigned int m_vbo_id{ 0 }; + unsigned int m_ibo_id{ 0 }; // // Size of the data sent to gpu, in bytes. // diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index 10fda297d4..b1a7550b42 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -883,6 +883,12 @@ void ViewerImpl::reset() m_travels_time = { 0.0f, 0.0f }; m_vertices.clear(); m_vertices_colors.clear(); + // swap rather than clear: these are sized by the print, and a reset means the memory + // should go back, not sit reserved until the next load + for (std::vector& times : m_layer_start_times) + std::vector().swap(times); + std::vector().swap(m_layer_first_vertex); + std::vector().swap(m_colors_scratch); m_valid_lines_bitset.clear(); #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS m_cog_marker.reset(); @@ -1056,6 +1062,37 @@ void ViewerImpl::load(GCodeInputData&& gcode_data) v.layer_duration = m_layers.get_layer_time(m_settings.time_mode, static_cast(v.layer_id)); } + // Index of the first vertex of each layer, walked back to front so that a layer with no + // vertex of its own inherits the next layer's index and the array stays non-decreasing. + if (!m_layers.empty()) { + const uint32_t vertices_count = static_cast(m_vertices.size()); + m_layer_first_vertex.assign(m_layers.count(), vertices_count); + for (uint32_t i = vertices_count; i > 0; --i) { + const uint32_t layer_id = m_vertices[i - 1].layer_id; + if (layer_id < m_layer_first_vertex.size()) + m_layer_first_vertex[layer_id] = i - 1; + } + for (size_t i = m_layer_first_vertex.size() - 1; i > 0; --i) + m_layer_first_vertex[i - 1] = std::min(m_layer_first_vertex[i - 1], m_layer_first_vertex[i]); + + // the running time at each layer's first vertex, summed in vertex order so that + // get_estimated_time_at() matches a full accumulation exactly + std::array running{}; + for (std::vector& times : m_layer_start_times) + times.assign(m_layer_first_vertex.size(), 0.0f); + size_t layer = 0; + for (size_t i = 0; i <= m_vertices.size(); ++i) { + for (; layer < m_layer_first_vertex.size() && m_layer_first_vertex[layer] == i; ++layer) { + for (size_t j = 0; j < TIME_MODES_COUNT; ++j) + m_layer_start_times[j][layer] = running[j]; + } + if (i < m_vertices.size()) { + for (size_t j = 0; j < TIME_MODES_COUNT; ++j) + running[j] += m_vertices[i].times[j]; + } + } + } + if (!m_layers.empty()) m_layers.set_view_range(0, static_cast(m_layers.count()) - 1); @@ -1269,7 +1306,10 @@ void ViewerImpl::update_colors_texture() // Based on current settings and slider position, we might want to render some // vertices as dark grey (or darkened, see above). Use either that or the normal color (from the cache). - std::vector colors(m_vertices_colors.size()); + // Reused across calls: this runs on every slider tick, and the allocation alone is + // 4 bytes per vertex of the whole print each time. + std::vector& colors = m_colors_scratch; + colors.resize(m_vertices_colors.size()); assert(colors.size() == m_vertices.size() && m_vertices_colors.size() == m_vertices.size()); for (size_t i=0; i(m_settings.time_mode)]; }); + const size_t mode = static_cast(m_settings.time_mode); + if (mode >= TIME_MODES_COUNT || id >= m_vertices.size()) + return 0.0f; + size_t first = 0; + float time = 0.0f; + const size_t layer = static_cast(m_vertices[id].layer_id); + if (layer < m_layer_first_vertex.size() && m_layer_first_vertex[layer] <= id) { + first = m_layer_first_vertex[layer]; + time = m_layer_start_times[mode][layer]; + } + for (size_t i = first; i <= id; ++i) + time += m_vertices[i].times[mode]; + return time; } Color ViewerImpl::get_vertex_color(const PathVertex& v) const @@ -1759,6 +1810,10 @@ size_t ViewerImpl::get_used_cpu_memory() const ret += sizeof(m_extrusion_roles_colors); ret += sizeof(m_options_colors); ret += STDVEC_MEMSIZE(m_vertices, PathVertex); + for (const std::vector& times : m_layer_start_times) + ret += STDVEC_MEMSIZE(times, float); + ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t); + ret += STDVEC_MEMSIZE(m_colors_scratch, float); ret += m_valid_lines_bitset.size_in_bytes_cpu(); ret += m_height_range.size_in_bytes_cpu(); ret += m_width_range.size_in_bytes_cpu(); @@ -1824,7 +1879,11 @@ void ViewerImpl::update_view_full_range() const bool travels_visible = m_settings.options_visibility[size_t(EOptionType::Travels)]; const bool wipes_visible = m_settings.options_visibility[size_t(EOptionType::Wipes)]; + // every vertex before m_layer_first_vertex[layers_range[0]] has a smaller layer_id, so the loop + // below would skip all of them anyway auto first_it = m_vertices.begin(); + if (layers_range[0] < m_layer_first_vertex.size()) + first_it += m_layer_first_vertex[layers_range[0]]; while (first_it != m_vertices.end() && (first_it->layer_id < layers_range[0] || !is_visible(*first_it, m_settings))) { ++first_it; diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index 9231cbdefc..c9d658beb1 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -252,6 +252,20 @@ private: // std::array m_total_time{ 0.0f, 0.0f }; // + // Running sum of the vertex estimated times at each layer's first vertex, for each time mode, + // so that get_estimated_time_at() only accumulates the vertices of one layer. + // + std::array, TIME_MODES_COUNT> m_layer_start_times; + // + // For each layer L, the index of the first vertex whose layer_id is >= L (m_vertices.size() + // if there is none). Derived from the vertices, so it stays exact whatever order they arrive in. + // + std::vector m_layer_first_vertex; + // + // Scratch buffer for update_colors_texture(), kept alive across slider steps + // + std::vector m_colors_scratch; + // // Detected travel moves times // std::array m_travels_time{ 0.0f, 0.0f }; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b86af5c259..68e81493c1 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1698,7 +1698,7 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height) glsafe(::glEnable(GL_DEPTH_TEST)); render_shells(canvas_width, canvas_height); - if (m_viewer.get_extrusion_roles().empty()) + if (m_viewer.get_extrusion_roles_count() == 0) return; render_toolpaths(); @@ -3520,6 +3520,12 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv std::vector>> ret; ret.reserve(custom_gcode_per_print_z.size()); + // Loop invariant, but built lazily: this lambda runs once per extruder on every frame + // and most prints reach neither colour change below, so fetching it up front would cost + // more than the per-item fetch it replaces. + std::vector zs; + bool zs_built = false; + for (const auto& item : custom_gcode_per_print_z) { if (extruder_id + 1 != static_cast(item.extruder)) continue; @@ -3527,7 +3533,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv if (item.type != ColorChange) continue; - const std::vector zs = m_viewer.get_layers_zs(); + if (!zs_built) { + zs = m_viewer.get_layers_zs(); + zs_built = true; + } auto lower_b = std::lower_bound(zs.begin(), zs.end(), static_cast(item.print_z - epsilon())); if (lower_b == zs.end()) @@ -4680,6 +4689,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv // ORCA: Get layer Zs as doubles std::vector layer_zs = get_layers_zs(); + // loop invariant, same reason as the layer Zs above + const std::vector layer_times = m_viewer.get_layers_estimated_times(); for (Slic3r::CustomGCode::Item custom_gcode : custom_gcode_per_print_z) { ImGui::Dummy({window_padding, window_padding}); @@ -4699,7 +4710,6 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv imgui.text(buf); ImGui::SameLine(max_len * 1.5); - std::vector layer_times = m_viewer.get_layers_estimated_times(); float custom_gcode_time = 0; if (layer > 0) { @@ -4748,7 +4758,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv std::string print_str = _u8L("Model printing time"); std::string total_str = _u8L("Total time"); float max_len = window_padding + 2 * ImGui::GetStyle().ItemSpacing.x; - if (m_viewer.get_layers_estimated_times().empty()) + if (m_viewer.get_layers_count() == 0) max_len += ImGui::CalcTextSize(total_str.c_str()).x; else { if (m_viewer.get_view_type() == libvgcode::EViewType::FeatureType) From 89dc4e1b99f836550b7683779cb1a3a5e4638909 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:43:08 +0800 Subject: [PATCH 18/19] Add an Align to Y Axis Option to the CLI Arrange (#15836) The CLI turned "align to Y axis" on for every i3 printer with no way to opt out. With rotations forbidden the pre-rotation is the result, so every object ends up turned 90 degrees from how it was loaded. The GUI defaults the checkbox the same way for i3 printers, but lets the user untick it. Add --align-to-y-axis. When it is not given the printer-structure rule still applies, so existing calls are unchanged; the CLI's own options are filled with defaults after parsing, so the keys the user typed are remembered to tell the two apart. --- src/OrcaSlicer.cpp | 15 +++++++++++++-- src/OrcaSlicer.hpp | 4 ++++ src/libslic3r/PrintConfig.cpp | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 0c9bd0b1ee..57be0595cb 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #if defined(__linux__) || defined(__LINUX__) #include @@ -1614,6 +1615,10 @@ int CLI::run(int argc, char **argv) ConfigOptionBool* allow_rotations_option = m_config.option("allow_rotations"); if (allow_rotations_option) allow_rotations = allow_rotations_option->value; + // Only an explicit --align-to-y-axis overrides the printer-structure default. + std::optional align_to_y_axis; + if (m_given_option_keys.count("align_to_y_axis") > 0) + align_to_y_axis = m_config.opt_bool("align_to_y_axis"); ConfigOptionBool* skip_modified_gcodes_option = m_config.option("skip_modified_gcodes"); if (skip_modified_gcodes_option) @@ -5298,7 +5303,9 @@ int CLI::run(int argc, char **argv) arrange_cfg.bed_shrink_x = BED_SHRINK_SEQ_PRINT; arrange_cfg.bed_shrink_y = BED_SHRINK_SEQ_PRINT; } - if (auto printer_structure_opt = m_print_config.option>("printer_structure")) { + if (align_to_y_axis.has_value()) { + arrange_cfg.align_to_y_axis = *align_to_y_axis; + } else if (auto printer_structure_opt = m_print_config.option>("printer_structure")) { arrange_cfg.align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3); } @@ -5748,7 +5755,9 @@ int CLI::run(int argc, char **argv) arrange_cfg.bed_shrink_x = BED_SHRINK_SEQ_PRINT; arrange_cfg.bed_shrink_y = BED_SHRINK_SEQ_PRINT; } - if (auto printer_structure_opt = m_print_config.option>("printer_structure")) { + if (align_to_y_axis.has_value()) { + arrange_cfg.align_to_y_axis = *align_to_y_axis; + } else if (auto printer_structure_opt = m_print_config.option>("printer_structure")) { arrange_cfg.align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3); } @@ -7943,6 +7952,8 @@ bool CLI::setup(int argc, char **argv) for (std::string &input_file : m_input_files) input_file = resolve_cli_input_path(input_file); + m_given_option_keys.insert(opt_order.begin(), opt_order.end()); + // Parse actions and transform options. for (auto const &opt_key : opt_order) { if (cli_actions_config_def.has(opt_key)) diff --git a/src/OrcaSlicer.hpp b/src/OrcaSlicer.hpp index e6aada821f..380607e95f 100644 --- a/src/OrcaSlicer.hpp +++ b/src/OrcaSlicer.hpp @@ -1,6 +1,8 @@ #ifndef SLIC3R_HPP #define SLIC3R_HPP +#include + #include "libslic3r/Config.hpp" #include "libslic3r/Model.hpp" @@ -113,6 +115,8 @@ private: std::vector m_input_files; std::vector m_actions; std::vector m_transforms; + // Options the user typed; setup() fills the CLI's own options with defaults afterwards. + std::set m_given_option_keys; std::vector m_models; bool setup(int argc, char **argv); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 92e7067ae5..bf7a651dcc 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -12347,6 +12347,12 @@ CLIMiscConfigDef::CLIMiscConfigDef() def->tooltip = L("If enabled, Arrange will allow rotation when placing objects."); def->set_default_value(new ConfigOptionBool(true)); + def = this->add("align_to_y_axis", coBool); + def->label = L("Align to Y axis when arranging"); + def->tooltip = L("If enabled, Arrange will turn each object so its long side runs along the Y axis before placing it. " + "When not given, it is on for i3 printers and off for the others, as in the GUI."); + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("avoid_extrusion_cali_region", coBool); def->label = L("Avoid extrusion calibrate region when arranging"); def->tooltip = L("If enabled, Arrange will avoid extrusion calibrate region when placing objects."); From ef956b995ae79aa37aee43293d45bbdfa43abca4 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:57:15 +0800 Subject: [PATCH 19/19] Pick the Parity Build From the Unfiltered Run List and Allow Pinning One (#15823) The nightly found its build with a filtered run listing (branch=main, status=success) and trusted the first result. GitHub serves filtered listings from a run search index that has intermittently returned weeks-old results, so some nights tested a build from weeks earlier and reported its differences as regressions. The same filter also matched fork PR builds whose branch is named main. The build is now picked from the unfiltered listing, which stays current, and filtered here: a successful build_all run of this repository on the requested branch. Fork PR builds are excluded by repository. A feature branch is normally built only for its PR, so this repository's own PR builds stay eligible, but a PR build compiles the PR merged into its base rather than the head commit the later jobs check out, so a push or dispatch build of the branch is preferred when the same page of the listing has one. A scheduled run fails instead of testing a build more than 48 hours old, and every run names the build it tested in the job summary. Manual runs scan further back, so a branch that last built weeks ago can still be tested, and a new build_run_id input pins one build_all run, read directly rather than through a search. --- .github/workflows/parity_nightly.yml | 60 ++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/.github/workflows/parity_nightly.yml b/.github/workflows/parity_nightly.yml index 79f5c9b514..44937fdad5 100644 --- a/.github/workflows/parity_nightly.yml +++ b/.github/workflows/parity_nightly.yml @@ -5,8 +5,9 @@ # re-sliced on its own to see whether it changes the G-code # harness - the GUI-vs-CLI parity harness (metrics only, never fails) # Both test the latest successful build_all.yml Linux AppImage from main, with -# sources checked out at the commit that build was made from. Nothing here -# gates a build or a PR. +# sources checked out at the commit that build was made from; a manual run can +# name another branch, or pin one build by its run id. Nothing here gates a +# build or a PR. name: Parity Nightly on: @@ -20,9 +21,13 @@ on: required: false default: "main" build_branch: - description: "branch whose latest successful build_all artifact to test" + description: "branch whose newest successful build_all artifact to test (a PR build is the PR merged into its base; sources are checked out at the PR head)" required: false default: "main" + build_run_id: + description: "build_all run id to test instead of build_branch's newest (same PR caveat)" + required: false + default: "" fixtures: description: "harness fixture ids, space-separated (empty = all)" required: false @@ -50,14 +55,53 @@ jobs: env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} + BRANCH: ${{ inputs.build_branch || 'main' }} + RUN_ID: ${{ inputs.build_run_id }} + SCHEDULED: ${{ github.event_name == 'schedule' }} run: | set -euo pipefail - gh run list --workflow build_all.yml \ - --branch "${{ inputs.build_branch || 'main' }}" \ - --status success --limit 1 --json databaseId,headSha \ - --jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \ - >> "$GITHUB_OUTPUT" + if [ -n "$RUN_ID" ]; then + [[ $RUN_ID =~ ^[0-9]+$ ]] || { echo "build_run_id must be a numeric run id, got '$RUN_ID'" >&2; exit 1; } + # a pinned build is read directly, not through a search; it must come + # from this repository, because the later jobs check out its commit here + found=$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID" --jq \ + 'select(.path == ".github/workflows/build_all.yml" and .conclusion == "success" + and .head_repository.full_name == env.GH_REPO) + | "\(.id) \(.head_sha) \(.created_at)"') + [ -n "$found" ] || { echo "run $RUN_ID is not a successful build_all run of $GH_REPO" >&2; exit 1; } + else + # GitHub serves filtered run listings (branch=, status=, head_sha=, ...) + # from a search index that has returned weeks-old results, while the + # unfiltered listing stays current, so list unfiltered and filter here. + # The repository check keeps out fork PRs whose branch has the same + # name. A feature branch is normally built only for its PR, and a PR + # build compiles the PR merged into its base rather than head_sha, so + # a build of the branch itself (push or dispatch) is preferred when + # the same page has one. + pick='([.workflow_runs[] | select(.head_branch == env.BRANCH and .conclusion == "success" + and .head_repository.full_name == env.GH_REPO)] + | map(select(.event != "pull_request"))[0] // .[0]) + | select(.) | "\(.id) \(.head_sha) \(.created_at)"' + # a page of 100 runs spans about a day and a half; a manual run may + # target a branch that last built weeks ago + pages=3 + if [ "$SCHEDULED" != true ]; then pages=20; fi + found="" + for page in $(seq "$pages"); do + found=$(gh api "repos/$GH_REPO/actions/workflows/build_all.yml/runs?per_page=100&page=$page" --jq "$pick") + if [ -n "$found" ]; then break; fi + done + [ -n "$found" ] || { echo "no successful $BRANCH build among the last $((pages * 100)) build_all runs; pass build_run_id to test an older one" >&2; exit 1; } + fi + read -r run_id head_sha created <<< "$found" + # the nightly fails rather than report on a stale build + if [ "$SCHEDULED" = true ] && [ $(( $(date +%s) - $(date -d "$created" +%s) )) -gt 172800 ]; then + echo "newest $BRANCH build $run_id is from $created, over 48 hours old" >&2 + exit 1 + fi + printf 'run_id=%s\nhead_sha=%s\n' "$run_id" "$head_sha" >> "$GITHUB_OUTPUT" cat "$GITHUB_OUTPUT" + echo "Testing build [$run_id](https://github.com/$GH_REPO/actions/runs/$run_id) of \`$head_sha\`, built $created" >> "$GITHUB_STEP_SUMMARY" effect: name: Override sweep effect stage (shard ${{ matrix.shard }})