Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ set(OPENVDB_FIND_MODULE_PATH "" CACHE PATH "Path to OpenVDB installation's find

set(SLIC3R_GTK "2" CACHE STRING "GTK version to use with wxWidgets on Linux")

# Empty leaves Discord Rich Presence inert.
set(SLIC3R_DISCORD_APP_ID "" CACHE STRING "Discord application id for Rich Presence")

set(IS_CROSS_COMPILE FALSE)

set(FLATPAK FALSE CACHE BOOL "Not copy FFMPEG file")
Expand Down
1 change: 1 addition & 0 deletions bbl/i18n/list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ src/slic3r/GUI/BedShapeDialog.hpp
src/slic3r/GUI/ConfigManipulation.cpp
src/slic3r/GUI/DeviceManager.cpp
src/slic3r/GUI/DeviceErrorDialog.cpp
src/slic3r/GUI/DiscordPresenceSnapshot.cpp
src/slic3r/GUI/ExtraRenderers.cpp
src/slic3r/GUI/Field.cpp
src/slic3r/GUI/GCodeRenderer/AdvancedRenderer.cpp
Expand Down
Binary file added resources/images/discord/bambu_studio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/discord/state_done.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/discord/state_error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/discord/state_idle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/discord/state_paused.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/discord/state_printing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/images/discord/state_slicing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/libslic3r/AppConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ void AppConfig::set_defaults()

if (get("single_instance").empty())
set_bool("single_instance", false);
// Discord Rich Presence sends activity off the machine, so it is opt-in.
if (get("discord_rich_presence").empty())
set_bool("discord_rich_presence", false);
if (get("discord_rich_presence_hide_names").empty())
set_bool("discord_rich_presence_hide_names", false);
if (get("import_3mf_as_project").empty())
set_bool("import_3mf_as_project", true);
if (get("show_bed_heat_soak_area").empty())
Expand Down
10 changes: 10 additions & 0 deletions src/slic3r/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ set(SLIC3R_GUI_SOURCES
GUI/ImageMessageDialog.cpp
GUI/DeviceManager.hpp
GUI/DeviceManager.cpp
GUI/DiscordPresenceSnapshot.hpp
GUI/DiscordPresenceSnapshot.cpp
GUI/DeviceErrorDialog.hpp
GUI/DeviceErrorDialog.cpp
GUI/UserManager.hpp
Expand Down Expand Up @@ -605,6 +607,10 @@ set(SLIC3R_GUI_SOURCES
Utils/EmbossStyleManager.hpp
Utils/Http.cpp
Utils/Http.hpp
Utils/DiscordIPC.cpp
Utils/DiscordIPC.hpp
Utils/DiscordPresence.cpp
Utils/DiscordPresence.hpp
Utils/FixModelByWin10.cpp
Utils/FixModelByWin10.hpp
Utils/Bonjour.cpp
Expand Down Expand Up @@ -841,3 +847,7 @@ endif ()

# Add a definition so that we can tell we are compiling slic3r.
target_compile_definitions(libslic3r_gui PRIVATE SLIC3R_CURRENTLY_COMPILING_GUI_MODULE)

if (SLIC3R_DISCORD_APP_ID)
target_compile_definitions(libslic3r_gui PRIVATE SLIC3R_DISCORD_APP_ID="${SLIC3R_DISCORD_APP_ID}")
endif ()
184 changes: 184 additions & 0 deletions src/slic3r/GUI/DiscordPresenceSnapshot.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#include "DiscordPresenceSnapshot.hpp"

#include <chrono>

#include "DeviceManager.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "PartPlate.hpp"
#include "Plater.hpp"
#include "DeviceCore/DevManager.h"

#include "libslic3r/Model.hpp"

namespace Slic3r {
namespace GUI {

using Activity = PresenceSnapshot::Activity;

static int64_t unix_now()
{
using namespace std::chrono;
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
}

// Elapsed-time origin for states with no more meaningful one of their own.
static int64_t session_start_time()
{
static const int64_t started = unix_now();
return started;
}

// A printer keeps reporting FINISH until the next job starts, so terminal
// states have to expire or the profile sticks on "Print complete" for hours.
static const int64_t TERMINAL_STATUS_LINGER_S = 5 * 60;

// GUI thread only.
static int64_t seconds_in_status(const std::string &status_key)
{
static std::string last_key;
static int64_t entered_at = 0;

const int64_t now = unix_now();
if (status_key != last_key) {
last_key = status_key;
entered_at = now;
}
return now - entered_at;
}

static std::string percent_suffix(int percent)
{
if (percent < 0)
return std::string();
return " - " + std::to_string(percent) + "%";
}

// False when there is no printer worth reporting.
static bool collect_from_printer(PresenceSnapshot &snapshot, bool hide_names)
{
DeviceManager *dev = wxGetApp().getDeviceManager();
if (dev == nullptr)
return false;

MachineObject *obj = dev->get_selected_machine();
if (obj == nullptr || !obj->is_connected())
return false;

snapshot.small_text = obj->get_printer_type_display_str().ToUTF8().data();

const std::string job_name = obj->subtask_name;
snapshot.details = (hide_names || job_name.empty()) ? _u8L("A print job") : job_name;

// Tracked on every call so a RUNNING -> FINISH transition is seen at once.
const int64_t status_age = seconds_in_status(obj->print_status + "|" + job_name);

const int percent = obj->mc_print_percent;

if (obj->is_in_printing_pause()) {
snapshot.activity = Activity::Paused;
snapshot.state = _u8L("Paused") + percent_suffix(percent);
return true;
}

// is_in_printing() also covers PREPARE and on-device SLICING, which can
// each last minutes; "Printing - 0%" would be misleading.
if (obj->print_status == "PREPARE") {
snapshot.activity = Activity::Printing;
snapshot.state = _u8L("Preparing");
return true;
}

if (obj->print_status == "SLICING") {
snapshot.activity = Activity::Slicing;
snapshot.state = _u8L("Slicing on the printer");
return true;
}

if (obj->is_in_printing()) {
snapshot.activity = Activity::Printing;
snapshot.state = _u8L("Printing") + percent_suffix(percent);
// An absolute end time lets Discord run the countdown itself.
if (obj->mc_left_time > 0)
snapshot.end_time = unix_now() + obj->mc_left_time;
return true;
}

if (status_age <= TERMINAL_STATUS_LINGER_S) {
if (obj->print_status == "FAILED") {
snapshot.activity = Activity::Failed;
snapshot.state = _u8L("Print failed");
return true;
}
if (obj->print_status == "FINISH") {
snapshot.activity = Activity::Finished;
snapshot.state = _u8L("Print complete");
return true;
}
}

// Connected but idle: local activity is more interesting than "printer is on".
snapshot = PresenceSnapshot();
return false;
}

static void collect_from_plater(PresenceSnapshot &snapshot, bool hide_names)
{
snapshot.start_time = session_start_time();

Plater *plater = wxGetApp().plater();
if (plater == nullptr) {
snapshot.activity = Activity::Idle;
snapshot.state = _u8L("Idle");
return;
}

const std::string project = plater->get_project_name().ToUTF8().data();
const size_t objects = plater->model().objects.size();

if (objects == 0) {
snapshot.activity = Activity::Idle;
snapshot.state = _u8L("Idle");
return;
}

snapshot.details = (hide_names || project.empty()) ? _u8L("Working on a project") : project;

// PartPlate holds the percentage the progress bar is driven from: -1 when
// unsliced, 0..100 while slicing, 100 once the result is valid.
PartPlate *plate = plater->get_partplate_list().get_curr_plate();
if (plate != nullptr && !plate->is_slice_result_valid()) {
const float percent = plate->get_slicing_percent();
if (percent >= 0.f && percent < 100.f) {
snapshot.activity = Activity::Slicing;
snapshot.state = _u8L("Slicing") + percent_suffix(static_cast<int>(percent));
return;
}
}

if (plater->is_preview_shown()) {
snapshot.activity = Activity::Previewing;
snapshot.state = _u8L("Previewing");
return;
}

snapshot.activity = Activity::Editing;
snapshot.state = wxString::Format(_L_PLURAL("Editing - %d object", "Editing - %d objects", (int) objects),
(int) objects)
.ToUTF8()
.data();
}

PresenceSnapshot collect_presence_snapshot(bool hide_names)
{
PresenceSnapshot snapshot;

if (collect_from_printer(snapshot, hide_names))
return snapshot;

collect_from_plater(snapshot, hide_names);
return snapshot;
}

} // namespace GUI
} // namespace Slic3r
20 changes: 20 additions & 0 deletions src/slic3r/GUI/DiscordPresenceSnapshot.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef slic3r_GUI_DiscordPresenceSnapshot_hpp_
#define slic3r_GUI_DiscordPresenceSnapshot_hpp_

#include "slic3r/Utils/DiscordPresence.hpp"

namespace Slic3r {
namespace GUI {

// Read the current application and printer state and describe it as a snapshot
// ready to publish. Must be called on the GUI thread; safe to call before the
// Plater exists (it reports Idle).
//
// hide_names honours the privacy preference: progress and state are kept, but
// project and print job names are replaced with generic wording.
PresenceSnapshot collect_presence_snapshot(bool hide_names);

} // namespace GUI
} // namespace Slic3r

#endif // slic3r_GUI_DiscordPresenceSnapshot_hpp_
54 changes: 54 additions & 0 deletions src/slic3r/GUI/GUI_App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@

#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "DiscordPresenceSnapshot.hpp"
#include "slic3r/Utils/DiscordPresence.hpp"
#include "3DScene.hpp"
#include "MainFrame.hpp"
#include "slic3r/GUI/Widgets/WebView.hpp"
Expand Down Expand Up @@ -2840,12 +2842,53 @@ bool GUI_App::OnInit()
}
}

void GUI_App::apply_discord_presence_setting()
{
if (!app_config)
return;

const bool enabled = app_config->get_bool("discord_rich_presence");

// Opted out and never opted in: do not even construct the publisher, so the
// feature costs nothing at all for users who do not want it.
if (!enabled && !m_discord_presence)
return;

// Config overrides the compiled-in id, so no rebuild is needed to retarget.
std::string application_id = app_config->get("discord_rich_presence_app_id");
if (application_id.empty())
application_id = Slic3r::discord_default_application_id();

// The id is fixed for the publisher's lifetime, so a change must replace it.
if (m_discord_presence && m_discord_presence->application_id() != application_id)
m_discord_presence.reset();

if (!m_discord_presence)
m_discord_presence.reset(
new Slic3r::DiscordPresence(application_id, std::string("Bambu Studio ") + SLIC3R_VERSION));

m_discord_presence->set_enabled(enabled);
}

void GUI_App::update_discord_presence()
{
if (!m_discord_presence || !m_discord_presence->is_enabled())
return;

const bool hide_names = app_config && app_config->get_bool("discord_rich_presence_hide_names");
m_discord_presence->update(collect_presence_snapshot(hide_names));
}

int GUI_App::OnExit()
{
#ifdef __APPLE__
UnRegisterMacPowerCallBack();
#endif

// Stop before the device manager goes away: the worker formats state that
// was read from it, and its destructor clears the presence from the profile.
m_discord_presence.reset();

Slic3r::HelioQuery::shutdown_background_requests();

stop_sync_user_preset();
Expand Down Expand Up @@ -3605,6 +3648,15 @@ bool GUI_App::on_init_inner()
app_config->save();
}

// Discord Rich Presence. This only bounds how often the state is read;
// the publisher does its own change detection and rate limiting, and
// returns immediately when the feature is switched off.
{
static auto s_last_discord_update = std::chrono::steady_clock::time_point{};
if (Slic3r::debounce_elapsed(s_last_discord_update, std::chrono::seconds(1)))
update_discord_presence();
}

// BBS
//this->obj_manipul()->update_if_dirty();

Expand Down Expand Up @@ -3634,6 +3686,8 @@ bool GUI_App::on_init_inner()
}
});

