Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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