mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 01:40:57 +00:00
feat: initial draft of lifecycle events API for plugins (#15293)
# Description
This PR introduces lifecycle events to the plugin API.
For all plugin capabilities, you can define a `on_lifecycle_event`
function in the plugin that takes in a event enum and a small payload
for some generic information on the lifecycle event.
The idea is to keep the payload generic and small, and if you want to
get more information, you should invoke other more targeted APIs to get
more information.
For example, lets say you are keeping track of the the `ObjectAdded*`
event hook for model transformation, addition or deletion. The payload
would tell you the name of the model, and you should use a targeted API
such as `orca.host.plater().model()` to get more information on the
model. This is the overall design principle of the API.
Currently the lifecycle events are the following:
```cpp
enum class LifecycleEvent {
// Project (3mf)
NewProject,
ProjectOpened,
ProjectBeforeSave,
ProjectAfterSave,
ProjectClosed,
ProjectDirtyChanged,
// Slicing pipeline
SliceStarted,
SliceGeometryFinished,
GCodeExportStarted,
GCodeExportFinished,
SlicingJobComplete,
// Plate/model editing
ObjectAdded,
ObjectDeleted,
ObjectTransformed,
ObjectChanged,
ObjectRenamed,
PlateCreated,
PlateDeleted,
PlateSelected,
PlateRenamed,
// Preset
PresetSelected,
PresetSaved,
// Printer/device
PrintStateChanged,
DeviceOnlineChanged,
DeviceDiscovered,
DeviceSelected,
DeviceConnected,
DeviceDisconnected,
UploadStarted,
UploadFinished,
// Print/send jobs
PrintJobStarted,
PrintJobFinished,
SendJobStarted,
SendJobFinished,
};
```
This is an initial draft and lifecycle events can be included later on.
[orca_telegram_notifier_plugin_any.py](https://github.com/user-attachments/files/31220973/orca_telegram_notifier_plugin_any.py)
If you're familiar with telegram bots, after you install the telegram
bot, in the config of this plugin, you can enter the Bot ID and the Chat
ID with said bot.
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
For this plugin, I am testing it with a telegram bot that sends me a
message on lifecycle event.
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
+51
-5
@@ -8,6 +8,7 @@
|
||||
#include "I18N.hpp"
|
||||
#include "GCode.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "LifecycleEvents.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "EdgeGrid.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
@@ -2476,6 +2477,15 @@ 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 = 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);
|
||||
}
|
||||
|
||||
// check if any custom gcode contains keywords used by the gcode processor to
|
||||
// produce time estimation and gcode toolpaths
|
||||
std::vector<std::pair<std::string, std::string>> validation_res = DoExport::validate_custom_gcode(*print);
|
||||
@@ -2511,12 +2521,23 @@ 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 = 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);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -2527,11 +2548,19 @@ 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 = 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;
|
||||
}
|
||||
file.close();
|
||||
@@ -2637,6 +2666,14 @@ 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 = 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(
|
||||
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');
|
||||
@@ -2647,7 +2684,16 @@ 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 = 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);
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
#pragma once
|
||||
|
||||
// LifecycleEvents.hpp
|
||||
// --------------------
|
||||
// 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.
|
||||
|
||||
#include <functional>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r
|
||||
{
|
||||
enum class LifecycleEvent {
|
||||
// Project (3mf)
|
||||
NewProject,
|
||||
ProjectOpened,
|
||||
ProjectBeforeSave,
|
||||
ProjectAfterSave,
|
||||
ProjectClosed,
|
||||
ProjectDirtyChanged,
|
||||
|
||||
// Slicing pipeline
|
||||
SliceStarted,
|
||||
SliceGeometryFinished,
|
||||
GCodeExportStarted,
|
||||
GCodeExportFinished,
|
||||
SlicingJobComplete,
|
||||
|
||||
// Plate/model editing
|
||||
ObjectAdded,
|
||||
ObjectDeleted,
|
||||
ObjectTransformed,
|
||||
ObjectChanged,
|
||||
ObjectRenamed,
|
||||
PlateCreated,
|
||||
PlateDeleted,
|
||||
PlateSelected,
|
||||
PlateRenamed,
|
||||
|
||||
// Preset
|
||||
PresetSelected,
|
||||
PresetSaved,
|
||||
|
||||
// Printer/device
|
||||
PrintStateChanged,
|
||||
DeviceOnline,
|
||||
DeviceOffline,
|
||||
DeviceDiscovered,
|
||||
DeviceSelected,
|
||||
UploadStarted,
|
||||
UploadFinished,
|
||||
|
||||
// Print/send jobs
|
||||
PrintJobStarted,
|
||||
PrintJobFinished,
|
||||
SendJobStarted,
|
||||
SendJobFinished,
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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<bool()> cancellation_check;
|
||||
};
|
||||
|
||||
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::ProjectDirtyChanged: return "ProjectDirtyChanged";
|
||||
|
||||
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::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";
|
||||
case LifecycleEvent::PrintStateChanged: return "PrintStateChanged";
|
||||
|
||||
case LifecycleEvent::DeviceOnline: return "DeviceOnline";
|
||||
case LifecycleEvent::DeviceOffline: return "DeviceOffline";
|
||||
case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered";
|
||||
case LifecycleEvent::DeviceSelected: return "DeviceSelected";
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
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<void(LifecycleEvent, const LifecycleEventContext&)>;
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct LifecycleHookState
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
class LifecycleDispatchGuard
|
||||
{
|
||||
public:
|
||||
explicit LifecycleDispatchGuard(LifecycleHookState& state) : m_state(state) {}
|
||||
|
||||
~LifecycleDispatchGuard()
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> lock(state.mutex);
|
||||
state.fn = std::move(fn);
|
||||
state.accepting = true;
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> 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)
|
||||
{
|
||||
detail::LifecycleHookState& state = detail::lifecycle_hook_state();
|
||||
LifecycleHookFn fn;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
if (!state.accepting || !state.fn)
|
||||
return;
|
||||
fn = state.fn;
|
||||
++state.active_dispatches;
|
||||
}
|
||||
|
||||
detail::LifecycleDispatchGuard guard(state);
|
||||
fn(event, ctx);
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "LifecycleEvents.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "Time.hpp"
|
||||
#include "PlaceholderParser.hpp"
|
||||
@@ -2972,6 +2973,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;
|
||||
@@ -3079,6 +3081,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);
|
||||
}
|
||||
}
|
||||
|
||||
// A detached standalone preset for the Full Publish receiver: create a user preset holding
|
||||
|
||||
+44
-1
@@ -14,6 +14,7 @@
|
||||
#include "Flow.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "LifecycleEvents.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "Thread.hpp"
|
||||
#include "Time.hpp"
|
||||
@@ -2694,6 +2695,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
if (m_objects.empty())
|
||||
return;
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
for (PrintObject *obj : m_objects)
|
||||
obj->clear_shared_object();
|
||||
|
||||
@@ -3312,6 +3321,14 @@ 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.name = std::to_string(m_model.id().id);
|
||||
ctx.code = LifecycleEvtCode::Ok;
|
||||
ctx.cancellation_check = [this]() { return canceled(); };
|
||||
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// G-code export process, running at a background thread.
|
||||
@@ -4911,6 +4928,15 @@ 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 = 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);
|
||||
}
|
||||
|
||||
try {
|
||||
GCodeProcessor processor;
|
||||
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
|
||||
@@ -4930,13 +4956,30 @@ 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 = 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(
|
||||
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 = 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);
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
|
||||
|
||||
@@ -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 "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
|
||||
#include "libslic3r/Time.hpp"
|
||||
@@ -355,7 +356,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
|
||||
// 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);
|
||||
/* if (!obj->dev_ip.empty()) {
|
||||
Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip);
|
||||
@@ -373,6 +377,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 DeviceOnline/DeviceOffline when its reachability actually changes.
|
||||
obj->m_is_online = true;
|
||||
|
||||
//load access code
|
||||
@@ -389,6 +397,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);
|
||||
}
|
||||
@@ -984,8 +1001,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)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "libslic3r/Time.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
|
||||
@@ -2626,7 +2627,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)
|
||||
@@ -2648,7 +2657,10 @@ int MachineObject::connect(bool use_openssl)
|
||||
int MachineObject::disconnect()
|
||||
{
|
||||
if (m_agent) {
|
||||
return m_agent->disconnect_printer();
|
||||
const int result = m_agent->disconnect_printer();
|
||||
if (result == 0)
|
||||
set_online_state(false);
|
||||
return result;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -2678,8 +2690,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(on_off ? LifecycleEvent::DeviceOnline : LifecycleEvent::DeviceOffline, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
bool MachineObject::is_info_ready(bool check_version) const
|
||||
@@ -2806,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<std::chrono::microseconds>(curr_time - last_update_time);
|
||||
|
||||
/* update last received time */
|
||||
last_update_time = std::chrono::system_clock::now();
|
||||
|
||||
json j_pre;
|
||||
bool parse_ok = false;
|
||||
@@ -2825,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<std::string>() == "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<std::chrono::microseconds>(curr_time - last_update_time);
|
||||
|
||||
/* update last received time */
|
||||
last_update_time = std::chrono::system_clock::now();
|
||||
|
||||
try {
|
||||
bool restored_json = false;
|
||||
json j;
|
||||
@@ -4597,19 +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<std::string>() == "client.disconnected")
|
||||
set_online_state(false);
|
||||
else if (j["event"]["event"].get<std::string>() == "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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "slic3r/Utils/MacDarkMode.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include <slic3r/GUI/GUI_Utils.hpp>
|
||||
|
||||
@@ -5028,8 +5029,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++) {
|
||||
@@ -5150,8 +5159,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;
|
||||
}
|
||||
@@ -5242,8 +5259,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;
|
||||
}
|
||||
|
||||
@@ -2182,6 +2182,8 @@ 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());
|
||||
|
||||
obj->set_online_state(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2220,6 +2222,8 @@ void GUI_App::init_networking_callbacks()
|
||||
obj->command_get_version();
|
||||
event.SetInt(0);
|
||||
event.SetString(obj->get_dev_id());
|
||||
|
||||
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;
|
||||
@@ -2235,10 +2239,14 @@ void GUI_App::init_networking_callbacks()
|
||||
wxGetApp().show_dialog(text);
|
||||
}
|
||||
event.SetInt(-1);
|
||||
|
||||
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";
|
||||
|
||||
obj->set_online_state(false);
|
||||
} else {
|
||||
event.SetInt(-1);
|
||||
BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state;
|
||||
|
||||
@@ -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"
|
||||
@@ -10,6 +11,7 @@
|
||||
#include "BitmapComboBox.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "Tab.hpp"
|
||||
@@ -1159,17 +1161,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
|
||||
@@ -3519,7 +3543,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;
|
||||
}
|
||||
@@ -4088,6 +4119,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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -698,6 +699,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<double>(ap.translation(X)) % unscale<double>(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();
|
||||
|
||||
@@ -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 <numeric>
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "PrintJob.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
@@ -152,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();
|
||||
@@ -173,7 +186,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") {
|
||||
@@ -372,11 +385,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 = "";
|
||||
}
|
||||
|
||||
@@ -543,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())
|
||||
@@ -566,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);
|
||||
@@ -582,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 = "";
|
||||
@@ -597,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) {
|
||||
@@ -619,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;
|
||||
}
|
||||
@@ -629,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:
|
||||
@@ -686,6 +707,7 @@ void PrintJob::process(Ctl &ctl)
|
||||
}
|
||||
wxQueueEvent(m_plater, evt);
|
||||
m_job_finished = true;
|
||||
m_lifecycle_success = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,6 +720,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;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ class PrintJob : public Job
|
||||
std::function<void()> 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<void()> m_enter_ip_address_fun_fail{ nullptr };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SendJob.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
@@ -148,6 +149,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);
|
||||
@@ -424,6 +432,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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -29,6 +29,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"
|
||||
@@ -2626,11 +2627,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
|
||||
@@ -4741,9 +4751,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;
|
||||
}
|
||||
@@ -4842,6 +4859,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
|
||||
@@ -4924,6 +4942,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) {
|
||||
@@ -5032,6 +5057,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) {
|
||||
@@ -5058,6 +5084,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;
|
||||
}
|
||||
|
||||
|
||||
+105
-13
@@ -57,6 +57,7 @@
|
||||
#include <wx/aui/aui.h>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "libslic3r/Format/STL.hpp"
|
||||
#include "libslic3r/Format/DRC.hpp"
|
||||
#include "libslic3r/Format/STEP.hpp"
|
||||
@@ -10264,7 +10265,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();
|
||||
@@ -10297,7 +10305,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);
|
||||
|
||||
@@ -10343,10 +10358,22 @@ 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();
|
||||
|
||||
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);
|
||||
}
|
||||
// A new project must not inherit the previous project's published selection (Feature A/B).
|
||||
m_has_pending_published = false;
|
||||
m_pending_published_keys.clear();
|
||||
@@ -12774,11 +12801,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.
|
||||
@@ -12816,6 +12847,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());
|
||||
@@ -15349,21 +15388,33 @@ 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());
|
||||
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();
|
||||
if (!silent) {
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx);
|
||||
}
|
||||
{
|
||||
// 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
|
||||
@@ -15481,6 +15532,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;
|
||||
@@ -15552,11 +15610,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);
|
||||
|
||||
@@ -20889,6 +20959,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)
|
||||
@@ -20925,6 +21003,20 @@ void Plater::changed_objects(const std::vector<size_t>& 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<int>(obj_idx);
|
||||
ctx.source = "geometry";
|
||||
fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Plater::schedule_background_process(bool schedule/* = true*/)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "MainFrame.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<std::string, Preset::TYPE_COUNT> m_initial_presets;
|
||||
DynamicPrintConfig m_initial_project_config;
|
||||
|
||||
@@ -38,6 +38,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"
|
||||
@@ -6964,6 +6965,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]() {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "TaskManager.hpp"
|
||||
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
|
||||
#include <exception>
|
||||
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -214,17 +217,34 @@ int TaskManager::schedule(TaskStateInfo* task)
|
||||
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);
|
||||
@@ -237,6 +257,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();
|
||||
|
||||
@@ -24,6 +24,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"
|
||||
@@ -360,12 +361,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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "PluginManager.hpp"
|
||||
|
||||
#include <exception>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <memory>
|
||||
#include <pybind11/embed.h>
|
||||
@@ -134,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.
|
||||
@@ -2089,4 +2091,33 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k
|
||||
return result;
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <libslic3r/Config.hpp>
|
||||
#include <libslic3r/LifecycleEvents.hpp>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -20,7 +21,6 @@
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include "CloudPluginService.hpp"
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginDescriptor.hpp"
|
||||
#include "PluginLoader.hpp"
|
||||
#include "PluginConfig.hpp"
|
||||
@@ -210,6 +210,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;
|
||||
|
||||
@@ -132,6 +132,11 @@ public:
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
|
||||
}
|
||||
|
||||
void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabilityInterface>
|
||||
|
||||
@@ -377,6 +377,61 @@ void bind_python_api(pybind11::module_& m)
|
||||
.value("FatalError", PluginResult::FatalError)
|
||||
.export_values();
|
||||
|
||||
py::enum_<LifecycleEvent>(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("ProjectDirtyChanged", LifecycleEvent::ProjectDirtyChanged)
|
||||
.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("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)
|
||||
.value("DeviceOnline", LifecycleEvent::DeviceOnline)
|
||||
.value("DeviceOffline", LifecycleEvent::DeviceOffline)
|
||||
.value("DeviceDiscovered", LifecycleEvent::DeviceDiscovered)
|
||||
.value("DeviceSelected", LifecycleEvent::DeviceSelected)
|
||||
.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_<LifecycleEvtCode>(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext")
|
||||
.value("Ok", LifecycleEvtCode::Ok)
|
||||
.value("Error", LifecycleEvtCode::Error)
|
||||
.value("Warn", LifecycleEvtCode::Warn)
|
||||
.export_values();
|
||||
|
||||
py::class_<LifecycleEventContext>(m, "LifecycleEventContext", "Payload accompanying a LifecycleEvent")
|
||||
.def(py::init<>())
|
||||
.def_readonly("name", &LifecycleEventContext::name)
|
||||
.def_readonly("code", &LifecycleEventContext::code)
|
||||
.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_<PluginContext>(m, "PluginContext", "Context shared with plugin entry points")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("orca_version", &PluginContext::orca_version);
|
||||
@@ -401,6 +456,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"
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/embed.h>
|
||||
|
||||
#include <libslic3r/LifecycleEvents.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Pages, 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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <libslic3r/LifecycleEvents.hpp>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
@@ -12,8 +13,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
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<std::mutex> 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<std::mutex> lock(mutex);
|
||||
callback_was_entered = cv.wait_for(lock, std::chrono::seconds(5), [&] { return callback_entered; });
|
||||
}
|
||||
if (!callback_was_entered) {
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex);
|
||||
shutdown_started = true;
|
||||
cv.notify_all();
|
||||
}
|
||||
set_lifecycle_hook_fn(nullptr);
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
shutdown_finished = true;
|
||||
cv.notify_all();
|
||||
});
|
||||
|
||||
bool shutdown_was_started = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> 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;
|
||||
|
||||
Reference in New Issue
Block a user