apply_discord_presence_setting();

m_initialized = true;

flush_logs();
Expand Down
6 changes: 6 additions & 0 deletions src/slic3r/GUI/GUI_App.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class UserManager;
class DeviceManager;
class NetworkAgent;
class TaskManager;
class DiscordPresence;

namespace GUI{

Expand Down Expand Up @@ -331,6 +332,7 @@ class GUI_App : public wxApp
std::unique_ptr<PrintHostJobQueue> m_printhost_job_queue;
std::unique_ptr <OtherInstanceMessageHandler> m_other_instance_message_handler;
std::unique_ptr <wxSingleInstanceChecker> m_single_instance_checker;
std::unique_ptr<Slic3r::DiscordPresence> m_discord_presence;
std::string m_instance_hash_string;
size_t m_instance_hash_int;

Expand Down Expand Up @@ -403,6 +405,10 @@ class GUI_App : public wxApp
bool confirm_mesh_paint_warning();
EAppMode get_app_mode() const { return m_app_mode; }
Slic3r::DeviceManager* getDeviceManager() { return m_device_manager; }
// Discord Rich Presence. apply_* reads the preference and starts or stops
// the publisher; update_* pushes the current state and is driven from idle.
void apply_discord_presence_setting();
void update_discord_presence();
bool is_blocking_printing(MachineObject *obj_ = nullptr);
Slic3r::TaskManager* getTaskManager() { return m_task_manager; }
wgtFilaManagerStore* fila_manager_store() { return m_fila_manager_store; }
Expand Down
Loading