Merge branch 'main' into feature/filament_id

This commit is contained in:
SoftFever
2026-08-30 20:54:33 +08:00
258 changed files with 42147 additions and 2744 deletions
+34 -18
View File
@@ -38,6 +38,8 @@ set(SLIC3R_GUI_SOURCES
GUI/AuxiliaryDataViewModel.hpp
GUI/AuxiliaryDialog.cpp
GUI/AuxiliaryDialog.hpp
GUI/AVVideoDecoder.cpp
GUI/AVVideoDecoder.hpp
GUI/Auxiliary.hpp
GUI/BackgroundSlicingProcess.cpp
GUI/BackgroundSlicingProcess.hpp
@@ -353,6 +355,16 @@ set(SLIC3R_GUI_SOURCES
GUI/Monitor.hpp
GUI/MonitorPage.cpp
GUI/MonitorPage.hpp
GUI/MixedFilamentDialog.cpp
GUI/MixedFilamentDialog.hpp
GUI/GradientCurveEditor.cpp
GUI/GradientCurveEditor.hpp
GUI/ColorDecomposeDialog.cpp
GUI/ColorDecomposeDialog.hpp
GUI/ColorDecomposeSupport.cpp
GUI/ColorDecomposeSupport.hpp
GUI/TextureImportDialog.cpp
GUI/TextureImportDialog.hpp
GUI/Mouse3DController.cpp
GUI/Mouse3DController.hpp
GUI/MsgDialog.cpp
@@ -601,6 +613,8 @@ set(SLIC3R_GUI_SOURCES
GUI/WipeTowerDialog.cpp
GUI/wxExtensions.cpp
GUI/wxExtensions.hpp
GUI/wxMediaCtrl3.cpp
GUI/wxMediaCtrl3.h
plugin/PythonInterpreter.cpp
plugin/PythonInterpreter.hpp
plugin/PythonPluginBridge.cpp
@@ -789,21 +803,8 @@ if (APPLE)
GUI/DeepLinkHandlerMac.mm
GUI/DeepLinkHandlerMac.h
GUI/GUI_UtilsMac.mm
GUI/wxMediaCtrl2.mm
GUI/wxMediaCtrl2.h
)
FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration)
else ()
list(APPEND SLIC3R_GUI_SOURCES
GUI/wxMediaCtrl2.cpp
GUI/wxMediaCtrl2.h
)
endif ()
if (UNIX AND NOT APPLE)
list(APPEND SLIC3R_GUI_SOURCES
GUI/Printer/gstbambusrc.c
)
endif ()
set(ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY}")
@@ -903,6 +904,26 @@ if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY)
add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE)
endif ()
if (APPLE)
# Static FFmpeg from the deps install: nothing to bundle into the .app,
# no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil.
find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "Static FFmpeg (libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.")
endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec
libswscale
libavutil
)
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
endif()
# We need to implement some hacks for wxWidgets and touch the underlying GTK
# layer and sub-libraries. This forces us to use the include locations and
# link these libraries.
@@ -929,11 +950,6 @@ if (UNIX AND NOT APPLE)
target_compile_definitions(libslic3r_gui PRIVATE wxHAVE_GDK_WAYLAND)
endif ()
# We add GStreamer for bambu:/// support.
pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0)
pkg_check_modules(GST_BASE REQUIRED gstreamer-base-1.0)
target_link_libraries(libslic3r_gui ${GSTREAMER_LIBRARIES} ${GST_BASE_LIBRARIES})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GSTREAMER_INCLUDE_DIRS} ${GST_BASE_INCLUDE_DIRS})
endif ()
# Add a definition so that we can tell we are compiling slic3r.
+12 -6
View File
@@ -682,13 +682,19 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
if (shader) {
if (idx == 0) {
int extruder_id = model_volume->extruder_id();
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]);
if (ban_light) {
new_color[3] = (255 - (extruder_id - 1))/255.0f;
// ORCA: extruder_id may be 0 (unset) or point past the colour list after a
// filament is deleted/remapped, so clamp the index instead of reading out of
// bounds.
if (!extruder_colors.empty()) {
int color_idx = std::clamp(extruder_id - 1, 0, int(extruder_colors.size()) - 1);
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[color_idx]);
if (ban_light) {
new_color[3] = (255 - color_idx)/255.0f;
}
m.set_color(new_color);
// shader->set_uniform("uniform_color", new_color);
}
m.set_color(new_color);
// shader->set_uniform("uniform_color", new_color);
}
else {
if (idx <= extruder_colors.size()) {
+170
View File
@@ -0,0 +1,170 @@
#include "AVVideoDecoder.hpp"
#include <assert.h>
extern "C"
{
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
}
AVVideoDecoder::AVVideoDecoder()
{
codec_ctx_ = avcodec_alloc_context3(nullptr);
}
AVVideoDecoder::~AVVideoDecoder()
{
if (sws_ctx_)
sws_freeContext(sws_ctx_);
if (frame_)
av_frame_free(&frame_);
if (codec_ctx_)
avcodec_free_context(&codec_ctx_);
}
int AVVideoDecoder::open(Bambu_StreamInfo const &info)
{
auto codec_id = info.sub_type == AVC1 ? AV_CODEC_ID_H264 : AV_CODEC_ID_MJPEG;
auto codec = avcodec_find_decoder(codec_id);
if (codec == nullptr) {
fprintf(stderr, "AVVideoDecoder: unsupported codec!\n");
return -1; // Codec not found
}
/* open the coderc */
if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) {
fprintf(stderr, "AVVideoDecoder: could not open codec\n");
return -1;
}
// Allocate an AVFrame structure
frame_ = av_frame_alloc();
if (frame_ == nullptr)
return -1;
return 0;
}
int AVVideoDecoder::decode(const Bambu_Sample &sample)
{
int ret = -1;
AVPacket *pkt = av_packet_alloc();
if (!pkt) {
return ret;
}
ret = av_new_packet(pkt, sample.size);
if (ret != 0) {
av_packet_free(&pkt);
return ret;
}
memcpy(pkt->data, sample.buffer, size_t(sample.size));
ret = avcodec_send_packet(codec_ctx_, pkt);
if (ret == 0) {
got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0;
}
av_packet_unref(pkt);
av_packet_free(&pkt);
return ret;
}
int AVVideoDecoder::flush()
{
int ret = avcodec_send_packet(codec_ctx_, nullptr);
got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0;
return ret;
}
void AVVideoDecoder::close()
{
}
bool AVVideoDecoder::toWxImage(wxImage &image, wxSize const &size2)
{
if (!got_frame_)
return false;
auto size1 = size2;
if (!size1.IsFullySpecified())
size1 = {frame_->width, frame_->height };
auto size = size1;
if (size.GetWidth() & 0x0f) {
size.SetWidth((size.GetWidth() & ~0x0f) + 0x10);
if (size.GetWidth() != width_) {
std::fill(bits_.begin(), bits_.end(), 0);
width_ = size.GetWidth();
}
}
AVPixelFormat wxFmt = AV_PIX_FMT_RGB24;
sws_ctx_ = sws_getCachedContext(sws_ctx_,
frame_->width, frame_->height, AVPixelFormat(frame_->format),
size1.GetWidth(), size1.GetHeight(), wxFmt,
SWS_GAUSS,
nullptr, nullptr, nullptr);
if (sws_ctx_ == nullptr)
return false;
int length = size.GetWidth() * size.GetHeight() * 3;
if (bits_.size() < length)
bits_.resize(length);
uint8_t * datas[] = { bits_.data() };
int strides[] = { size.GetWidth() * 3 };
int result_h = sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, frame_->height, datas, strides);
if (result_h != size.GetHeight()) {
return false;
}
// Copy: the frame outlives this decoder and is painted by the GUI thread while the
// next sws_scale is already overwriting bits_, so it must own its pixels. The Windows
// path below needs no equivalent, wxBitmap copies the bits into GDI.
image = wxImage(size.GetWidth(), size.GetHeight(), bits_.data(), true).Copy();
if (!image.IsOk()) {
fprintf(stderr, "AVVideoDecoder: image not ok %dx%d\n", size.GetWidth(), size.GetHeight());
return false;
}
return true;
}
bool AVVideoDecoder::toWxBitmap(wxBitmap &bitmap, wxSize const &size2)
{
if (!got_frame_)
return false;
auto size1 = size2;
if (!size1.IsFullySpecified())
size1 = {frame_->width, frame_->height };
auto size = size1;
if (size.GetWidth() & 0x0f) {
size.SetWidth((size.GetWidth() & ~0x0f) + 0x10);
if (size.GetWidth() != width_) {
std::fill(bits_.begin(), bits_.end(), 0);
width_ = size.GetWidth();
}
}
AVPixelFormat wxFmt = AV_PIX_FMT_RGB32;
sws_ctx_ = sws_getCachedContext(sws_ctx_,
frame_->width, frame_->height, AVPixelFormat(frame_->format),
size1.GetWidth(), size1.GetHeight(), wxFmt,
SWS_GAUSS,
nullptr, nullptr, nullptr);
if (sws_ctx_ == nullptr)
return false;
int length = size.GetWidth() * size.GetHeight() * 4;
if (bits_.size() < length)
bits_.resize(length);
uint8_t *datas[] = { bits_.data() };
int strides[] = { size.GetWidth() * 4 };
int result_h = sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, frame_->height, datas, strides);
if (result_h != size.GetHeight()) {
fprintf(stderr, "AVVideoDecoder: result_h %d %d\n", result_h, size.GetHeight());
return false;
}
bitmap = wxBitmap((char const *) bits_.data(), size.GetWidth(), size.GetHeight(), 32);
assert(bitmap.IsOk());
if (!bitmap.IsOk()) {
fprintf(stderr, "AVVideoDecoder: bitmap not ok %dx%d\n", size.GetWidth(), size.GetHeight());
return false;
}
return true;
}
+46
View File
@@ -0,0 +1,46 @@
#ifndef AVVIDEODECODER_HPP
#define AVVIDEODECODER_HPP
#include "Printer/BambuTunnel.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
#include <vector>
#include <wx/bitmap.h>
#include <wx/gdicmn.h>
#include <wx/image.h>
class wxBitmap;
class AVVideoDecoder
{
public:
AVVideoDecoder();
~AVVideoDecoder();
public:
int open(Bambu_StreamInfo const &info);
int decode(Bambu_Sample const &sample);
int flush();
void close();
bool toWxImage(wxImage &image, wxSize const &size);
bool toWxBitmap(wxBitmap &bitmap, wxSize const & size);
private:
AVCodecContext *codec_ctx_ = nullptr;
AVFrame * frame_ = nullptr;
SwsContext * sws_ctx_ = nullptr;
bool got_frame_ = false;
int width_ { 0 }; // scale result width
std::vector<uint8_t> bits_;
};
#endif // AVVIDEODECODER_HPP
-30
View File
@@ -1,30 +0,0 @@
//
// BambuPlayer.h
// BambuPlayer
//
// Created by cmguo on 2021/12/6.
//
#import <Foundation/Foundation.h>
#import <AVFoundation/AVSampleBufferDisplayLayer.h>
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface BambuPlayer : NSObject
+ (void) initialize;
- (instancetype) initWithDisplayLayer: (AVSampleBufferDisplayLayer*) layer;
- (instancetype) initWithImageView: (NSView*) view;
- (int) open: (char const *) url;
- (NSSize) videoSize;
- (int) play;
- (void) stop;
- (void) close;
- (void) setLogger: (void (*)(void const * context, int level, char const * msg)) logger withContext: (void const *) context;
@end
NS_ASSUME_NONNULL_END
@@ -1015,7 +1015,7 @@ wxBoxSizer* CalibrationPresetPage::create_ams_items_sizer(MachineObject* obj, wx
auto ams_items_sizer = new wxBoxSizer(wxHORIZONTAL);
for (auto &info : ams_info) {
auto preview_ams_item = new AMSPreview(ams_preview_panel, wxID_ANY, info, info.ams_type);
preview_ams_item->Update(info);
preview_ams_item->UpdateInfo(info);
preview_ams_item->Open();
ams_preview_list.push_back(preview_ams_item);
std::string ams_id = preview_ams_item->get_ams_id();
+1 -1
View File
@@ -193,7 +193,7 @@ public:
void show_panels(CalibrationMethod method, const PrinterSeries printer_ser);
void on_device_connected(MachineObject* obj);
void on_device_connected(MachineObject* obj) override;
void update(MachineObject* obj) override;
@@ -48,8 +48,8 @@ public:
void create_page(wxWindow* parent);
void on_reset_page();
void on_device_connected(MachineObject* obj);
void on_reset_page() override;
void on_device_connected(MachineObject* obj) override;
void msw_rescale() override;
};
@@ -63,8 +63,8 @@ public:
long style = wxTAB_TRAVERSAL);
void create_page(wxWindow* parent);
void on_reset_page();
void on_device_connected(MachineObject* obj);
void on_reset_page() override;
void on_device_connected(MachineObject* obj) override;
void msw_rescale() override;
};
+951
View File
@@ -0,0 +1,951 @@
#include "ColorDecomposeDialog.hpp"
#include <algorithm>
#include <cmath>
#include <functional>
#include <memory>
#include <set>
#include <wx/sizer.h>
#include <wx/dcclient.h>
#include <wx/dcbuffer.h>
#include "wx/graphics.h"
#include "I18N.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "format.hpp"
#include "Widgets/ComboBox.hpp"
#include "Widgets/DropDown.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/Label.hpp"
#include "wxExtensions.hpp"
#include "ColorDecomposeSupport.hpp"
#include "libslic3r/ColorDecomposeRecipe.hpp"
namespace Slic3r {
namespace GUI {
static const wxColour COLOR_BRAND("#009688");
static const wxColour COLOR_BORDER_NORMAL("#EEEEEE");
static const wxColour COLOR_BG_CARD("#F8F8F8");
static const wxColour COLOR_LABEL_GREY("#ACACAC");
static const wxColour COLOR_TEXT_DARK("#262E30");
static const wxColour COLOR_DIVIDER("#EEEEEE");
// Standard CMYW base colors
static const wxColour CMYW_CYAN(0, 255, 255);
static const wxColour CMYW_MAGENTA(255, 0, 255);
static const wxColour CMYW_YELLOW(255, 255, 0);
static const wxColour CMYW_WHITE(255, 255, 255);
// Standard RYBW base colors
static const wxColour RYBW_RED(255, 0, 0);
static const wxColour RYBW_YELLOW(255, 255, 0);
static const wxColour RYBW_BLUE(0, 0, 255);
static const wxColour RYBW_WHITE(255, 255, 255);
static size_t mode_index(DecomposeMode mode)
{
return static_cast<size_t>(mode);
}
static ColorDecomposeRgb wx_colour_to_recipe_rgb(const wxColour& color)
{
return {
static_cast<unsigned char>(color.Red()),
static_cast<unsigned char>(color.Green()),
static_cast<unsigned char>(color.Blue())
};
}
static wxColour hex_to_wx_colour(const std::string& hex, const wxColour& fallback)
{
wxColour color(hex);
return color.IsOk() ? color : fallback;
}
static bool same_rgb(const wxColour& lhs, const wxColour& rhs)
{
return lhs.Red() == rhs.Red() && lhs.Green() == rhs.Green() && lhs.Blue() == rhs.Blue();
}
static DecomposeBaseColor standard_base_color_from_key(const std::string& key)
{
if (key == "Cyan") return DecomposeBaseColor::Cyan;
if (key == "Magenta") return DecomposeBaseColor::Magenta;
if (key == "Yellow") return DecomposeBaseColor::Yellow;
if (key == "White") return DecomposeBaseColor::White;
if (key == "Red") return DecomposeBaseColor::Red;
if (key == "Green") return DecomposeBaseColor::Green;
if (key == "Blue") return DecomposeBaseColor::Blue;
return DecomposeBaseColor::None;
}
static wxColour pure_color_for_base(DecomposeBaseColor base)
{
switch (base) {
case DecomposeBaseColor::Cyan: return CMYW_CYAN;
case DecomposeBaseColor::Magenta: return CMYW_MAGENTA;
case DecomposeBaseColor::Yellow: return CMYW_YELLOW;
case DecomposeBaseColor::White: return CMYW_WHITE;
case DecomposeBaseColor::Red: return RYBW_RED;
case DecomposeBaseColor::Blue: return RYBW_BLUE;
default: return *wxBLACK;
}
}
static DecomposeBaseColor standard_base_color_for(DecomposeMode mode, const wxColour& color)
{
if (mode == DecomposeMode::CMYW) {
if (same_rgb(color, CMYW_CYAN)) return DecomposeBaseColor::Cyan;
if (same_rgb(color, CMYW_MAGENTA)) return DecomposeBaseColor::Magenta;
if (same_rgb(color, CMYW_YELLOW)) return DecomposeBaseColor::Yellow;
if (same_rgb(color, CMYW_WHITE)) return DecomposeBaseColor::White;
} else if (mode == DecomposeMode::RYBW) {
if (same_rgb(color, RYBW_RED)) return DecomposeBaseColor::Red;
if (same_rgb(color, RYBW_YELLOW)) return DecomposeBaseColor::Yellow;
if (same_rgb(color, RYBW_BLUE)) return DecomposeBaseColor::Blue;
if (same_rgb(color, RYBW_WHITE)) return DecomposeBaseColor::White;
}
return DecomposeBaseColor::None;
}
static ColorDecomposeResult to_dialog_result(const ColorDecomposeRecipeResult& recipe,
const wxColour& fallback)
{
ColorDecomposeResult result;
result.mode = recipe.mode;
result.matched_color = hex_to_wx_colour(recipe.matched_color_hex, fallback);
for (const auto& comp_recipe : recipe.components) {
DecomposeComponent comp;
comp.colour = hex_to_wx_colour(comp_recipe.color_hex, fallback);
comp.ratio = comp_recipe.ratio;
comp.filament_index = static_cast<int>(comp_recipe.filament_index);
comp.base_color = standard_base_color_from_key(comp_recipe.base_color);
if (comp.base_color == DecomposeBaseColor::None)
comp.base_color = standard_base_color_for(recipe.mode, comp.colour);
result.components.push_back(comp);
}
return result;
}
static wxPanel* create_h_divider(wxWindow* parent, int fixed_width = -1)
{
const int h = parent->FromDIP(1);
int w = fixed_width > 0 ? fixed_width : -1;
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(w, h));
panel->SetMinSize(wxSize(w, h));
if (fixed_width > 0)
panel->SetMaxSize(wxSize(fixed_width, h));
panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_DIVIDER));
return panel;
}
static wxStaticText* create_mode_group_label(wxWindow* parent, const wxString& text)
{
auto* label = new wxStaticText(parent, wxID_ANY, text);
label->SetFont(Label::Body_11);
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_GREY));
return label;
}
static void match_parent_bg(wxWindow* w, const wxColour& bg)
{
w->SetBackgroundColour(bg);
}
static bool material_type_matches(const std::string& a, const std::string& b)
{
if (a.empty() || b.empty())
return false;
return a == b || a == b + " Basic" || b == a + " Basic";
}
ColorDecomposeDialog::ColorDecomposeDialog(wxWindow* parent,
int filament_idx,
const wxColour& target_color,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& filament_names,
const std::vector<std::string>& filament_types,
size_t current_filament_count,
size_t max_filament_count,
std::vector<size_t> physical_config_indices)
: DPIDialog(parent, wxID_ANY, _L("Decompose Color"), wxDefaultPosition,
wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_filament_idx(filament_idx)
, m_target_color(target_color)
, m_physical_colors(physical_colors)
, m_filament_names(filament_names)
, m_filament_types(filament_types)
, m_current_filament_count(current_filament_count)
, m_max_filament_count(max_filament_count)
, m_physical_config_indices(std::move(physical_config_indices))
{
for (const auto& t : m_filament_types) {
if (std::find(m_project_types.begin(), m_project_types.end(), t) == m_project_types.end())
m_project_types.push_back(t);
}
if (m_filament_idx >= 0 && static_cast<size_t>(m_filament_idx) < m_filament_types.size())
m_preferred_type = m_filament_types[m_filament_idx];
else if (!m_project_types.empty())
m_preferred_type = m_project_types.front();
build_ui();
wxGetApp().UpdateDlgDarkUI(this);
// Restore target swatch after dark mode color remapping
if (m_target_swatch) {
m_target_swatch->SetBackgroundColour(m_target_color);
m_target_swatch->Refresh();
}
update_card_visibility();
Fit();
compute_decomposition();
update_matched_color_display();
update_ok_button_state();
}
void ColorDecomposeDialog::on_dpi_changed(const wxRect& suggested_rect)
{
(void)suggested_rect;
Fit();
Refresh();
}
void ColorDecomposeDialog::build_ui()
{
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
auto* main_sizer = new wxBoxSizer(wxVERTICAL);
const int selector_side_margin = FromDIP(26);
const int selector_top_gap = FromDIP(22);
const int content_side_margin = FromDIP(30);
const int target_section_top_gap = FromDIP(18);
main_sizer->AddSpacer(selector_top_gap);
main_sizer->Add(create_filament_selector(), 0, wxEXPAND | wxLEFT | wxRIGHT, selector_side_margin);
main_sizer->AddSpacer(target_section_top_gap);
main_sizer->Add(create_target_color_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
main_sizer->AddSpacer(FromDIP(16));
main_sizer->Add(create_h_divider(this), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
main_sizer->AddSpacer(FromDIP(16));
main_sizer->Add(create_mode_selection_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
main_sizer->AddSpacer(FromDIP(16));
main_sizer->Add(create_button_panel(), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, content_side_margin);
SetSizer(main_sizer);
SetMinSize(wxSize(FromDIP(477), FromDIP(380)));
Fit();
CenterOnParent();
}
wxBoxSizer* ColorDecomposeDialog::create_filament_selector()
{
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
m_type_combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
wxSize(-1, FromDIP(36)), 0, nullptr, wxCB_READONLY);
m_type_combo->SetFont(Label::Body_13);
m_combo_item_types.clear();
int default_sel = -1;
// --- Group 1: Project filament list (deduplicated by type) ---
m_type_combo->Append(_L("Project Filament List"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
m_combo_item_types.push_back(std::string());
std::set<std::string> seen_types;
for (size_t i = 0; i < m_filament_names.size(); ++i) {
const std::string& type = (i < m_filament_types.size()) ? m_filament_types[i] : "PLA";
if (!seen_types.insert(type).second)
continue;
int idx = m_type_combo->Append(wxString::FromUTF8(m_filament_names[i]));
m_combo_item_types.push_back(type);
if (type == m_preferred_type && default_sel < 0)
default_sel = idx;
}
// --- Group 2: Standard mode material recommendations ---
static const char* kStandardTypes[] = {
kDecomposePlaBasicType
};
m_type_combo->Append(_L("Standard Mode Recommendations"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
m_combo_item_types.push_back(std::string());
for (size_t s = 0; s < sizeof(kStandardTypes) / sizeof(kStandardTypes[0]); ++s) {
// Always show standard recommendations, even if the same type already
// appears in the project filament list above.
const std::string label = std::string(kDecomposeBambuPresetPrefix) + kStandardTypes[s];
int idx = m_type_combo->Append(wxString::FromUTF8(label));
m_combo_item_types.push_back(kStandardTypes[s]);
if (kStandardTypes[s] == m_preferred_type && default_sel < 0)
default_sel = idx;
}
if (default_sel < 0) {
for (int i = 0; i < static_cast<int>(m_combo_item_types.size()); ++i) {
if (!m_combo_item_types[i].empty()) {
default_sel = i;
break;
}
}
}
if (default_sel >= 0) {
m_type_combo->SetSelection(default_sel);
if (!m_combo_item_types[default_sel].empty())
m_preferred_type = m_combo_item_types[default_sel];
}
m_type_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) {
evt.StopPropagation();
int sel = m_type_combo->GetSelection();
if (sel >= 0 && static_cast<size_t>(sel) < m_combo_item_types.size()
&& !m_combo_item_types[sel].empty()) {
m_preferred_type = m_combo_item_types[sel];
}
update_card_visibility();
compute_decomposition();
update_matched_color_display();
update_ok_button_state();
});
sizer->Add(m_type_combo, 1, wxEXPAND);
return sizer;
}
static wxPanel* create_color_swatch(wxWindow* parent, const wxColour& color, int size)
{
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(size, size));
panel->SetBackgroundColour(color);
panel->SetMinSize(wxSize(size, size));
panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
panel->Bind(wxEVT_PAINT, [panel](wxPaintEvent&) {
wxAutoBufferedPaintDC dc(panel);
wxSize sz = panel->GetClientSize();
wxColour c = panel->GetBackgroundColour();
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(c));
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
// Mirror sidebar (FilamentBitmapUtils::create_single_filament_bitmap):
// gray border for near-white in light mode so white swatches stay
// visible on a white background; light border for near-black in dark mode.
const bool light_mode = !wxGetApp().dark_mode();
if ((light_mode && c.Red() > 224 && c.Green() > 224 && c.Blue() > 224) ||
(!light_mode && c.Red() < 45 && c.Green() < 45 && c.Blue() < 45)) {
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.SetPen(wxPen(light_mode ? wxColour(130, 130, 128) : wxColour(207, 207, 207),
1, wxPENSTYLE_SOLID));
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
}
});
return panel;
}
wxBoxSizer* ColorDecomposeDialog::create_target_color_section()
{
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
auto* label = new wxStaticText(this, wxID_ANY, _L("Target Color"));
label->SetFont(Label::Head_14);
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(19));
m_target_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
sizer->Add(m_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
m_target_rgb_text = new wxStaticText(this, wxID_ANY,
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
m_target_rgb_text->SetFont(Label::Body_13);
m_target_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(m_target_rgb_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
auto* arrow_text = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92"));
arrow_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(arrow_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
m_matched_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
sizer->Add(m_matched_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
m_matched_rgb_text = new wxStaticText(this, wxID_ANY,
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
m_matched_rgb_text->SetFont(Label::Head_13);
m_matched_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(m_matched_rgb_text, 0, wxALIGN_CENTER_VERTICAL);
return sizer;
}
wxPanel* ColorDecomposeDialog::create_mode_card(wxWindow* parent, DecomposeMode mode,
const wxString& title)
{
const int pad = FromDIP(12);
auto* card = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
card->SetBackgroundStyle(wxBG_STYLE_PAINT);
auto* card_sizer = new wxBoxSizer(wxVERTICAL);
auto* title_sizer = new wxBoxSizer(wxHORIZONTAL);
auto* title_label = new wxStaticText(card, wxID_ANY, title);
title_label->SetFont(Label::Body_14);
title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A")));
match_parent_bg(title_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
title_sizer->Add(title_label, 1, wxALIGN_CENTER_VERTICAL);
auto* chk = new ::CheckBox(card);
chk->SetValue(mode == m_selected_mode);
match_parent_bg(chk, StateColor::darkModeColorFor(COLOR_BG_CARD));
switch (mode) {
case DecomposeMode::MaterialList: m_chk_material_list = chk; break;
case DecomposeMode::CMYW: m_chk_cmyw = chk; break;
case DecomposeMode::RYBW: m_chk_rybw = chk; break;
}
chk->Bind(wxEVT_TOGGLEBUTTON, [this, mode](wxCommandEvent& e) {
select_mode(mode);
e.Skip(); // let CheckBox::update() re-sync its bitmap to GetValue()
});
title_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL);
card_sizer->Add(title_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, pad);
card_sizer->Add(create_h_divider(card), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(8));
auto* colors_sizer = new wxBoxSizer(wxHORIZONTAL);
card_sizer->Add(colors_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad);
auto& controls = m_mode_cards[mode_index(mode)];
controls.card = card;
controls.components_sizer = colors_sizer;
card->SetSizer(card_sizer);
card->SetMinSize(wxSize(FromDIP(128), FromDIP(111)));
card->SetMaxSize(wxSize(FromDIP(128), FromDIP(111)));
card->Bind(wxEVT_PAINT, [this, card, mode](wxPaintEvent&) {
wxBufferedPaintDC dc(card);
wxSize sz = card->GetClientSize();
dc.SetBackground(wxBrush(StateColor::darkModeColorFor(*wxWHITE)));
dc.Clear();
bool selected = (m_selected_mode == mode);
wxColour border_col = selected
? StateColor::darkModeColorFor(COLOR_BRAND)
: StateColor::darkModeColorFor(COLOR_BORDER_NORMAL);
const int border_width = FromDIP(selected ? 2 : 1);
const double inset = border_width / 2.0;
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (gc) {
gc->SetPen(wxPen(border_col, border_width));
gc->SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
gc->DrawRoundedRectangle(inset, inset, sz.x - 2 * inset, sz.y - 2 * inset, FromDIP(8));
} else {
const int fallback_inset = (border_width + 1) / 2;
dc.SetPen(wxPen(border_col, border_width));
dc.SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
dc.DrawRoundedRectangle(fallback_inset, fallback_inset, sz.x - 2 * fallback_inset, sz.y - 2 * fallback_inset, FromDIP(8));
}
});
std::function<void(wxWindow*)> bind_click;
bind_click = [this, mode, chk, &bind_click](wxWindow* w) {
if (w == chk || dynamic_cast<::CheckBox*>(w))
return;
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
select_mode(mode);
});
w->SetCursor(wxCursor(wxCURSOR_HAND));
for (auto* child : w->GetChildren())
bind_click(child);
};
bind_click(card);
return card;
}
wxBoxSizer* ColorDecomposeDialog::create_mode_selection_section()
{
auto* sizer = new wxBoxSizer(wxVERTICAL);
auto* section_label = new wxStaticText(this, wxID_ANY, _L("Select Color Decomposition"));
section_label->SetFont(Label::Head_14);
section_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
sizer->Add(section_label, 0, wxBOTTOM, FromDIP(4));
auto* modes_sizer = new wxBoxSizer(wxHORIZONTAL);
// --- Arbitrary mode column (wrapped in a panel so the whole column hides together) ---
m_arb_column_panel = new wxPanel(this, wxID_ANY);
m_arb_column_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
auto* arb_col = new wxBoxSizer(wxVERTICAL);
{
auto* arb_header_sizer = new wxBoxSizer(wxHORIZONTAL);
arb_header_sizer->Add(create_mode_group_label(m_arb_column_panel, _L("Arbitrary Mode")),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
arb_header_sizer->Add(create_h_divider(m_arb_column_panel, FromDIP(88)), 0, wxALIGN_CENTER_VERTICAL);
arb_col->Add(arb_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
m_card_material_list = create_mode_card(m_arb_column_panel, DecomposeMode::MaterialList,
_L("Material List"));
arb_col->Add(m_card_material_list, 0, wxEXPAND);
}
m_arb_column_panel->SetSizer(arb_col);
modes_sizer->Add(m_arb_column_panel, 0, wxEXPAND | wxRIGHT, FromDIP(16));
// --- Standard mode column ---
auto* std_col = new wxBoxSizer(wxVERTICAL);
{
auto* std_header_sizer = new wxBoxSizer(wxHORIZONTAL);
std_header_sizer->Add(create_mode_group_label(this, _L("Standard Mode")),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
std_header_sizer->Add(create_h_divider(this), 1, wxALIGN_CENTER_VERTICAL);
std_col->Add(std_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
auto* cards_sizer = new wxBoxSizer(wxHORIZONTAL);
m_card_cmyw = create_mode_card(this, DecomposeMode::CMYW, "CMYW");
cards_sizer->Add(m_card_cmyw, 0, wxRIGHT, FromDIP(12));
m_card_rybw = create_mode_card(this, DecomposeMode::RYBW, "RYBW");
cards_sizer->Add(m_card_rybw, 0);
std_col->Add(cards_sizer, 0, wxEXPAND);
}
modes_sizer->Add(std_col, 0, wxEXPAND);
sizer->Add(modes_sizer, 0, wxEXPAND);
m_no_card_hint = new wxStaticText(this, wxID_ANY,
_L("At least two filaments of the same material type are required for decomposition"));
m_no_card_hint->SetFont(Label::Body_13);
m_no_card_hint->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A")));
m_no_card_hint->Wrap(FromDIP(400));
m_no_card_hint->Hide();
sizer->Add(m_no_card_hint, 0, wxTOP, FromDIP(8));
m_limit_warning_panel = new wxPanel(this, wxID_ANY);
m_limit_warning_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
auto* warning_sizer = new wxBoxSizer(wxHORIZONTAL);
auto* warn_bmp = new wxStaticBitmap(m_limit_warning_panel, wxID_ANY,
create_scaled_bitmap("obj_warning", m_limit_warning_panel, 16),
wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
m_limit_warning_text = new wxStaticText(m_limit_warning_panel, wxID_ANY, wxEmptyString);
m_limit_warning_text->SetFont(Label::Body_13);
m_limit_warning_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B")));
m_limit_warning_text->Wrap(FromDIP(400));
warning_sizer->Add(warn_bmp, 0, wxALIGN_TOP | wxRIGHT, FromDIP(6));
warning_sizer->Add(m_limit_warning_text, 1, wxEXPAND);
m_limit_warning_panel->SetSizer(warning_sizer);
m_limit_warning_panel->Hide();
sizer->Add(m_limit_warning_panel, 0, wxEXPAND | wxTOP, FromDIP(8));
return sizer;
}
wxBoxSizer* ColorDecomposeDialog::create_button_panel()
{
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->AddStretchSpacer();
m_btn_cancel = new Button(this, _L("Cancel"));
m_btn_cancel->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
m_btn_ok = new Button(this, _L("OK"));
m_btn_ok->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
m_btn_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
EndModal(wxID_OK);
});
sizer->Add(m_btn_cancel, 0, wxRIGHT, FromDIP(12));
sizer->Add(m_btn_ok, 0);
return sizer;
}
void ColorDecomposeDialog::select_mode(DecomposeMode mode)
{
m_selected_mode = mode;
m_result = m_mode_results[mode_index(mode)];
update_card_styles();
update_matched_color_display();
update_ok_button_state();
}
void ColorDecomposeDialog::update_card_styles()
{
if (m_card_material_list) m_card_material_list->Refresh();
if (m_card_cmyw) m_card_cmyw->Refresh();
if (m_card_rybw) m_card_rybw->Refresh();
if (m_chk_material_list)
m_chk_material_list->SetValue(m_selected_mode == DecomposeMode::MaterialList);
if (m_chk_cmyw)
m_chk_cmyw->SetValue(m_selected_mode == DecomposeMode::CMYW);
if (m_chk_rybw)
m_chk_rybw->SetValue(m_selected_mode == DecomposeMode::RYBW);
}
void ColorDecomposeDialog::update_card_visibility()
{
// Count physical filaments of the same type (excluding the source filament)
int same_type_count = 0;
for (size_t i = 0; i < m_filament_types.size(); ++i) {
if (static_cast<int>(i) == m_filament_idx)
continue;
if (material_type_matches(m_filament_types[i], m_preferred_type))
++same_type_count;
}
bool show_arb = (same_type_count >= 2);
bool show_cmyw = (m_preferred_type == kDecomposePlaBasicType);
bool show_rybw = (m_preferred_type == kDecomposePlaBasicType);
if (m_arb_column_panel) m_arb_column_panel->Show(show_arb);
if (m_card_material_list) m_card_material_list->Show(show_arb);
if (m_card_cmyw) m_card_cmyw->Show(show_cmyw);
if (m_card_rybw) m_card_rybw->Show(show_rybw);
bool any_visible = show_arb || show_cmyw || show_rybw;
if (m_no_card_hint)
m_no_card_hint->Show(!any_visible);
// Auto-select a visible mode when current selection becomes hidden
if (any_visible) {
bool cur_visible = false;
if (m_selected_mode == DecomposeMode::MaterialList && show_arb) cur_visible = true;
if (m_selected_mode == DecomposeMode::CMYW && show_cmyw) cur_visible = true;
if (m_selected_mode == DecomposeMode::RYBW && show_rybw) cur_visible = true;
if (!cur_visible) {
if (show_arb) select_mode(DecomposeMode::MaterialList);
else if (show_cmyw) select_mode(DecomposeMode::CMYW);
else select_mode(DecomposeMode::RYBW);
}
}
Layout();
update_ok_button_state();
}
void ColorDecomposeDialog::update_filament_limit_warning()
{
if (!m_limit_warning_panel || !m_limit_warning_text)
return;
size_t missing_new = 0;
if (m_missing_calculator) {
missing_new = m_missing_calculator(m_result);
} else {
const size_t source_physical_idx = m_filament_idx >= 0 ? static_cast<size_t>(m_filament_idx) : size_t(-1);
const std::vector<size_t>* indices =
m_physical_config_indices.empty() ? nullptr : &m_physical_config_indices;
missing_new = count_decompose_new_physical_filaments(
m_result, m_physical_colors, m_filament_types, source_physical_idx, indices);
}
// A result with fewer than 2 components (e.g. target color is already a
// standard base color shown as "100%") creates no mixed filament and no new
// physical filament, so it can never exceed the limit.
const bool creates_mixed = m_result.components.size() >= 2;
// +1 for the mixed filament slot that will be created after decomposition.
const size_t needed = m_current_filament_count + missing_new + 1;
const bool blocked = creates_mixed && needed > m_max_filament_count;
const bool was_shown = m_limit_warning_panel->IsShown();
if (!blocked) {
if (was_shown) {
m_limit_warning_panel->Hide();
Layout();
Fit();
}
return;
}
wxString mode_name;
switch (m_selected_mode) {
case DecomposeMode::CMYW: mode_name = "CMYW"; break;
case DecomposeMode::RYBW: mode_name = "RYBW"; break;
case DecomposeMode::MaterialList: mode_name = _L("Material List"); break;
}
const wxString warning_text = format_wxstr(
_L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."),
m_max_filament_count, mode_name);
// Show first so the panel is laid out and the text control gets its real
// width, then wrap to that width so the paragraph fills the content area.
m_limit_warning_panel->Show();
Layout();
const int avail = m_limit_warning_text->GetClientSize().x;
m_limit_warning_text->SetLabel(warning_text);
if (avail > FromDIP(50))
m_limit_warning_text->Wrap(avail);
Layout();
// Only resize when the warning panel actually toggled from hidden to shown.
// While already visible, switching modes must not re-Fit the dialog, which
// would make it jump on every card switch. Fit keeps the user-moved position.
if (!was_shown) {
Fit();
}
}
void ColorDecomposeDialog::set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn)
{
m_missing_calculator = std::move(fn);
update_ok_button_state();
}
void ColorDecomposeDialog::update_ok_button_state()
{
if (!m_btn_ok) return;
update_filament_limit_warning();
bool any_card_visible = (m_card_material_list && m_card_material_list->IsShown())
|| (m_card_cmyw && m_card_cmyw->IsShown())
|| (m_card_rybw && m_card_rybw->IsShown());
const bool blocked = m_limit_warning_panel && m_limit_warning_panel->IsShown();
m_btn_ok->Enable(any_card_visible && !blocked);
Layout();
}
void ColorDecomposeDialog::update_mode_card_content(DecomposeMode mode)
{
auto& controls = m_mode_cards[mode_index(mode)];
auto* sizer = controls.components_sizer;
auto* card = controls.card;
if (!sizer || !card)
return;
sizer->Clear(true);
const auto& components = m_mode_results[mode_index(mode)].components;
const size_t count = components.size();
if (count == 0) {
card->Layout();
card->Refresh();
return;
}
const int swatch_sz = FromDIP(24);
const int plus_gap = FromDIP(24);
const wxFont& ratio_font = Label::Body_13;
auto bind_select = [this, mode](wxWindow* w) {
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
select_mode(mode);
});
w->SetCursor(wxCursor(wxCURSOR_HAND));
};
for (size_t i = 0; i < count; ++i) {
auto* col = new wxBoxSizer(wxVERTICAL);
auto* swatch = create_color_swatch(card, components[i].colour, swatch_sz);
bind_select(swatch);
col->Add(swatch, 0, wxALIGN_CENTER_HORIZONTAL);
auto* ratio_text = new wxStaticText(card, wxID_ANY, wxString::Format("%d%%", components[i].ratio));
ratio_text->SetFont(ratio_font);
ratio_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
match_parent_bg(ratio_text, StateColor::darkModeColorFor(COLOR_BG_CARD));
bind_select(ratio_text);
col->Add(ratio_text, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4));
sizer->Add(col, 0, wxALIGN_TOP);
if (i + 1 < count) {
sizer->AddStretchSpacer();
auto* plus_panel = new wxPanel(card, wxID_ANY, wxDefaultPosition, wxSize(plus_gap, swatch_sz));
plus_panel->SetMinSize(wxSize(plus_gap, swatch_sz));
plus_panel->SetMaxSize(wxSize(plus_gap, swatch_sz));
plus_panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_BG_CARD));
auto* plus_sizer = new wxBoxSizer(wxVERTICAL);
auto* plus_label = new wxStaticText(plus_panel, wxID_ANY, "+");
plus_label->SetFont(Label::Body_13);
plus_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
match_parent_bg(plus_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
bind_select(plus_panel);
bind_select(plus_label);
plus_sizer->AddStretchSpacer();
plus_sizer->Add(plus_label, 0, wxALIGN_CENTER_HORIZONTAL);
plus_sizer->AddStretchSpacer();
plus_panel->SetSizer(plus_sizer);
sizer->Add(plus_panel, 0, wxALIGN_TOP);
sizer->AddStretchSpacer();
}
}
const int card_width = FromDIP(128 + (count > 2 ? static_cast<int>(count - 2) * 31 : 0));
card->SetMinSize(wxSize(card_width, FromDIP(111)));
card->SetMaxSize(wxSize(card_width, FromDIP(111)));
card->Layout();
card->Refresh();
}
void ColorDecomposeDialog::update_mode_card_contents()
{
update_mode_card_content(DecomposeMode::MaterialList);
update_mode_card_content(DecomposeMode::CMYW);
update_mode_card_content(DecomposeMode::RYBW);
Layout();
Fit();
}
void ColorDecomposeDialog::update_matched_color_display()
{
if (!m_result.matched_color.IsOk())
m_result.matched_color = m_target_color;
if (m_matched_swatch) {
m_matched_swatch->SetBackgroundColour(m_result.matched_color);
m_matched_swatch->Refresh();
}
if (m_matched_rgb_text) {
m_matched_rgb_text->SetLabel(wxString::Format("RGB: %d, %d, %d",
m_result.matched_color.Red(), m_result.matched_color.Green(), m_result.matched_color.Blue()));
}
}
bool ColorDecomposeDialog::try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const
{
// Gate by preferred type, matching card visibility: CMYW and RYBW only for PLA Basic.
if (mode == DecomposeMode::CMYW || mode == DecomposeMode::RYBW) {
if (m_preferred_type != kDecomposePlaBasicType)
return false;
} else {
return false;
}
static const DecomposeBaseColor cmyw_bases[] = {
DecomposeBaseColor::Cyan, DecomposeBaseColor::Magenta,
DecomposeBaseColor::Yellow, DecomposeBaseColor::White
};
static const DecomposeBaseColor rybw_bases[] = {
DecomposeBaseColor::Red, DecomposeBaseColor::Yellow,
DecomposeBaseColor::Blue, DecomposeBaseColor::White
};
const DecomposeBaseColor* bases = (mode == DecomposeMode::CMYW) ? cmyw_bases : rybw_bases;
const size_t base_count = (mode == DecomposeMode::CMYW)
? sizeof(cmyw_bases) / sizeof(cmyw_bases[0])
: sizeof(rybw_bases) / sizeof(rybw_bases[0]);
const std::string target_hex = decompose_normalize_color_hex(
m_target_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
for (size_t i = 0; i < base_count; ++i) {
const DecomposeBaseColor base = bases[i];
DecomposeOfficialComponent official =
lookup_decompose_official_component(m_preferred_type, base, pure_color_for_base(base));
if (decompose_normalize_color_hex(official.color_hex) != target_hex)
continue;
out = ColorDecomposeResult{};
out.mode = mode;
out.matched_color = hex_to_wx_colour(official.color_hex, m_target_color);
DecomposeComponent comp;
comp.colour = out.matched_color;
comp.ratio = 100;
comp.filament_index = -1;
comp.base_color = base;
out.components.push_back(comp);
return true;
}
return false;
}
void ColorDecomposeDialog::compute_decomposition()
{
auto fallback_result = [this](DecomposeMode mode, const std::vector<DecomposeComponent>& components) {
ColorDecomposeResult result;
result.mode = mode;
result.components = components;
int total = 0;
double r = 0.0, g = 0.0, b = 0.0;
for (const auto& comp : result.components)
total += comp.ratio;
if (total <= 0)
total = 100;
for (const auto& comp : result.components) {
const double w = static_cast<double>(comp.ratio) / total;
r += comp.colour.Red() * w;
g += comp.colour.Green() * w;
b += comp.colour.Blue() * w;
}
result.matched_color = result.components.empty()
? m_target_color
: wxColour(static_cast<unsigned char>(std::clamp(r, 0.0, 255.0)),
static_cast<unsigned char>(std::clamp(g, 0.0, 255.0)),
static_cast<unsigned char>(std::clamp(b, 0.0, 255.0)));
return result;
};
std::vector<ColorDecomposePhysicalFilament> physical_filaments;
physical_filaments.reserve(m_physical_colors.size());
for (size_t i = 0; i < m_physical_colors.size(); ++i) {
if (m_filament_idx >= 0 && i == static_cast<size_t>(m_filament_idx))
continue;
ColorDecomposePhysicalFilament filament;
filament.color_hex = m_physical_colors[i];
filament.name = i < m_filament_names.size() ? m_filament_names[i] : "";
filament.type = i < m_filament_types.size() ? m_filament_types[i] : "";
filament.filament_index = static_cast<unsigned int>(i + 1);
physical_filaments.push_back(std::move(filament));
}
const ColorDecomposeRgb target_rgb = wx_colour_to_recipe_rgb(m_target_color);
auto material_recipe = recommend_from_physical_filaments(target_rgb, physical_filaments, m_preferred_type);
if (material_recipe.valid) {
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
to_dialog_result(material_recipe, m_target_color);
} else {
std::vector<DecomposeComponent> components;
for (size_t i = 0; i < std::min<size_t>(2, physical_filaments.size()); ++i) {
DecomposeComponent comp;
comp.colour = wxColour(physical_filaments[i].color_hex);
comp.ratio = 50;
comp.filament_index = static_cast<int>(physical_filaments[i].filament_index);
components.push_back(comp);
}
if (components.empty()) {
components.push_back({m_target_color, 100, -1});
} else if (components.size() == 1) {
components.front().ratio = 100;
}
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
fallback_result(DecomposeMode::MaterialList, components);
}
ColorDecomposeResult single_base;
if (try_build_single_base_result(DecomposeMode::CMYW, single_base)) {
m_mode_results[mode_index(DecomposeMode::CMYW)] = single_base;
} else {
auto cmyw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::CMYW, m_preferred_type);
m_mode_results[mode_index(DecomposeMode::CMYW)] = cmyw_recipe.valid
? to_dialog_result(cmyw_recipe, m_target_color)
: fallback_result(DecomposeMode::CMYW, {
{CMYW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
{CMYW_CYAN, 50, -1, DecomposeBaseColor::Cyan}
});
}
if (try_build_single_base_result(DecomposeMode::RYBW, single_base)) {
m_mode_results[mode_index(DecomposeMode::RYBW)] = single_base;
} else {
auto rybw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::RYBW, m_preferred_type);
m_mode_results[mode_index(DecomposeMode::RYBW)] = rybw_recipe.valid
? to_dialog_result(rybw_recipe, m_target_color)
: fallback_result(DecomposeMode::RYBW, {
{RYBW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
{RYBW_BLUE, 50, -1, DecomposeBaseColor::Blue}
});
}
m_result = m_mode_results[mode_index(m_selected_mode)];
update_mode_card_contents();
update_ok_button_state();
}
} // namespace GUI
} // namespace Slic3r
+152
View File
@@ -0,0 +1,152 @@
#ifndef slic3r_ColorDecomposeDialog_hpp_
#define slic3r_ColorDecomposeDialog_hpp_
#include <array>
#include <functional>
#include <string>
#include <vector>
#include <utility>
#include <wx/colour.h>
#include <wx/panel.h>
#include <wx/statbmp.h>
#include <wx/stattext.h>
#include "GUI_Utils.hpp"
#include "libslic3r/ColorDecomposeRecipe.hpp"
class Button;
class CheckBox;
class ComboBox;
namespace Slic3r {
namespace GUI {
using DecomposeMode = ColorDecomposeRecipeMode;
enum class DecomposeBaseColor {
None,
Cyan,
Magenta,
Yellow,
White,
Red,
Green,
Blue
};
struct DecomposeComponent {
wxColour colour;
int ratio{50}; // percentage
int filament_index{-1}; // 1-based physical filament index, -1 if standard base color
DecomposeBaseColor base_color{DecomposeBaseColor::None};
};
struct ColorDecomposeResult {
DecomposeMode mode{DecomposeMode::MaterialList};
wxColour matched_color;
std::vector<DecomposeComponent> components;
};
class ColorDecomposeDialog : public DPIDialog
{
public:
ColorDecomposeDialog(wxWindow* parent,
int filament_idx,
const wxColour& target_color,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& filament_names,
const std::vector<std::string>& filament_types,
size_t current_filament_count = 0,
size_t max_filament_count = 32,
std::vector<size_t> physical_config_indices = {});
ColorDecomposeResult get_result() const { return m_result; }
// Override the "new physical filaments" count used by the filament-limit
// warning. The Texture import path supplies its own calculator so the
// pre-check shares the exact reuse rule as its write-back (existing +
// virtual physical filaments), instead of the project-config based default
// that cannot see not-yet-committed virtual base colors.
void set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn);
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void build_ui();
wxBoxSizer* create_filament_selector();
wxBoxSizer* create_target_color_section();
wxBoxSizer* create_mode_selection_section();
wxPanel* create_mode_card(wxWindow* parent, DecomposeMode mode, const wxString& title);
wxBoxSizer* create_button_panel();
void select_mode(DecomposeMode mode);
void update_card_styles();
void update_card_visibility();
void update_mode_card_content(DecomposeMode mode);
void update_mode_card_contents();
void update_matched_color_display();
void update_ok_button_state();
void update_filament_limit_warning();
void compute_decomposition();
// When the target color is exactly one of the standard base colors for the
// preferred type, the standard card should show that base at 100% instead of
// a mix. PLA Basic covers CMYW and RYBW.
bool try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const;
struct ModeCardControls {
wxPanel* card{nullptr};
wxBoxSizer* components_sizer{nullptr};
};
ColorDecomposeResult m_result;
std::array<ColorDecomposeResult, 3> m_mode_results;
std::array<ModeCardControls, 3> m_mode_cards;
int m_filament_idx{-1};
wxColour m_target_color;
std::vector<std::string> m_physical_colors;
std::vector<std::string> m_filament_names;
std::vector<std::string> m_filament_types;
std::vector<std::string> m_project_types;
std::string m_preferred_type;
// Dropdown selectable item index -> material type string
std::vector<std::string> m_combo_item_types;
size_t m_current_filament_count{0};
size_t m_max_filament_count{32};
std::vector<size_t> m_physical_config_indices;
std::function<size_t(const ColorDecomposeResult&)> m_missing_calculator;
// UI controls
ComboBox* m_type_combo{nullptr};
wxPanel* m_target_swatch{nullptr};
wxStaticText* m_target_rgb_text{nullptr};
wxPanel* m_matched_swatch{nullptr};
wxStaticText* m_matched_rgb_text{nullptr};
// Mode cards
wxPanel* m_card_material_list{nullptr};
wxPanel* m_card_cmyw{nullptr};
wxPanel* m_card_rybw{nullptr};
wxPanel* m_arb_column_panel{nullptr};
CheckBox* m_chk_material_list{nullptr};
CheckBox* m_chk_cmyw{nullptr};
CheckBox* m_chk_rybw{nullptr};
DecomposeMode m_selected_mode{DecomposeMode::MaterialList};
// Hint shown when no mode card is visible
wxStaticText* m_no_card_hint{nullptr};
// Warning shown when decomposition would exceed filament limit
wxPanel* m_limit_warning_panel{nullptr};
wxStaticText* m_limit_warning_text{nullptr};
Button* m_btn_ok{nullptr};
Button* m_btn_cancel{nullptr};
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_ColorDecomposeDialog_hpp_
+386
View File
@@ -0,0 +1,386 @@
#include "ColorDecomposeSupport.hpp"
#include "MixedFilamentDialog.hpp"
#include "GUI_App.hpp"
#include "MsgDialog.hpp"
#include "I18N.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Utils.hpp"
#include "nlohmann/json.hpp"
#include <fstream>
#include <algorithm>
#include <cctype>
using json = nlohmann::json;
namespace Slic3r { namespace GUI {
std::string decompose_normalize_color_hex(std::string color)
{
if (color.size() >= 7)
color = color.substr(0, 7);
std::transform(color.begin(), color.end(), color.begin(), [](unsigned char c) {
return static_cast<char>(std::toupper(c));
});
return color;
}
const char* decompose_base_color_en(DecomposeBaseColor color)
{
switch (color) {
case DecomposeBaseColor::Cyan: return "Cyan";
case DecomposeBaseColor::Magenta: return "Magenta";
case DecomposeBaseColor::Yellow: return "Yellow";
case DecomposeBaseColor::White: return "White";
case DecomposeBaseColor::Red: return "Red";
case DecomposeBaseColor::Green: return "Green";
case DecomposeBaseColor::Blue: return "Blue";
default: return "";
}
}
wxString decompose_base_color_display(DecomposeBaseColor color)
{
switch (color) {
case DecomposeBaseColor::Cyan: return _L("Cyan");
case DecomposeBaseColor::Magenta: return _L("Magenta");
case DecomposeBaseColor::Yellow: return _L("Yellow");
case DecomposeBaseColor::White: return _L("White");
case DecomposeBaseColor::Red: return _L("Red");
case DecomposeBaseColor::Green: return _L("Green");
case DecomposeBaseColor::Blue: return _L("Blue");
default: return wxString();
}
}
std::string decompose_basic_type_from_source(size_t source_config_idx,
size_t source_physical_idx,
const std::vector<std::string>& physical_types)
{
auto& project_config = wxGetApp().preset_bundle->project_config;
if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) {
if (source_config_idx < filament_id_opt->values.size()) {
const std::string& filament_id = filament_id_opt->values[source_config_idx];
if (filament_id == kDecomposePetgFilamentId)
return kDecomposePetgBasicType;
if (filament_id == kDecomposePlaFilamentId)
return kDecomposePlaBasicType;
}
}
if (source_physical_idx < physical_types.size()) {
const std::string& type = physical_types[source_physical_idx];
if (type == kDecomposePetgShortType || type == kDecomposePetgBasicType)
return kDecomposePetgBasicType;
if (type == kDecomposePlaShortType || type == kDecomposePlaBasicType)
return kDecomposePlaBasicType;
}
return kDecomposePlaBasicType;
}
std::string decompose_basic_filament_id(const std::string& basic_type)
{
if (basic_type == kDecomposePetgBasicType)
return kDecomposePetgFilamentId;
return kDecomposePlaFilamentId;
}
void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component)
{
auto& project_config = wxGetApp().preset_bundle->project_config;
if (!component.filament_id.empty()) {
if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) {
while (filament_id_opt->values.size() <= config_idx)
filament_id_opt->values.push_back("");
filament_id_opt->values[config_idx] = component.filament_id;
}
}
const std::string type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgShortType :
component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaShortType : "";
if (!type.empty()) {
if (auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type")) {
while (type_opt->values.size() <= config_idx)
type_opt->values.push_back("");
type_opt->values[config_idx] = type;
}
}
}
DecomposeOfficialComponent lookup_decompose_official_component(
const std::string& basic_type,
DecomposeBaseColor base_color,
const wxColour& fallback)
{
DecomposeOfficialComponent result;
result.base_color = base_color;
result.color_hex = decompose_normalize_color_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
result.filament_id = decompose_basic_filament_id(basic_type);
const char* color_name = decompose_base_color_en(base_color);
if (color_name[0] == '\0')
return result;
// Some materials name a standard base color differently in the color-code
// table. PETG Basic's RYBW blue base is "Reflex Blue" (deep blue, B00,
// #001489), not "Blue". Match by an ordered list of exact English names so
// "Navy Blue" (B01, #0086D6) is never picked up by mistake.
std::vector<std::string> candidate_names;
candidate_names.emplace_back(color_name);
if (base_color == DecomposeBaseColor::Blue && basic_type == kDecomposePetgBasicType)
candidate_names.emplace_back("Reflex Blue");
std::ifstream ifs(resources_dir() + "/profiles/BBL/filament/filaments_color_codes.json");
if (!ifs)
return result;
json root = json::parse(ifs, nullptr, false);
if (root.is_discarded() || !root.contains("data") || !root["data"].is_array())
return result;
for (const std::string& candidate : candidate_names) {
for (const auto& item : root["data"]) {
if (!item.is_object() || item.value("fila_type", "") != basic_type)
continue;
if (!item.contains("fila_color_name"))
continue;
const auto& names = item["fila_color_name"];
if (!names.is_object() || names.value("en", "") != candidate)
continue;
if (item.contains("fila_color") && item["fila_color"].is_array() && !item["fila_color"].empty())
result.color_hex = decompose_normalize_color_hex(item["fila_color"][0].get<std::string>());
result.filament_id = item.value("fila_id", result.filament_id);
return result;
}
}
return result;
}
std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type)
{
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
if (source_config_idx < preset_bundle.filament_presets.size()) {
const std::string& source_name = preset_bundle.filament_presets[source_config_idx];
if (source_name.find(std::string(kDecomposeBambuPresetPrefix) + basic_type) != std::string::npos)
return source_name;
}
const std::string prefix = std::string(kDecomposeBambuPresetPrefix) + basic_type + " @BBL ";
for (const std::string& preset_name : preset_bundle.filament_presets) {
if (preset_name.find(prefix) == 0)
return preset_name;
}
return {};
}
std::string official_basic_type_from_preset_name(const std::string& preset_name)
{
if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePlaBasicType) != std::string::npos)
return kDecomposePlaBasicType;
if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePetgBasicType) != std::string::npos)
return kDecomposePetgBasicType;
return {};
}
std::string filament_type_for_color_decompose(Preset* preset)
{
if (!preset)
return kDecomposePlaShortType;
std::string display_type;
std::string ft = preset->config.get_filament_type(display_type);
const std::string basic = official_basic_type_from_preset_name(preset->name);
if (!basic.empty())
ft = basic;
if (ft.empty())
ft = kDecomposePlaShortType;
return ft;
}
int find_existing_decompose_component(
const DecomposeOfficialComponent& component,
const std::vector<std::string>& physical_colors,
const std::vector<size_t>& physical_config_indices,
size_t source_config_idx)
{
auto& project_config = wxGetApp().preset_bundle->project_config;
auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id");
auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type");
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
const size_t num_physical = physical_colors.size();
const std::string expected_basic_type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgBasicType :
component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaBasicType : "";
const std::string expected_short_type = expected_basic_type == kDecomposePetgBasicType ? kDecomposePetgShortType :
expected_basic_type == kDecomposePlaBasicType ? kDecomposePlaShortType : "";
const std::string expected_preset_part = expected_basic_type.empty() ? "" : std::string(kDecomposeBambuPresetPrefix) + expected_basic_type;
for (size_t i = 0; i < num_physical && i < physical_config_indices.size(); ++i) {
const size_t config_idx = physical_config_indices[i];
const std::string slot_color = decompose_normalize_color_hex(physical_colors[i]);
const std::string slot_filament_id = (filament_id_opt && config_idx < filament_id_opt->values.size()) ? filament_id_opt->values[config_idx] : "";
const std::string slot_type = (type_opt && config_idx < type_opt->values.size()) ? type_opt->values[config_idx] : "";
const std::string preset_name = config_idx < preset_bundle.filament_presets.size() ? preset_bundle.filament_presets[config_idx] : "";
if (config_idx == source_config_idx) {
continue;
}
if (slot_color != component.color_hex) {
continue;
}
if (!component.filament_id.empty() && slot_filament_id == component.filament_id) {
return static_cast<int>(config_idx + 1);
}
if (!expected_basic_type.empty() && (slot_type == expected_basic_type || slot_type == expected_short_type)) {
return static_cast<int>(config_idx + 1);
}
if (!expected_preset_part.empty() && preset_name.find(expected_preset_part) != std::string::npos) {
return static_cast<int>(config_idx + 1);
}
const bool has_material_hint = !slot_filament_id.empty() || !slot_type.empty() || !preset_name.empty();
if (!expected_basic_type.empty() && has_material_hint)
continue;
return static_cast<int>(config_idx + 1);
}
return -1;
}
bool prepare_decompose_mixed_result(
const ColorDecomposeResult& result,
size_t source_config_idx,
size_t source_physical_idx,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_types,
const std::vector<size_t>& physical_config_indices,
MixedFilamentResult& out_result,
std::vector<DecomposeMissingComponent>& missing)
{
out_result = {};
missing.clear();
if (result.components.size() < 2) {
return false;
}
const bool standard_mode = result.mode == DecomposeMode::CMYW || result.mode == DecomposeMode::RYBW;
std::string basic_type;
std::string preset_name;
if (standard_mode) {
basic_type = decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types);
preset_name = find_decompose_standard_preset_name(source_config_idx, basic_type);
}
for (size_t i = 0; i < result.components.size(); ++i) {
const DecomposeComponent& comp = result.components[i];
out_result.ratios.push_back(comp.ratio);
if (!standard_mode) {
if (comp.filament_index <= 0) {
return false;
}
const size_t physical_idx = static_cast<size_t>(comp.filament_index - 1);
if (physical_idx >= physical_config_indices.size()) {
return false;
}
out_result.components.push_back(static_cast<unsigned int>(physical_config_indices[physical_idx] + 1));
continue;
}
if (comp.base_color == DecomposeBaseColor::None) {
return false;
}
DecomposeOfficialComponent official_component =
lookup_decompose_official_component(basic_type, comp.base_color, comp.colour);
int existing_idx = find_existing_decompose_component(official_component, physical_colors,
physical_config_indices, source_config_idx);
if (existing_idx > 0) {
out_result.components.push_back(static_cast<unsigned int>(existing_idx));
continue;
}
DecomposeMissingComponent missing_comp;
missing_comp.component_idx = out_result.components.size();
missing_comp.official_component = official_component;
missing_comp.preset_name = preset_name;
missing_comp.display_name = decompose_base_color_display(comp.base_color) +
wxString::FromUTF8(" ") + wxString::FromUTF8(basic_type);
missing.push_back(std::move(missing_comp));
out_result.components.push_back(0);
}
const bool ok = out_result.components.size() == out_result.ratios.size() && out_result.components.size() >= 2;
return ok;
}
size_t count_decompose_new_physical_filaments(
const ColorDecomposeResult& result,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_types,
size_t source_physical_idx,
const std::vector<size_t>* physical_config_indices)
{
if (result.mode != DecomposeMode::CMYW && result.mode != DecomposeMode::RYBW)
return 0;
std::vector<size_t> fallback_indices;
const std::vector<size_t>* indices = physical_config_indices;
if (!indices) {
fallback_indices.resize(physical_colors.size());
for (size_t i = 0; i < fallback_indices.size(); ++i)
fallback_indices[i] = i;
indices = &fallback_indices;
}
size_t source_config_idx = size_t(-1);
if (source_physical_idx < indices->size())
source_config_idx = (*indices)[source_physical_idx];
const std::string basic_type =
decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types);
size_t missing_count = 0;
for (const DecomposeComponent& comp : result.components) {
if (comp.base_color == DecomposeBaseColor::None)
continue;
DecomposeOfficialComponent official_component =
lookup_decompose_official_component(basic_type, comp.base_color, comp.colour);
int existing_idx = find_existing_decompose_component(official_component, physical_colors,
*indices, source_config_idx);
if (existing_idx <= 0)
++missing_count;
}
return missing_count;
}
bool confirm_create_decompose_missing_components(wxWindow* parent, const std::vector<DecomposeMissingComponent>& missing)
{
if (missing.empty())
return true;
static const char* config_key = "not_show_color_decompose_missing_component_tip";
if (wxGetApp().app_config->get(config_key) == "1") {
return true;
}
wxString missing_text;
for (size_t i = 0; i < missing.size(); ++i) {
if (i > 0)
missing_text += _L(", ");
missing_text += missing[i].display_name;
}
wxString message = _L("The current filament list does not contain ") + missing_text +
_L(". A project filament required by the mixed filament will be created automatically after decomposition.");
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
dlg.show_dsa_button();
int res = dlg.ShowModal();
if (res == wxID_OK && dlg.get_checkbox_state())
wxGetApp().app_config->set(config_key, "1");
return res == wxID_OK;
}
}} // namespace Slic3r::GUI
+104
View File
@@ -0,0 +1,104 @@
#ifndef slic3r_GUI_ColorDecomposeSupport_hpp_
#define slic3r_GUI_ColorDecomposeSupport_hpp_
#include <string>
#include <vector>
#include <wx/string.h>
#include <wx/colour.h>
#include "ColorDecomposeDialog.hpp"
class wxWindow;
namespace Slic3r {
class Preset;
namespace GUI {
// ---- Constants ----
inline constexpr const char* kDecomposePlaBasicType = "PLA Basic";
inline constexpr const char* kDecomposePetgBasicType = "PETG Basic";
inline constexpr const char* kDecomposePlaShortType = "PLA";
inline constexpr const char* kDecomposePetgShortType = "PETG";
inline constexpr const char* kDecomposePlaFilamentId = "GFA00";
inline constexpr const char* kDecomposePetgFilamentId = "GFG00";
inline constexpr const char* kDecomposeBambuPresetPrefix = "Bambu ";
// ---- Types ----
struct DecomposeOfficialComponent {
DecomposeBaseColor base_color{DecomposeBaseColor::None};
std::string color_hex;
std::string filament_id;
};
struct DecomposeMissingComponent {
size_t component_idx{0};
DecomposeOfficialComponent official_component;
std::string preset_name;
wxString display_name;
};
struct MixedFilamentResult;
// ---- Functions ----
std::string decompose_normalize_color_hex(std::string color);
const char* decompose_base_color_en(DecomposeBaseColor color);
wxString decompose_base_color_display(DecomposeBaseColor color);
std::string decompose_basic_type_from_source(size_t source_config_idx,
size_t source_physical_idx,
const std::vector<std::string>& physical_types);
std::string decompose_basic_filament_id(const std::string& basic_type);
void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component);
DecomposeOfficialComponent lookup_decompose_official_component(
const std::string& basic_type,
DecomposeBaseColor base_color,
const wxColour& fallback);
std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type);
// Returns "PLA Basic" / "PETG Basic" when preset_name names an official Bambu
// basic filament, else an empty string.
std::string official_basic_type_from_preset_name(const std::string& preset_name);
// Resolve display type for color-decompose: official Bambu Basic overrides
// get_filament_type when preset name matches; empty/missing -> "PLA".
std::string filament_type_for_color_decompose(Preset* preset);
int find_existing_decompose_component(
const DecomposeOfficialComponent& component,
const std::vector<std::string>& physical_colors,
const std::vector<size_t>& physical_config_indices,
size_t source_config_idx);
bool prepare_decompose_mixed_result(
const ColorDecomposeResult& result,
size_t source_config_idx,
size_t source_physical_idx,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_types,
const std::vector<size_t>& physical_config_indices,
MixedFilamentResult& out_result,
std::vector<DecomposeMissingComponent>& missing);
// For standard modes: how many base colors are not reusable from physical list.
// MaterialList returns 0. When physical_config_indices is null, indices are 0..n-1.
size_t count_decompose_new_physical_filaments(
const ColorDecomposeResult& result,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_types,
size_t source_physical_idx,
const std::vector<size_t>* physical_config_indices);
bool confirm_create_decompose_missing_components(wxWindow* parent,
const std::vector<DecomposeMissingComponent>& missing);
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_ColorDecomposeSupport_hpp_
+57 -12
View File
@@ -577,22 +577,67 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
}
// BBS
static const char* keys[] = { "support_filament", "support_interface_filament"};
for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
std::string key = std::string(keys[i]);
// Reset filament overrides pointing at a slot that no longer exists. Support and the wipe
// tower additionally reject mixed slots: the engine consumes those keys directly, so a virtual
// slot would reach the G-code unresolved, while the per-feature keys are resolved per layer.
static const char* physical_only_keys[] = { "support_filament", "support_interface_filament", "wipe_tower_filament" };
static const char* feature_keys[] = { "outer_wall_filament_id", "inner_wall_filament_id",
"sparse_infill_filament_id", "internal_solid_filament_id",
"top_surface_filament_id", "bottom_surface_filament_id" };
auto reset_invalid_filament = [this, config, filament_cnt](const char* key, bool allow_mixed) {
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
if (opt != nullptr) {
if (opt->getInt() > filament_cnt) {
DynamicPrintConfig new_conf = *config;
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
int new_value = 0;
if (conf_temp != nullptr && conf_temp->has(key)) {
new_value = conf_temp->opt_int(key);
if (opt == nullptr)
return;
const int val = opt->getInt();
const bool out_of_range = val > filament_cnt;
const bool is_mixed = !allow_mixed && val > 0 && val <= filament_cnt &&
wxGetApp().preset_bundle->is_mixed_filament(val - 1);
if (!out_of_range && !is_mixed)
return;
DynamicPrintConfig new_conf = *config;
int new_value = 0;
if (out_of_range) {
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
if (conf_temp != nullptr && conf_temp->has(key))
new_value = conf_temp->opt_int(key);
}
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
apply(config, &new_conf);
};
for (const char* key : physical_only_keys)
reset_invalid_filament(key, false);
for (const char* key : feature_keys)
reset_invalid_filament(key, true);
// Sub-layer splitting divides each layer by the mix ratio; an adaptive layer profile makes
// those sub-layer heights vary per layer, which degrades the blend. Warn once per enable.
{
static bool s_mixed_sublayer_warned = false;
bool sublayer_on = config->opt_bool("enable_mixed_color_sublayer");
if (sublayer_on && !s_mixed_sublayer_warned &&
wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
bool has_variable_layer = false;
for (const auto* obj : wxGetApp().model().objects) {
if (obj->layer_height_profile.get().size() > 4) {
has_variable_layer = true;
break;
}
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
apply(config, &new_conf);
}
if (has_variable_layer) {
MessageDialog dialog(m_msg_dlg_parent,
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
"", wxICON_WARNING | wxOK);
dialog.show_dsa_button();
is_msg_dlg_already_exist = true;
dialog.ShowModal();
is_msg_dlg_already_exist = false;
if (dialog.get_checkbox_state())
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
s_mixed_sublayer_warned = true;
}
}
if (!sublayer_on)
s_mixed_sublayer_warned = false;
}
if (config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None &&
+1 -1
View File
@@ -64,7 +64,7 @@ public:
DevFirmware(MachineObject* obj) : m_owner(obj) {}
private:
MachineObject* m_owner = nullptr;
[[maybe_unused]] MachineObject* m_owner = nullptr;
};
} // namespace Slic3r
+1
View File
@@ -27,6 +27,7 @@ void DevStatus::ParseStatus(const nlohmann::json& print_jj)
#else
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get exception=" << e.what();
#endif
(void)e; // suppress C4101 when BBL_RELEASE_TO_PUBLIC
}
}
@@ -2,7 +2,7 @@
/* File: uiAMSBestPositionPopup.hpp
* Description: The popup with suggest best ams position
*
//**********************************************************/
************************************************************/
#include "uiAMSBestPositionPopup.hpp"
@@ -2,7 +2,7 @@
/* File: uiAMSBestPositionPopup.hpp
* Description: The popup with suggest best ams position
*
//**********************************************************/
************************************************************/
#pragma once
#include "slic3r/GUI/Widgets/AMSItem.hpp"
@@ -6,7 +6,7 @@
* \n class wgtDeviceNozzleRackNozzleItem;
* \n class wgtDeviceNozzleRackToolHead;
* \n class wgtDeviceNozzleRackPos;
//**********************************************************/
************************************************************/
#include "wgtDeviceNozzleRack.h"
#include "wgtDeviceNozzleRackUpdate.h"
@@ -6,7 +6,7 @@
* \n class wgtDeviceNozzleRackNozzleItem;
* \n class wgtDeviceNozzleRackToolHead;
* \n class wgtDeviceNozzleRackPos;
//**********************************************************/
************************************************************/
#pragma once
#include "slic3r/GUI/DeviceCore/DevNozzleRack.h"
@@ -3,7 +3,7 @@
* Description: The panel with rack updating
*
* \n class wgtDeviceNozzleRackUpdate
//**********************************************************/
************************************************************/
#include "wgtDeviceNozzleRackUpdate.h"
@@ -3,7 +3,7 @@
* Description: The panel for updating hotends
*
* \n class wgtDeviceNozzleRackUpdate
//**********************************************************/
************************************************************/
#pragma once
#include "slic3r/GUI/DeviceCore/DevNozzleRack.h"
@@ -3,7 +3,7 @@
* Description: The panel to select nozzle
*
* \n class wgtDeviceNozzleSelect;
//**********************************************************/
************************************************************/
#include "wgtDeviceNozzleSelect.h"
#include "wgtDeviceNozzleRack.h"
@@ -3,7 +3,7 @@
* Description: The panel to select nozzle
*
* \n class wgtDeviceNozzleSelect;
//**********************************************************/
************************************************************/
#pragma once
@@ -26,8 +26,6 @@
#include "Widgets/HyperLink.hpp" // ORCA
#define DESIGN_INPUT_SIZE wxSize(FromDIP(100), -1)
namespace Slic3r {
namespace GUI {
+2 -2
View File
@@ -385,7 +385,7 @@ public:
wxWindow* window{ nullptr };
void BUILD() override;
/// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
void propagate_value() ;
void propagate_value() override;
void set_value(const std::string& value, bool change_event = false) {
m_disable_change_event = !change_event;
@@ -440,7 +440,7 @@ public:
wxWindow* window{ nullptr };
void BUILD() override;
// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
void propagate_value();
void propagate_value() override;
/* Under OSX: wxBitmapComboBox->GetWindowStyle() returns some weard value,
* so let use a flag, which has TRUE value for a control without wxCB_READONLY style
+184
View File
@@ -4,7 +4,10 @@
#include <cmath>
#include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r { namespace GUI {
@@ -28,6 +31,113 @@ void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from,
}
}
static std::string to_hex(const wxColour& c)
{
return wxString::Format("#%02X%02X%02X", c.Red(), c.Green(), c.Blue()).ToStdString();
}
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights)
{
const size_t n = std::min(cols.size(), weights.size());
std::vector<std::string> hex_colors;
std::vector<int> int_weights;
hex_colors.reserve(n);
int_weights.reserve(n);
for (size_t i = 0; i < n; ++i) {
hex_colors.push_back(to_hex(cols[i]));
// Scale double weights (e.g. 0.5) to int (5000) for blend_color_multi;
// only relative magnitude matters.
int_weights.push_back(static_cast<int>(std::lround(weights[i] * 10000.0)));
}
wxColour blended(Slic3r::blend_color_multi(hex_colors, int_weights));
return blended.IsOk() ? blended : wxColour(128, 128, 128);
}
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
const wxColour& second,
const Slic3r::GradientCurve& curve,
int steps)
{
std::vector<wxColour> ramp;
if (steps <= 0 || curve.points.size() < 2) return ramp;
ramp.reserve(steps);
for (int i = 0; i < steps; ++i) {
const double t = (steps > 1) ? (i + 0.5) / steps : 0.5;
const double r1 = Slic3r::sample_gradient_curve(curve, t);
ramp.push_back(blend_n_colors({first, second}, {r1, 1.0 - r1}));
}
return ramp;
}
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
// endpoints, otherwise the 0.10 -> 0.90 default.
static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
{
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
Slic3r::GradientCurve custom = Slic3r::parse_gradient_curve(curve_opt->values[slot]);
if (custom.points.size() >= 2) return custom;
}
double start = kGradientMinRatio, end = kGradientMaxRatio;
const auto* range_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_range");
if (range_opt && slot < range_opt->values.size() && !range_opt->values[slot].empty()) {
CNumericLocalesSetter c_locale_setter;
float v0 = 0, v1 = 0;
if (std::sscanf(range_opt->values[slot].c_str(), "%f,%f", &v0, &v1) == 2 &&
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
start = v0;
end = v1;
}
}
Slic3r::GradientCurve curve;
curve.points = {{0.0, start, NAN, NAN}, {1.0, end, NAN, NAN}};
return curve;
}
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps)
{
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
if (!is_mixed_opt || !grad_opt || !comp_opt || !colour_opt) return {};
if (slot >= is_mixed_opt->values.size() || !is_mixed_opt->values[slot]) return {};
if (slot >= grad_opt->values.size() || !grad_opt->values[slot]) return {};
if (slot >= comp_opt->values.size()) return {};
// Only two-component slots fade; anything else stays on the plain blended swatch.
const auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[slot]);
if (comp_ids.size() != 2) return {};
auto component_colour = [&](unsigned int id) {
wxColour c = (id >= 1 && id <= colour_opt->values.size()) ? wxColour(colour_opt->values[id - 1]) : wxColour();
return c.IsOk() ? c : wxColour("#D9D9D9");
};
// Both gradient_range and the curve express the *first* component's ratio over Z, so
// the components stay in config order and the curve alone decides which end is which.
return sample_gradient_ramp(component_colour(comp_ids[0]), component_colour(comp_ids[1]),
mixed_gradient_curve(cfg, slot), steps);
}
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp)
{
if (rect.width <= 0 || rect.height <= 0 || ramp.empty()) return;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int y = 0; y < rect.height; ++y) {
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
// Mapping over height - 1 puts both ends of the ramp on screen even in a short swatch.
const double t = (rect.height > 1) ? (double) (rect.height - 1 - y) / (rect.height - 1) : 0.5;
dc.SetBrush(wxBrush(ramp[static_cast<size_t>(t * (ramp.size() - 1) + 0.5)]));
dc.DrawRectangle(rect.x, rect.y + y, rect.width, 1);
}
}
// Helper struct to hold bitmap and DC
struct BitmapDC {
wxBitmap bitmap;
@@ -47,6 +157,19 @@ static BitmapDC init_bitmap_dc(const wxSize& size) {
return BitmapDC(size);
}
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size)
{
if (ramp.empty()) return wxNullBitmap;
BitmapDC bdc = init_bitmap_dc(size);
if (!bdc.dc.IsOk()) return wxNullBitmap;
fill_gradient_ramp_rect(bdc.dc, wxRect(0, 0, size.GetWidth(), size.GetHeight()), ramp);
bdc.dc.SelectObject(wxNullBitmap);
return bdc.bitmap;
}
// Check if a color is transparent (alpha == 0)
static bool is_transparent_color(const wxColour& color) {
return color.Alpha() == 0;
@@ -265,4 +388,65 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
}
}
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
const Slic3r::DynamicPrintConfig& cfg)
{
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
const auto* ratio_opt = cfg.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
if (!is_mixed_opt || !comp_opt) return;
const size_t n = is_mixed_opt->values.size();
if (colors.size() < n) colors.resize(n);
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
const auto kFallback = wxColour(128, 128, 128, 255);
for (size_t i = 0; i < n; ++i) {
if (!is_mixed_opt->values[i]) continue;
if (i >= comp_opt->values.size()) { colors[i] = kFallback; continue; }
auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[i]);
if (comp_ids.empty()) { colors[i] = kFallback; continue; }
bool is_gradient = grad_opt && i < grad_opt->values.size() && grad_opt->values[i];
std::vector<unsigned int> use_ids = comp_ids;
std::vector<int> weights;
if (is_gradient && comp_ids.size() >= 2) {
use_ids = { comp_ids.front(), comp_ids.back() };
weights = { 5000, 5000 };
} else {
auto ratios_d = Slic3r::parse_mixed_ratios(
(ratio_opt && i < ratio_opt->values.size()) ? ratio_opt->values[i] : std::string{},
comp_ids.size());
weights.reserve(comp_ids.size());
for (double r : ratios_d)
weights.push_back(static_cast<int>(std::lround(r * 10000.0)));
}
std::vector<std::string> hex_colors;
hex_colors.reserve(use_ids.size());
bool any_invalid = false;
for (unsigned int id : use_ids) {
if (id == 0 || id > colors.size()) { any_invalid = true; break; }
wxColour c = colors[id - 1];
if (c.IsOk() && (c.Red() > 0 || c.Green() > 0 || c.Blue() > 0)) {
hex_colors.push_back(to_hex(c));
} else if (colour_opt && (id - 1) < colour_opt->values.size()) {
hex_colors.push_back(colour_opt->values[id - 1]);
} else {
any_invalid = true; break;
}
}
if (any_invalid) { colors[i] = kFallback; continue; }
std::string hex = Slic3r::blend_color_multi(hex_colors, weights);
wxColour blended(hex);
if (!blended.IsOk()) blended = kFallback;
colors[i] = wxColour(blended.Red(), blended.Green(), blended.Blue(), 255);
}
}
}} // namespace Slic3r::GUI
+35
View File
@@ -7,6 +7,10 @@
#include <wx/gdicmn.h>
#include <vector>
// Orca: forward-declare so the header is self-contained outside libslic3r_gui's
// force-included pch (the GUI test suite includes it directly).
namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; }
namespace Slic3r { namespace GUI {
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
@@ -28,6 +32,37 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors,
const wxSize& size,
bool force_gradient = false);
// Blend colours at the given relative weights through blend_color_multi, so a measured
// real-world mix is used where one exists instead of a plain channel lerp.
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights);
// Sample a gradient mixed filament the way the slicer builds it: t runs 0..1 over the
// model's height, the curve gives the first component's ratio at t, and the two
// components are blended at that ratio through blend_n_colors. Entry 0 is the bottom
// of the model, the last entry its top.
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
const wxColour& second,
const Slic3r::GradientCurve& curve,
int steps);
// Same ramp for a project config slot, resolving components, colours and curve (or the
// linear gradient_range fallback) from cfg. Returns empty for any slot that is not a
// two-component gradient mixed filament. steps is the ramp's resolution; pass the
// destination's height in pixels.
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
// Fill rect with a ramp, ramp.front() along the bottom edge.
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
// Swatch bitmap for a gradient mixed filament, drawn bottom to top from the ramp.
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size);
// Recompute blended representative colors for mixed (virtual) filament slots.
// Reads mixed-filament config keys from cfg and writes back into colors[i]
// for every slot where filament_is_mixed[i] is true.
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
const Slic3r::DynamicPrintConfig& cfg);
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_FilamentBitmapUtils_hpp_
+22 -1
View File
@@ -420,7 +420,7 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
if (properties_shown) {
float label_w = 0.0f;
float value_w = 0.0f;
properties_rows.reserve(13);
properties_rows.reserve(14);
auto add_row = [&properties_rows, &label_w, &value_w](std::string label, std::string value) {
label_w = std::max(label_w, ImGui::CalcTextSize(label.c_str()).x);
value_w = std::max(value_w, ImGui::CalcTextSize(value.c_str()).x);
@@ -433,6 +433,27 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
add_row(_u8L("Width"), buff);
if (is_extrusion) sprintf(buff, ("%.3f " + _u8L("mm")).c_str(), vertex.height); else strcpy(buff, NA_CSTR);
add_row(_u8L("Height"), buff);
// ORCA: Length of the move ending at the current vertex. Arc moves (G2/G3) are discretized
// into several vertices sharing the same gcode line id, so accumulate the whole run to report
// the arc length instead of the length of a single chord.
if (vertex_id > 0 && (is_extrusion || vertex.is_travel() || vertex.is_wipe())) {
const size_t vertices_count = viewer->get_vertices_count();
size_t first_id = vertex_id;
while (first_id > 0 && viewer->get_vertex_at(first_id - 1).gcode_id == vertex.gcode_id)
--first_id;
size_t last_id = vertex_id;
while (last_id + 1 < vertices_count && viewer->get_vertex_at(last_id + 1).gcode_id == vertex.gcode_id)
++last_id;
float length = 0.0f;
for (size_t i = std::max<size_t>(first_id, 1); i <= last_id; ++i) {
length += (libvgcode::convert(viewer->get_vertex_at(i).position) -
libvgcode::convert(viewer->get_vertex_at(i - 1).position)).norm();
}
sprintf(buff, ("%.3f " + _u8L("mm")).c_str(), length);
}
else
strcpy(buff, NA_CSTR);
add_row(_u8L("Length"), buff);
sprintf(buff, "%d", vertex.layer_id + 1);
add_row(_u8L("Layer"), buff);
sprintf(buff, ("%.1f " + _u8L("mm/s")).c_str(), vertex.feedrate);
+44 -2
View File
@@ -155,6 +155,11 @@ std::string& get_filament_mixture_warning_text(){
return filament_mixture_warning_text;
}
std::string& get_single_extruder_mixed_filament_warning_text(){
static std::string single_extruder_mixed_filament_warning_text;
return single_extruder_mixed_filament_warning_text;
}
static std::string format_number(float value)
{
@@ -2984,6 +2989,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
bool mix_pla_and_petg = cur_plate->check_mixture_of_pla_and_petg(full_config_temp);
_set_warning_notification(EWarning::MixUsePLAAndPETG, !mix_pla_and_petg);
bool single_extruder_mixed_risk = cur_plate->check_single_extruder_mixed_filament_risk(full_config_temp, get_single_extruder_mixed_filament_warning_text());
_set_warning_notification(EWarning::SingleExtruderMixedFilament, single_extruder_mixed_risk);
bool filament_nozzle_compatible = cur_plate->check_compatible_of_nozzle_and_filament(full_config_temp, wxGetApp().preset_bundle->filament_presets, get_nozzle_filament_incompatible_text());
_set_warning_notification(EWarning::NozzleFilamentIncompatible, !filament_nozzle_compatible);
@@ -3010,6 +3018,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
_set_warning_notification(EWarning::TPUPrintableError, false);
_set_warning_notification(EWarning::FilamentPrintableError, false);
_set_warning_notification(EWarning::MixUsePLAAndPETG, false);
_set_warning_notification(EWarning::SingleExtruderMixedFilament, false);
_set_warning_notification(EWarning::PrimeTowerOutside, false);
_set_warning_notification(EWarning::MultiExtruderPrintableError,false);
_set_warning_notification(EWarning::MultiExtruderHeightOutside,false);
@@ -8902,7 +8911,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED;
}
else {
if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice()))
// A plate using a mixed filament whose components are broken cannot be sliced,
// so surface that on the plate toolbar the same way an unsliceable plate is.
if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice())
|| wxGetApp().plater()->sidebar().has_broken_mixed_filament(plate_list.get_plate(i)))
m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED;
else {
if (plate_list.get_plate(i)->get_slicing_percent() < 0.0f)
@@ -9669,6 +9681,13 @@ void GLCanvas3D::_render_paint_toolbar() const
}
}
}
// ORCA: the loop above only labels a slot whose preset was found in the preset collection,
// while the render loop below iterates extruder_num. Pad the label arrays so a slot without a
// matching preset cannot index past them; a garbage std::string crashes ImGui::CalcTextSize.
while (int(filament_text_first_line.size()) < extruder_num) {
filament_text_first_line.emplace_back();
filament_text_second_line.emplace_back();
}
ImGuiWrapper& imgui = *wxGetApp().imgui();
const float canvas_w = float(get_canvas_size().get_width());
@@ -9698,6 +9717,10 @@ void GLCanvas3D::_render_paint_toolbar() const
bool disabled = !wxGetApp().plater()->can_fillcolor();
ColorRGBA rgba;
// Gradient mixed filaments fade over Z, so their swatch is drawn as that fade rather than
// the single blended colour in `colors`. Every other slot's ramp is empty.
const auto& gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
for (int i = 0; i < extruder_num; i++) {
if (i > 0)
ImGui::SameLine();
@@ -9711,6 +9734,8 @@ void GLCanvas3D::_render_paint_toolbar() const
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max))
wxPostEvent(m_canvas, IntEvent(EVT_GLTOOLBAR_FILLCOLOR, i + 1));
}
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty())
ImGuiWrapper::draw_gradient_ramp(draw_list, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), gradient_ramps[i]);
if (ImGui::IsItemHovered() && i < 9) {
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });
@@ -9726,7 +9751,13 @@ void GLCanvas3D::_render_paint_toolbar() const
const float text_offset_y = 4.0f * em_unit * f_scale;
for (int i = 0; i < extruder_num; i++) {
decode_color(colors[i], rgba);
// A gradient slot's swatch shows its fade instead of the blended colour in `colors`, so the
// labels take their contrast from the colour printed at the middle of the fade they sit on.
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty()) {
const wxColour& c = gradient_ramps[i][gradient_ramps[i].size() / 2];
rgba = ColorRGBA(c.Red(), c.Green(), c.Blue(), c.Alpha());
} else
decode_color(colors[i], rgba);
float gray = 0.299 * rgba.r_uchar() + 0.587 * rgba.g_uchar() + 0.114 * rgba.b_uchar();
ImVec4 text_color = gray < 80 ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0, 0, 0, 1.0f);
@@ -10570,6 +10601,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
case EWarning::MixUsePLAAndPETG:
text = _u8L("PLA and PETG filaments detected in the mixture. Adjust parameters according to the Wiki to ensure print quality.");
break;
case EWarning::SingleExtruderMixedFilament:
text = get_single_extruder_mixed_filament_warning_text();
break;
case EWarning::PrimeTowerOutside:
text = _u8L("The prime tower extends beyond the plate boundary.");
break;
@@ -10618,6 +10652,14 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
notification_manager.close_slicing_customize_error_notification(NotificationType::BBLNozzleFilamentIncompatible, NotificationLevel::WarningNotificationLevel);
}
}
else if (warning == EWarning::SingleExtruderMixedFilament) {
// Close by type: check_single_extruder_mixed_filament_risk() clears the shared text
// buffer on every call, so a close-by-text would miss once the risk is gone.
if (state)
notification_manager.push_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel, text);
else
notification_manager.close_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel);
}
else {
if (state)
notification_manager.push_plater_warning_notification(text);
+1
View File
@@ -391,6 +391,7 @@ class GLCanvas3D
PrimeTowerOutside,
NozzleFilamentIncompatible,
MixtureFilamentIncompatible,
SingleExtruderMixedFilament,
FlushingVolumeZero
};
+2
View File
@@ -18,7 +18,9 @@
#import <IOKit/pwr_mgt/IOPMLib.h>
#elif _WIN32
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include "boost/nowide/convert.hpp"
#endif
+13 -3
View File
@@ -521,10 +521,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = {
/* FT_GCODE */ { L("G-code files"), { ".gcode"sv} },
#ifdef __APPLE__
/* FT_MODEL */
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
#else
/* FT_MODEL */
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}},
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".drc"sv}},
#endif
/* FT_ZIP */ { L("ZIP files"), { ".zip"sv } },
/* FT_PROJECT */ { L("Project files"), { ".3mf"sv} },
@@ -8905,7 +8905,17 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch
if (printer_technology == ptFFF && !edited_printer_preset.config.opt_bool("single_extruder_multi_material")) {
auto* nozzle_diameter = edited_printer_preset.config.option<ConfigOptionFloats>("nozzle_diameter");
if (nozzle_diameter) {
preset_bundle->set_num_filaments(nozzle_diameter->values.size());
// Mixed-color slots are virtual filaments kept at the tail of the list, so they have no
// nozzle of their own and the count has to allow for them. Only ever grow: this sizes
// the list so the combo boxes have something to bind to, and set_num_filaments() trims
// at the raw tail, so shrinking here would eat the mixes rather than the surplus
// physical slots. A list longer than the nozzle count is a state the app reaches
// legitimately - raising the extruder count and not saving the printer preset leaves
// exactly that on the next start - and losing the project's mixes to it is worse than
// carrying a filament the printer has no nozzle for until the count is next changed.
const size_t target = nozzle_diameter->values.size() + preset_bundle->num_mixed_filaments();
if (target > preset_bundle->filament_presets.size())
preset_bundle->set_num_filaments(target);
}
}
this->plater()->set_printer_technology(printer_technology);
+14 -10
View File
@@ -1656,16 +1656,16 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
{
wxMenu *menu = &m_filament_action_menu;
if (init) {
// ORCA rebuild menu everytime instead checking existing of every item then deleting
while (menu->GetMenuItemCount() > 0)
menu->Destroy(menu->FindItemByPosition(0));
//if (init) { //
append_menu_item(
menu, wxID_ANY, _L("Edit"), "", [](wxCommandEvent&) {
plater()->sidebar().edit_filament(); }, "", nullptr,
[]() { return true; }, m_parent);
}
const int item_id = menu->FindItem(_L("Merge with"));
if (item_id != wxNOT_FOUND)
menu->Destroy(item_id);
//}
wxMenu* sub_menu = new wxMenu();
std::vector<wxBitmap*> icons = get_extruder_color_icons(true);
@@ -1684,11 +1684,15 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
append_submenu(menu, sub_menu, wxID_ANY, _L("Merge with"), "", "",
[filaments_cnt]() { return filaments_cnt > 1; }, m_parent);
// Decompose a target colour into a printable mix of the loaded filaments. Placed before the
append_menu_item(
menu, wxID_ANY, _L("Decompose Color"), "", [](wxCommandEvent&) {
plater()->sidebar().decompose_filament_color(kSidebarContextMenuFilamentId); }, "", nullptr,
[]() { return plater()->sidebar().combos_filament().size() >= 2; }, m_parent);
menu->AppendSeparator(); // ORCA use seperator for reducing accidental clicks to delete
// ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS
const int delete_id = menu->FindItem(_L("Delete"));
if (delete_id != wxNOT_FOUND)
menu->Destroy(delete_id);
append_menu_item(
menu, wxID_ANY, _L("Delete"), _L("Delete this filament"), [](wxCommandEvent&) {
plater()->sidebar().delete_filament(-2); }, "", nullptr,
+18
View File
@@ -3233,6 +3233,24 @@ void ObjectList::merge(bool to_multipart_object)
void ObjectList::layers_editing()
{
// Height ranges give each range its own layer height, varying the mixed sub-layer heights just
// like an adaptive profile, so this raises the same warning as variable layer height and shares
// its do-not-show-again flag.
const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
if (print_config.opt_bool("enable_mixed_color_sublayer")) {
if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
// Orca: parent to the plater like the sibling site in Plater::priv::on_action_layersediting
// (BBS passes nullptr, which MsgDialog remaps to the main frame).
MessageDialog dlg(wxGetApp().plater(),
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
_L("Warning"), wxICON_WARNING | wxOK);
dlg.show_dsa_button();
dlg.ShowModal();
if (dlg.get_checkbox_state())
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
}
}
const Selection& selection = scene_selection();
const int obj_idx = selection.get_object_idx();
wxDataViewItem item = obj_idx >= 0 && GetSelectedItemsCount() > 1 && selection.is_single_full_object() ?
+3
View File
@@ -155,6 +155,9 @@ public:
update_dark_config();
on_sys_color_changed();
event.Skip();
#else
// Not calling Skip() is what stops the event propagating on Windows.
(void) this;
#endif // __WINDOWS__
});
+1 -1
View File
@@ -85,7 +85,7 @@ public:
void update_model_object();
//ClippingPlane get_sla_clipping_plane() const;
bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); }
bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); }
bool wants_enter_leave_snapshots() const override { return true; }
std::string get_gizmo_entering_text() const override { return _u8L("Entering Brim Ears"); }
-1
View File
@@ -597,7 +597,6 @@ void GLGizmoMeasure::on_render()
}
}
Vec3d position_on_model;
Vec3d direction_on_model;
size_t model_facet_idx = -1;
double closest_hit_distance = std::numeric_limits<double>::max();
{
+1 -1
View File
@@ -75,7 +75,7 @@ protected:
virtual void on_render() override;
virtual void on_set_state() override;
virtual CommonGizmosDataID on_get_requirements() const override;
virtual void on_render_input_window(float x, float y, float bottom_limit);
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
void on_load(cereal::BinaryInputArchive &ar) override;
void on_save(cereal::BinaryOutputArchive &ar) const override;
@@ -78,6 +78,9 @@ void GLGizmoMmuSegmentation::init_extruders_data()
m_extruders_colors = wxGetApp().plater()->get_extruders_colors();
m_selected_extruder_idx = 0;
m_gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
m_gradient_ramps.resize(m_extruders_colors.size());
// keep remap table consistent with current extruder count
m_extruder_remap.resize(m_extruders_colors.size());
for (size_t i = 0; i < m_extruder_remap.size(); ++i)
@@ -305,15 +308,32 @@ void GLGizmoMmuSegmentation::render_tooltip_button(float x, float y)
}
// ORCA
bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale)
bool GLGizmoMmuSegmentation::draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale)
{
// Inset of the frame stroked below, which is what trims the swatch down to its visible shape.
const float frame_inset = 1.5f;
ImDrawList* draw_list = ImGui::GetWindowDrawList();
std::string label_id = std::to_string(idx) + id_str + std::to_string(idx);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 size = ImVec2(27.f * scale, 27.f * scale);
ImVec4 color_vec = ImGuiWrapper::to_ImVec4(color);
ImU32 br_color = ImGui::ColorConvertFloat4ToU32(active ? ImGuiWrapper::COL_ORCA : m_is_dark_mode ? ImVec4(.35f, .35f, .35f, 1) : ImVec4(.85f, .85f, .85f, 1));
bool dark_tone = (0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
// Every caller labels the button with the 1 based slot number, so idx - 1 picks out the slot's fade.
const std::vector<wxColour>* gradient = gradient_of(idx - 1);
// The centered slot number sits at the swatch's mid height, so take its contrast from the colour
// printed there rather than from the slot's blended color.
bool dark_tone = gradient ? (*gradient)[gradient->size() / 2].GetLuminance() < 0.51 :
(0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
// Paint a gradient mixed filament's fade before the button and keep the button transparent, so
// the slot number and the frame below stay on top of it. The bands cannot round their corners,
// so the fade is inset to the frame, which masks it into the shape a plain color slot gets.
if (gradient) {
ImGuiWrapper::draw_gradient_ramp(draw_list, {pos.x + frame_inset * scale, pos.y + frame_inset * scale},
{pos.x + size.x - frame_inset * scale, pos.y + size.y - frame_inset * scale}, *gradient);
color_vec.w = 0.f; // let the fade show through
}
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding , 7.f * scale);
@@ -329,7 +349,7 @@ bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, cons
auto drawBorder = [&](float d, float r, float t, ImU32 col) {
draw_list->AddRect({pos.x + d * scale, pos.y + d * scale}, {pos.x + size.x - d * scale , pos.y + size.y - d * scale}, col, r * scale, 0, t * scale);
};
drawBorder(1.5f, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)));
drawBorder(frame_inset, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)));
if(active)
drawBorder(.5f, 4.f , 2.f, br_color);
else
@@ -433,7 +453,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott
m_selected_extruder_idx = extruder_idx;
}
if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
}
// ORCA: Remap filaments section (Border only, Title in border).
// Styled as a panel for visual grouping.
@@ -731,6 +751,10 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors()
continue;
int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0;
// A volume may be assigned to a mixed-color slot, whose index can sit past the
// physical colour list; fall back to the first colour rather than reading OOB.
if (extruder_idx >= (int)m_extruders_colors.size())
extruder_idx = 0;
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]);
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
@@ -753,6 +777,9 @@ void GLGizmoMmuSegmentation::update_triangle_selectors_colors()
TriangleSelectorPatch* selector = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get());
int extruder_idx = m_volumes_extruder_idxs[i];
int extruder_color_idx = std::max(0, extruder_idx - 1);
// A mixed-color slot can index past the physical colour list; fall back to the first colour.
if (extruder_color_idx >= (int)m_extruders_colors.size())
extruder_color_idx = 0;
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(m_extruders_colors[extruder_color_idx]);
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
@@ -73,11 +73,10 @@ public:
void data_changed(bool is_serializing) override;
// TriangleSelector::serialization/deserialization has a limit to store 19 different states.
// EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored.
// When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization
// will be also extended to support additional states, requiring at least one state to remain free out of 19 states.
static const constexpr size_t EXTRUDERS_LIMIT = 16;
// The paint material limit follows EnforcerBlockerType::ExtruderMax: TriangleSelector
// serialization covers the extended (17..32) range through an escape nibble. Mixed-color
// filaments occupy ordinary slots, so they draw from the same budget as physical ones.
static const constexpr size_t EXTRUDERS_LIMIT = static_cast<size_t>(EnforcerBlockerType::ExtruderMax);
const float get_cursor_radius_min() const override { return CursorRadiusMin; }
@@ -116,6 +115,10 @@ protected:
// Filament remap feature
std::vector<size_t> m_extruder_remap; // index → target extruder index
// Colours each gradient mixed filament actually prints, bottom of the model first, mirrored
// from Plater so the extruder swatches draw the same fade the editor previews. Plain
// filament slots keep an empty ramp.
std::vector<std::vector<wxColour>> m_gradient_ramps;
// ORCA: Cache used filaments to filter UI
std::set<size_t> m_used_filaments; // Set of used filament indices (cached)
@@ -137,7 +140,13 @@ private:
void init_model_triangle_selectors();
// ORCA
bool draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
bool draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
// Gradient ramp of a filament slot, or nullptr when the slot is a plain single color
// filament. A non-null result is never empty.
const std::vector<wxColour>* gradient_of(int idx) const
{
return idx >= 0 && idx < (int) m_gradient_ramps.size() && !m_gradient_ramps[idx].empty() ? &m_gradient_ramps[idx] : nullptr;
}
// BBS
void update_triangle_selectors_colors();
+1 -1
View File
@@ -67,7 +67,7 @@ protected:
void on_register_raycasters_for_picking() override;
void on_unregister_raycasters_for_picking() override;
//BBS: GUI refactor: add object manipulation
virtual void on_render_input_window(float x, float y, float bottom_limit);
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
private:
double calc_projection(const UpdateData& data) const;
+1 -1
View File
@@ -89,7 +89,7 @@ protected:
virtual void on_register_raycasters_for_picking() override;
virtual void on_unregister_raycasters_for_picking() override;
//BBS: GUI refactor: add object manipulation
virtual void on_render_input_window(float x, float y, float bottom_limit);
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
private:
void render_grabbers_connection(unsigned int id_1, unsigned int id_2, const ColorRGBA& color);
+37 -9
View File
@@ -998,16 +998,40 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
keyCode = keyCode- WXK_NUMPAD0+'0';
}
if (keyCode >= '0' && keyCode <= '9') {
if (keyCode == '1' && !m_timer_set_color.IsRunning()) {
// The paint palette reaches EXTRUDERS_LIMIT slots (mixed-color filaments take
// ordinary slots too), so any leading digit that can start a valid two-digit
// number waits briefly for a second one.
const int digit = keyCode - '0';
const int shortcut_max = int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT);
auto can_start_two_digit = [shortcut_max](int d) { return d > 0 && d * 10 <= shortcut_max; };
auto select = [mmu_seg](int number) { return number > 0 && mmu_seg->on_number_key_down(number); };
if (m_timer_set_color.IsRunning() && m_pending_color_shortcut_tens > 0) {
const int two_digit = m_pending_color_shortcut_tens * 10 + digit;
const int pending = m_pending_color_shortcut_tens;
m_pending_color_shortcut_tens = 0;
m_timer_set_color.Stop();
if (two_digit <= shortcut_max) {
processed = select(two_digit);
} else {
// Out of range: commit the pending digit, then treat this one as new input.
processed = select(pending);
if (can_start_two_digit(digit)) {
m_pending_color_shortcut_tens = digit;
m_timer_set_color.StartOnce(500);
processed = true;
} else {
processed = select(digit) || processed;
}
}
}
else if (can_start_two_digit(digit)) {
m_pending_color_shortcut_tens = digit;
m_timer_set_color.StartOnce(500);
processed = true;
}
else if (keyCode < '7' && m_timer_set_color.IsRunning()) {
processed = mmu_seg->on_number_key_down(keyCode - '0'+10);
m_timer_set_color.Stop();
}
else {
processed = mmu_seg->on_number_key_down(keyCode - '0');
processed = select(digit);
}
}
else if (keyCode == 'F' || keyCode == 'T' || keyCode == 'S' || keyCode == 'C' || keyCode == 'H' || keyCode == 'G') {
@@ -1054,11 +1078,15 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
void GLGizmosManager::on_set_color_timer(wxTimerEvent& evt)
{
if (m_current == MmSegmentation) {
// No second digit arrived in time: commit the pending leading digit on its own.
if (m_current == MmSegmentation && m_pending_color_shortcut_tens > 0) {
GLGizmoMmuSegmentation* mmu_seg = dynamic_cast<GLGizmoMmuSegmentation*>(get_current());
mmu_seg->on_number_key_down(1);
m_parent.set_as_dirty();
if (mmu_seg != nullptr) {
mmu_seg->on_number_key_down(m_pending_color_shortcut_tens);
m_parent.set_as_dirty();
}
}
m_pending_color_shortcut_tens = 0;
}
void GLGizmosManager::update_after_undo_redo(const UndoRedo::Snapshot& snapshot)
@@ -144,6 +144,8 @@ private:
//When there are more than 9 colors, shortcut key coloring
wxTimer m_timer_set_color;
// Leading digit of a two-digit color shortcut still waiting for its second digit.
int m_pending_color_shortcut_tens = 0;
void on_set_color_timer(wxTimerEvent& evt);
// key MENU_ICON_NAME, value = ImtextureID
+656
View File
@@ -0,0 +1,656 @@
#include "GradientCurveEditor.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
#include "I18N.hpp"
#include "Widgets/StateColor.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <wx/dcbuffer.h>
#include <wx/dcclient.h>
#include <wx/dcgraph.h>
#include <wx/settings.h>
namespace Slic3r {
namespace GUI {
wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
namespace {
// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing.
// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels.
constexpr double kPlotLeftRatio = 0.0316;
constexpr double kPlotRightRatio = 0.6766;
constexpr double kPlotTopRatio = 0.1529;
constexpr double kPlotBottomRatio = 0.8474;
constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders.
// Hit / stroke (DIP).
constexpr int kHitRadius = 6;
constexpr int kCurveHitRadius = 5;
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
constexpr int kStrokeUnselected = 2;
constexpr int kStrokeSelected = 4;
constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention)
constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP)
constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP)
// Light-mode design tokens. Resolved through StateColor::darkModeColorFor()
// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B ->
// #818183, #262E30 -> #EFEFF0, #ACACAC -> #65656A, *wxWHITE -> #2D2D31). Don't read these
// directly in paint; always go through the resolved locals declared at the top of on_paint().
const wxColour kGridColor (238, 238, 238); // #EEEEEE grey 300
const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700
const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700
const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900
const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements
// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve
// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than
// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets an outline.
constexpr float kBgSimilarThreshold = 15.0f;
constexpr int kOutlineExtraDip = 2;
} // namespace
GradientCurveEditor::GradientCurveEditor(wxWindow* parent,
const wxColour& color_low,
const wxColour& color_high)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
, m_color_low(color_low)
, m_color_high(color_high)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetBackgroundColour(wxGetApp().get_window_default_clr());
// Wide enough so the X-axis "Material Ratio" label fits past the arrow tip without overlap.
SetMinSize(FromDIP(wxSize(260, 200)));
reset_to_linear(0.10, 0.90);
Bind(wxEVT_PAINT, &GradientCurveEditor::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &GradientCurveEditor::on_left_down, this);
Bind(wxEVT_LEFT_UP, &GradientCurveEditor::on_left_up, this);
Bind(wxEVT_RIGHT_DOWN, &GradientCurveEditor::on_right_down, this);
Bind(wxEVT_MOTION, &GradientCurveEditor::on_motion, this);
Bind(wxEVT_LEAVE_WINDOW,&GradientCurveEditor::on_leave, this);
Bind(wxEVT_SIZE, &GradientCurveEditor::on_size, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) {
m_drag_mode = DragMode::None;
m_drag_idx = -1;
m_dragged_moved = false;
});
}
GradientCurveEditor::~GradientCurveEditor()
{
// See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it
// still holds the capture wedges mouse input for the whole application.
if (HasCapture())
ReleaseMouse();
}
void GradientCurveEditor::set_points(const PointList& pts)
{
m_points = pts;
normalize_points();
Refresh();
}
void GradientCurveEditor::set_colors(const wxColour& color_low, const wxColour& color_high)
{
m_color_low = color_low;
m_color_high = color_high;
Refresh();
}
void GradientCurveEditor::set_selected_curve(int curve_idx)
{
const int new_sel = (curve_idx == 0) ? 0 : 1;
if (m_selected_curve == new_sel) return;
m_selected_curve = new_sel;
Refresh();
}
void GradientCurveEditor::reset_to_linear(double y0, double y1)
{
auto clamp_y = [](double v) {
return std::max(kGradientMinRatio, std::min(kGradientMaxRatio, v));
};
m_points.clear();
GradientAnchor a0; a0.x = 0.0; a0.y = clamp_y(y0);
GradientAnchor a1; a1.x = 1.0; a1.y = clamp_y(y1);
m_points.push_back(a0);
m_points.push_back(a1);
m_selected_curve = 0;
Refresh();
emit_changed();
}
void GradientCurveEditor::reverse()
{
// Mirror y around 0.5. Tangents are slopes dy/dx so they flip sign to keep the
// local shape consistent across the mirror; NaN tangents remain "use PCHIP default".
for (auto& p : m_points) {
p.y = 1.0 - p.y;
if (std::isfinite(p.m_in)) p.m_in = -p.m_in;
if (std::isfinite(p.m_out)) p.m_out = -p.m_out;
}
Refresh();
emit_changed();
}
void GradientCurveEditor::normalize_points()
{
if (m_points.empty()) {
GradientAnchor a0; a0.x = 0.0; a0.y = kGradientMinRatio;
GradientAnchor a1; a1.x = 1.0; a1.y = kGradientMaxRatio;
m_points.push_back(a0);
m_points.push_back(a1);
return;
}
for (auto& p : m_points) {
p.x = std::max(0.0, std::min(1.0, p.x));
p.y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, p.y));
}
std::sort(m_points.begin(), m_points.end(),
[](const GradientAnchor& a, const GradientAnchor& b) {
return a.x < b.x;
});
if (m_points.size() < 2) {
GradientAnchor tail; tail.x = 1.0; tail.y = m_points.front().y;
m_points.push_back(tail);
}
m_points.front().x = 0.0;
m_points.back().x = 1.0;
}
void GradientCurveEditor::emit_changed()
{
wxCommandEvent evt(wxEVT_GRADIENT_CURVE_CHANGED, GetId());
evt.SetEventObject(this);
ProcessWindowEvent(evt);
}
wxRect GradientCurveEditor::plot_rect() const
{
const wxSize sz = GetClientSize();
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio));
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio));
// Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at
// the top-left so the "100%" labels on the bottom/right still align with the plot edges.
const int side = std::max(1, std::min(x2 - x, y2 - y));
return wxRect(x, y, side, side);
}
wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const
{
const wxRect r = plot_rect();
// y axis is inverted: y=1 should sit at the top.
return wxPoint2DDouble(r.x + x * r.width, r.y + (1.0 - y) * r.height);
}
wxPoint GradientCurveEditor::data_to_px(double x, double y) const
{
const wxPoint2DDouble p = data_to_px_f(x, y);
return wxPoint(static_cast<int>(std::lround(p.m_x)), static_cast<int>(std::lround(p.m_y)));
}
void GradientCurveEditor::px_to_data(int px, int py, double& x, double& y) const
{
const wxRect r = plot_rect();
const double w = std::max(1, r.width);
const double h = std::max(1, r.height);
x = std::max(0.0, std::min(1.0, (px - r.x) / w));
y = std::max(0.0, std::min(1.0, 1.0 - (py - r.y) / h));
}
double GradientCurveEditor::sample_curve_y(double x) const
{
GradientCurve gc;
gc.points = m_points;
return sample_gradient_curve(gc, x);
}
int GradientCurveEditor::hit_test(int px, int py) const
{
const int tol = FromDIP(kHitRadius);
int best_idx = -1;
int best_d2 = tol * tol;
for (size_t i = 0; i < m_points.size(); ++i) {
// Anchor visual y is curve-specific: component 1's anchor sits at (x, 1 - stored_y).
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
const wxPoint p = data_to_px(m_points[i].x, vy);
const int dx = px - p.x;
const int dy = py - p.y;
const int d2 = dx * dx + dy * dy;
if (d2 <= best_d2) {
best_idx = static_cast<int>(i);
best_d2 = d2;
}
}
return best_idx;
}
int GradientCurveEditor::hit_test_curve(int px, int py, int* seg_out) const
{
if (seg_out) *seg_out = -1;
if (m_points.size() < 2) return -1;
const int tol = FromDIP(kCurveHitRadius);
const int tol2 = tol * tol;
auto dist2_to_seg = [&](int ax, int ay, int bx, int by) -> int {
const double dx = bx - ax;
const double dy = by - ay;
const double l2 = dx * dx + dy * dy;
if (l2 == 0.0) {
const double ddx = px - ax;
const double ddy = py - ay;
return static_cast<int>(ddx * ddx + ddy * ddy);
}
double t = ((px - ax) * dx + (py - ay) * dy) / l2;
t = std::max(0.0, std::min(1.0, t));
const double ex = ax + t * dx;
const double ey = ay + t * dy;
const double ddx = px - ex;
const double ddy = py - ey;
return static_cast<int>(ddx * ddx + ddy * ddy);
};
// Hit-test against the same dense Hermite polyline that on_paint draws, so the
// clickable line follows the visual curve exactly (no offset on the bent parts).
// When a hit is found, also report the index of the left anchor of the data-space
// segment that covers cursor x; needed by the segment-bend interaction.
const wxRect rc = plot_rect();
const int samples = std::max(128, rc.width * 2);
auto seg_for_x = [&](double cursor_x) -> int {
for (size_t i = 1; i < m_points.size(); ++i) {
if (cursor_x <= m_points[i].x)
return static_cast<int>(i - 1);
}
return static_cast<int>(m_points.size() - 2);
};
auto curve_hit = [&](int curve_idx) -> bool {
wxPoint prev;
for (int s = 0; s <= samples; ++s) {
const double x = double(s) / samples;
const double y0 = sample_curve_y(x);
const double vy = to_visual_y(curve_idx, y0);
const wxPoint cur = data_to_px(x, vy);
if (s > 0 && dist2_to_seg(prev.x, prev.y, cur.x, cur.y) <= tol2)
return true;
prev = cur;
}
return false;
};
// Prefer the selected curve so overlapping segments don't unintentionally steal focus.
if (curve_hit(m_selected_curve)) {
if (seg_out) {
double nx = 0, dummy = 0;
px_to_data(px, py, nx, dummy);
*seg_out = seg_for_x(nx);
}
return m_selected_curve;
}
const int other = 1 - m_selected_curve;
if (curve_hit(other)) {
if (seg_out) {
double nx = 0, dummy = 0;
px_to_data(px, py, nx, dummy);
*seg_out = seg_for_x(nx);
}
return other;
}
return -1;
}
void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/)
{
// Resolve theme colors every paint so dark-mode toggles (no re-construction) take
// effect without an explicit listener. Window bg is read from GUI_App, not
// GetBackgroundColour(), since the latter is snapshotted at construction time.
const wxColour bg = wxGetApp().get_window_default_clr();
const wxColour grid_color = StateColor::darkModeColorFor(kGridColor);
const wxColour axis_color = StateColor::darkModeColorFor(kAxisColor);
const wxColour label_muted = StateColor::darkModeColorFor(kLabelMuted);
const wxColour label_strong = StateColor::darkModeColorFor(kLabelStrong);
const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE);
// Softer than axis_color: the curve outline only has to lift the curve off the
// background, it must not compete with the structural axis / grid.
const wxColour outline_color = StateColor::darkModeColorFor(kOutlineColor);
wxAutoBufferedPaintDC raw_dc(this);
raw_dc.SetBackground(wxBrush(bg));
raw_dc.Clear();
// Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered
// DC is the actual back buffer that gets blitted to the window.
wxGCDC dc(raw_dc);
// The curve and its anchors are drawn straight on the graphics context so their
// coordinates stay sub-pixel accurate (see data_to_px_f).
wxGraphicsContext* gc = dc.GetGraphicsContext();
const wxRect rc = plot_rect();
if (rc.width <= 0 || rc.height <= 0)
return;
// 10x10 light grid (10 lines including outer borders, 9 equal divisions).
dc.SetPen(wxPen(grid_color, 1));
for (int i = 0; i <= kGridDivisions; ++i) {
const int x = rc.x + rc.width * i / kGridDivisions;
const int y = rc.y + rc.height * i / kGridDivisions;
dc.DrawLine(x, rc.y, x, rc.y + rc.height);
dc.DrawLine(rc.x, y, rc.x + rc.width, y);
}
// Set the label font first so text width measurements drive arrow / label placement.
wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1));
dc.SetFont(label_font);
const wxString axis_y_title = _L("Material Ratio");
const wxString axis_x_title = _L("Model Height");
const wxString pct_text = wxT("100%");
const wxSize x_title_sz = dc.GetTextExtent(axis_x_title);
const wxSize y_title_sz = dc.GetTextExtent(axis_y_title);
wxFont strong_font = label_font;
strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD);
dc.SetFont(strong_font);
const wxSize pct_text_sz = dc.GetTextExtent(pct_text);
dc.SetFont(label_font);
// Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the
// canvas top edge; X-axis extends past the plot right toward the canvas right edge.
const int arrow_half = FromDIP(kAxisArrowHalf);
const int arrow_len = FromDIP(kAxisArrowLen);
const wxSize sz = GetClientSize();
dc.SetPen(wxPen(axis_color, kStrokeAxis));
dc.SetBrush(wxBrush(axis_color));
// Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom.
const int y_axis_x = rc.x;
const int y_title_pct_gap = FromDIP(1);
const int y_title_bottom_pad = FromDIP(2);
const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad);
const int y_arrow_tip_y = y_title_y;
const int y_arrow_ty = y_arrow_tip_y + arrow_len;
dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height);
{
wxPoint tri[3] = {
wxPoint(y_axis_x, y_arrow_tip_y),
wxPoint(y_axis_x - arrow_half, y_arrow_ty),
wxPoint(y_axis_x + arrow_half, y_arrow_ty),
};
dc.DrawPolygon(3, tri);
}
// X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing
// "Material Ratio" label still fits inside the canvas without overlapping the arrow.
const int x_axis_y = rc.y + rc.height;
const int x_label_gap = FromDIP(4);
const int x_edge_pad = FromDIP(6);
const int x_arrow_ideal = rc.x + rc.width + FromDIP(10);
const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len;
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len,
std::min(x_arrow_ideal, x_arrow_max));
const int x_arrow_tip_x = x_arrow_tx + arrow_len;
const int x_title_x = x_arrow_tip_x + x_label_gap;
dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y);
{
wxPoint tri[3] = {
wxPoint(x_arrow_tip_x, x_axis_y),
wxPoint(x_arrow_tx, x_axis_y - arrow_half),
wxPoint(x_arrow_tx, x_axis_y + arrow_half),
};
dc.DrawPolygon(3, tri);
}
// Labels.
// "Model Height" and "100%" share the same left x; the gap is larger than the
// axis-arrow half-base so the text never visually touches the Y-axis arrow.
const int label_left_x = y_axis_x + FromDIP(10);
dc.SetTextForeground(label_muted);
dc.DrawText(axis_y_title, label_left_x, y_title_y);
dc.SetFont(strong_font);
dc.SetTextForeground(label_strong);
dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap);
// Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the
// X-axis arrow tip (placement was already clamped above to leave room).
dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y);
dc.SetFont(label_font);
dc.SetTextForeground(label_muted);
dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2);
if (m_points.size() < 2 || !gc)
return;
auto color_for_curve = [&](int curve_idx) -> wxColour {
wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
// Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
// Lift alpha so the curve stays visible while still hinting at transparency.
if (c.Alpha() == 0)
c.Set(c.Red(), c.Green(), c.Blue(), 150);
return c;
};
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> {
const int samples = std::max(128, rc.width * 2);
std::vector<wxPoint2DDouble> poly;
poly.reserve(samples + 1);
for (int s = 0; s <= samples; ++s) {
const double x = double(s) / samples;
const double y0 = sample_curve_y(x);
const double vy = to_visual_y(curve_idx, y0);
poly.push_back(data_to_px_f(x, vy));
}
return poly;
};
// Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint
// and would quantize the curve back to whole pixels. The pen is still set on the dc, which
// forwards it here while keeping its own cached state in sync for later dc drawing.
auto draw_polyline = [&](const std::vector<wxPoint2DDouble>& poly, const wxColour& col, int stroke_dip) {
dc.SetPen(wxPen(col, FromDIP(stroke_dip)));
gc->StrokeLines(poly.size(), poly.data());
};
// Outline only when the curve color is perceptually close to the background; otherwise
// the plain filament color reads fine and the extra stroke would look heavy.
auto needs_outline = [&](const wxColour& c) {
return calc_color_distance(c, bg) < kBgSimilarThreshold;
};
auto draw_one = [&](int curve_idx, int stroke_dip) {
const auto poly = build_polyline(curve_idx);
const wxColour col = color_for_curve(curve_idx);
if (needs_outline(col))
draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip);
draw_polyline(poly, col, stroke_dip);
};
// Draw unselected first so the selected curve sits on top.
const int other = 1 - m_selected_curve;
draw_one(other, kStrokeUnselected);
draw_one(m_selected_curve, kStrokeSelected);
// Control points (selected curve only): hollow circle with axis-color border, theme-aware fill.
// Drawn on the graphics context with a sub-pixel center so the ring stays centered on the
// curve instead of drifting up to half a pixel off it; pen and brush go through the dc for
// the same reason as in draw_polyline above.
const double r = FromDIP(kPointRadius);
dc.SetPen(wxPen(axis_color, 1));
dc.SetBrush(wxBrush(point_fill));
for (size_t i = 0; i < m_points.size(); ++i) {
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy);
gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2);
}
}
void GradientCurveEditor::on_left_down(wxMouseEvent& evt)
{
const wxPoint pos = evt.GetPosition();
m_dragged_moved = false;
// 1) Anchor on the selected curve takes precedence over everything else.
// Dragging an anchor resets its tangent overrides so the surrounding curve
// returns to PCHIP-default shape (matches user expectation that pulling an
// anchor "straightens out" the local mess).
const int idx = hit_test(pos.x, pos.y);
if (idx >= 0) {
m_drag_mode = DragMode::Anchor;
m_drag_idx = idx;
// Only emit a change event when clearing the tangents actually mutates
// the curve. A plain click on an already-default anchor must not trigger
// re-slicing through the changed-event listener.
const bool had_tangent = std::isfinite(m_points[idx].m_in)
|| std::isfinite(m_points[idx].m_out);
m_points[idx].m_in = std::numeric_limits<double>::quiet_NaN();
m_points[idx].m_out = std::numeric_limits<double>::quiet_NaN();
if (!HasCapture())
CaptureMouse();
Refresh();
if (had_tangent)
emit_changed();
return;
}
// 2) Line-body hit. Determine which curve and which segment.
int seg = -1;
const int curve_hit = hit_test_curve(pos.x, pos.y, &seg);
if (curve_hit < 0) {
m_drag_mode = DragMode::None;
evt.Skip();
return;
}
// 3) Non-selected curve hit -> switch selection only, no drag arming.
if (curve_hit != m_selected_curve) {
m_selected_curve = curve_hit;
m_drag_mode = DragMode::None;
Refresh();
evt.Skip();
return;
}
// 4) Selected curve line body hit -> insert a new anchor at cursor x (snapped
// to the current smooth curve so the initial click is visually invisible)
// and immediately enter Anchor drag mode. Bending the segment without
// inserting an anchor is not an option: a single cubic between two existing
// anchors cannot put its peak under an off-center cursor.
double nx = 0, dummy = 0;
px_to_data(pos.x, pos.y, nx, dummy);
if (nx <= 0.0 || nx >= 1.0 || seg < 0) {
m_drag_mode = DragMode::None;
evt.Skip();
return;
}
GradientAnchor a;
a.x = nx;
a.y = sample_curve_y(nx);
const size_t insert_idx = static_cast<size_t>(seg) + 1;
m_points.insert(m_points.begin() + insert_idx, a);
m_drag_mode = DragMode::Anchor;
m_drag_idx = static_cast<int>(insert_idx);
if (!HasCapture())
CaptureMouse();
Refresh();
emit_changed();
}
void GradientCurveEditor::on_left_up(wxMouseEvent& evt)
{
if (HasCapture())
ReleaseMouse();
// Anchor mode (either an existing anchor or one freshly inserted by on_left_down)
// already fired emit_changed on mouse_down; only fire again here if the user
// actually dragged so the slicer doesn't re-run on a pure click.
if (m_drag_mode == DragMode::Anchor && m_dragged_moved)
emit_changed();
m_drag_mode = DragMode::None;
m_drag_idx = -1;
m_dragged_moved = false;
(void)evt;
}
void GradientCurveEditor::on_right_down(wxMouseEvent& evt)
{
const wxPoint pos = evt.GetPosition();
const int idx = hit_test(pos.x, pos.y);
if (idx > 0 && static_cast<size_t>(idx) + 1 < m_points.size()) {
// Interior anchor on the selected curve -> delete it. Endpoints stay locked.
m_points.erase(m_points.begin() + idx);
Refresh();
emit_changed();
return;
}
// Right-click on the non-selected curve switches selection (never deletes).
const int curve_hit = hit_test_curve(pos.x, pos.y);
if (curve_hit >= 0 && curve_hit != m_selected_curve) {
m_selected_curve = curve_hit;
Refresh();
return;
}
evt.Skip();
}
void GradientCurveEditor::on_motion(wxMouseEvent& evt)
{
if (!evt.LeftIsDown() || m_drag_mode != DragMode::Anchor) {
evt.Skip();
return;
}
if (static_cast<size_t>(m_drag_idx) >= m_points.size())
return;
const wxPoint pos = evt.GetPosition();
double nx = 0, vy = 0;
px_to_data(pos.x, pos.y, nx, vy);
auto& p = m_points[m_drag_idx];
const bool is_first = (m_drag_idx == 0);
const bool is_last = (static_cast<size_t>(m_drag_idx) + 1 == m_points.size());
// Endpoints stay locked at x=0 / x=1; interior anchors clamp into
// (left_neighbor.x, right_neighbor.x) so they can't cross or coincide.
if (!is_first && !is_last) {
const double xl = m_points[m_drag_idx - 1].x;
const double xr = m_points[m_drag_idx + 1].x;
const double eps = 1e-4;
nx = std::max(xl + eps, std::min(xr - eps, nx));
p.x = nx;
}
// y is constrained to the reserved blend band so neither component ever
// reaches 0% / 100%, matching the sampler's clamp.
p.y = std::max(kGradientMinRatio,
std::min(kGradientMaxRatio, to_stored_y(m_selected_curve, vy)));
m_dragged_moved = true;
Refresh();
}
void GradientCurveEditor::on_leave(wxMouseEvent& evt)
{
evt.Skip();
}
void GradientCurveEditor::on_size(wxSizeEvent& evt)
{
Refresh();
evt.Skip();
}
} // namespace GUI
} // namespace Slic3r
+122
View File
@@ -0,0 +1,122 @@
#ifndef slic3r_GradientCurveEditor_hpp_
#define slic3r_GradientCurveEditor_hpp_
#include <vector>
#include <wx/colour.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
#include <wx/geometry.h>
#include <wx/panel.h>
#include "libslic3r/FilamentMixer.hpp"
namespace Slic3r {
namespace GUI {
// Photoshop-style curve editor for "Z progress -> first-component ratio" mapping.
// Curve evaluation uses cubic Hermite with PCHIP defaults plus optional per-anchor
// tangent overrides (m_in / m_out, NaN = use PCHIP default). The same evaluator
// (FilamentMixer::sample_gradient_curve) is shared with the slicing backend so what
// the editor renders matches the G-code output 1:1.
//
// Interaction model (PS Curves style):
// - Click or press-and-drag on the line body inserts a new anchor at the cursor x
// (snapped to the current smooth curve, NaN tangents) and starts dragging it.
// A pure click leaves an anchor sitting exactly on the previous curve shape; a
// drag moves the new anchor freely so the bump follows the cursor 1:1.
// - Dragging an existing anchor moves (x, y) and clears its m_in / m_out so the
// local curve returns to the PCHIP default shape around it.
// - Right-click on an interior anchor deletes it; endpoints stay locked.
class GradientCurveEditor : public wxPanel
{
public:
using PointList = std::vector<GradientAnchor>;
GradientCurveEditor(wxWindow* parent,
const wxColour& color_low = wxColour(217, 217, 217),
const wxColour& color_high = wxColour(217, 217, 217));
~GradientCurveEditor() override;
// Replace the entire point list. The widget enforces x in [0,1], y in [0,1],
// sorts by x, and clamps the first / last x to 0 / 1. Tangent overrides are
// preserved as-is (NaN entries continue to use PCHIP defaults).
void set_points(const PointList& pts);
const PointList& get_points() const { return m_points; }
void set_colors(const wxColour& color_low, const wxColour& color_high);
// Which curve currently responds to drag / add / delete and is drawn with the thick stroke.
// 0 = first component (color_low), 1 = second component (color_high). Storage layer is
// unaffected: m_points always represents component 0's ratio.
void set_selected_curve(int curve_idx);
int get_selected_curve() const { return m_selected_curve; }
// Reset to a two-point linear curve from y0 at t=0 to y1 at t=1.
// Clears all tangent overrides.
void reset_to_linear(double y0, double y1);
// Flip the curve top to bottom (all y -> 1 - y; tangents negated to mirror shape).
void reverse();
private:
enum class DragMode {
None, // nothing armed
Anchor, // dragging an anchor (either existing or just inserted from a line hit)
};
void normalize_points();
void emit_changed();
void on_paint(wxPaintEvent& evt);
void on_left_down(wxMouseEvent& evt);
void on_left_up(wxMouseEvent& evt);
void on_right_down(wxMouseEvent& evt);
void on_motion(wxMouseEvent& evt);
void on_leave(wxMouseEvent& evt);
void on_size(wxSizeEvent& evt);
// Coordinate mapping between data (x, y in [0,1]) and pixels in plot area.
wxRect plot_rect() const;
// Sub-pixel accurate mapping, used for drawing: rounding the curve vertices to whole
// pixels leaves a staircase that anti-aliasing cannot smooth out, and the step is
// twice as coarse on 2x (Retina) displays.
wxPoint2DDouble data_to_px_f(double x, double y) const;
wxPoint data_to_px(double x, double y) const;
void px_to_data(int px, int py, double& x, double& y) const;
// Anchor hit test for the currently-selected curve (uses translated visual y).
int hit_test(int px, int py) const; // returns point index or -1
// Line-body hit test across both curves. Returns 0/1 for which curve was hit, -1 if none.
// Prefers the selected curve when both are within threshold. seg_out (when non-null)
// receives the left-anchor index of the segment that was hit on the returned curve;
// on_left_down uses it to know where in m_points to insert a freshly-added anchor.
int hit_test_curve(int px, int py, int* seg_out = nullptr) const;
// Sample the curve in stored space (component 0) at x.
double sample_curve_y(double x) const;
// Symmetric translation between visual y (what the user sees / clicks) and stored y
// (component 0's ratio in m_points).
static double to_stored_y(int curve_idx, double visual_y) {
return (curve_idx == 0) ? visual_y : (1.0 - visual_y);
}
static double to_visual_y(int curve_idx, double stored_y) {
return (curve_idx == 0) ? stored_y : (1.0 - stored_y);
}
PointList m_points;
wxColour m_color_low;
wxColour m_color_high;
int m_selected_curve = 0;
DragMode m_drag_mode = DragMode::None;
int m_drag_idx = -1; // valid when m_drag_mode == Anchor
bool m_dragged_moved = false;
};
// Custom event raised when the curve is edited (drag / add / remove / reset / reverse).
wxDECLARE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GradientCurveEditor_hpp_
+56 -12
View File
@@ -2404,6 +2404,25 @@ void ImGuiWrapper::draw(
}
}
void ImGuiWrapper::draw_gradient_ramp(ImDrawList *draw_list, const ImVec2 &top_left, const ImVec2 &bottom_right, const std::vector<wxColour> &ramp)
{
if (draw_list == nullptr || ramp.empty() || bottom_right.x <= top_left.x || bottom_right.y <= top_left.y)
return;
const int rows = std::max(1, (int) std::lround(bottom_right.y - top_left.y));
const float row_h = (bottom_right.y - top_left.y) / rows;
const size_t last = ramp.size() - 1;
for (int r = 0; r < rows; ++r) {
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
const double t = (rows > 1) ? (double) (rows - 1 - r) / (rows - 1) : 0.5;
const wxColour &c = ramp[(size_t) (t * last + 0.5)];
// The bottom row snaps to the rect's edge so rounding never leaves a sliver uncovered.
const float y0 = top_left.y + r * row_h;
const float y1 = (r + 1 == rows) ? bottom_right.y : top_left.y + (r + 1) * row_h;
draw_list->AddRectFilled({top_left.x, y0}, {bottom_right.x, y1}, IM_COL32(c.Red(), c.Green(), c.Blue(), c.Alpha()));
}
}
void ImGuiWrapper::draw_cross_hair(const ImVec2 &position, float radius, ImU32 color, int num_segments, float thickness) {
auto draw_list = ImGui::GetOverlayDrawList();
draw_list->AddCircle(position, radius, color, num_segments, thickness);
@@ -2463,6 +2482,19 @@ static const ImWchar ranges_keyboard_shortcuts[] =
};
#endif // __APPLE__
// Names drawn through the atlas come from file names and CAD data, not from the UI language.
// GetGlyphRangesDefault() already gives every language the CJK ideographs, which is why a
// Chinese file name renders under an English UI; these are the alphabetic scripts it omits.
// Codepoints the font lacks are skipped at build time, so only existing glyphs cost anything.
static const ImWchar ranges_language_independent[] =
{
0x0100, 0x024F, // Latin Extended-A and Extended-B
0x0370, 0x03FF, // Greek and Coptic
0x0400, 0x04FF, // Cyrillic
0x1E00, 0x1EFF, // Latin Extended Additional (Vietnamese)
0,
};
std::vector<unsigned char> ImGuiWrapper::load_svg(const std::string& bitmap_name, unsigned target_width, unsigned target_height, unsigned *outwidth, unsigned *outheight)
{
@@ -2773,6 +2805,7 @@ void ImGuiWrapper::init_font(bool compress)
ImFontAtlas::GlyphRangesBuilder builder;
builder.AddRanges(m_glyph_ranges);
builder.AddRanges(ImGui::GetIO().Fonts->GetGlyphRangesDefault());
builder.AddRanges(ranges_language_independent);
#ifdef __APPLE__
if (m_font_cjk)
// Apple keyboard shortcuts are only contained in the CJK fonts.
@@ -2794,12 +2827,17 @@ void ImGuiWrapper::init_font(bool compress)
// Orca: temp fix for Korean font
auto font_name_regular = "HarmonyOS_Sans_SC_Regular.ttf";
auto font_name_bold = "HarmonyOS_Sans_SC_Bold.ttf";
// The Korean and Thai fonts cover their own script and little else, so they need the
// default font merged in behind them to reach the full range.
bool needs_glyph_fallback = false;
if(m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesKorean()) {
font_name_regular = "NanumGothic-Regular.ttf";
font_name_bold = "NanumGothic-Bold.ttf";
needs_glyph_fallback = true;
} else if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
font_name_regular = "Sarabun-Medium.ttf";
font_name_bold = "Sarabun-SemiBold.ttf";
needs_glyph_fallback = true;
}
default_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_regular).c_str(), m_font_size, &cfg, ranges.Data);
if (default_font == nullptr) {
@@ -2809,11 +2847,12 @@ void ImGuiWrapper::init_font(bool compress)
}
}
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
// A merged font only supplies glyphs the font ahead of it lacks, so this fills the gaps
// without restyling anything the script font already covers.
if (needs_glyph_fallback) {
ImFontConfig fallback_cfg = cfg;
fallback_cfg.MergeMode = true;
static constexpr ImWchar celsius_range[] = { 0x2103, 0x2103, 0 };
io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf").c_str(), m_font_size, &fallback_cfg, celsius_range);
io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf").c_str(), m_font_size, &fallback_cfg, ranges.Data);
}
bold_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_bold).c_str(), m_font_size, &cfg, ranges.Data);
@@ -2822,11 +2861,10 @@ void ImGuiWrapper::init_font(bool compress)
if (bold_font == nullptr) { throw Slic3r::RuntimeError("ImGui: Could not load deafult font"); }
}
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
if (needs_glyph_fallback) {
ImFontConfig fallback_cfg = cfg;
fallback_cfg.MergeMode = true;
static constexpr ImWchar celsius_range[] = { 0x2103, 0x2103, 0 };
io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Bold.ttf").c_str(), m_font_size, &fallback_cfg, celsius_range);
io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Bold.ttf").c_str(), m_font_size, &fallback_cfg, ranges.Data);
}
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
@@ -2878,13 +2916,18 @@ void ImGuiWrapper::init_font(bool compress)
glsafe(::glGetIntegerv(GL_MAX_TEXTURE_SIZE, &gl_max_tex_size));
constexpr int max_retries = 6;
for (int attempt = 0; attempt < max_retries && io.Fonts->TexHeight > gl_max_tex_size; ++attempt) {
io.Fonts->TexDesiredWidth = (io.Fonts->TexDesiredWidth > 0 ? io.Fonts->TexDesiredWidth : io.Fonts->TexWidth) * 2;
const int width = io.Fonts->TexDesiredWidth > 0 ? io.Fonts->TexDesiredWidth : io.Fonts->TexWidth;
// Both dimensions share the same limit, so widening past it would only trade an
// illegal height for an illegal width.
if (width * 2 > gl_max_tex_size)
break;
io.Fonts->TexDesiredWidth = width * 2;
io.Fonts->Build();
}
if (io.Fonts->TexHeight > gl_max_tex_size) {
// Shouldn't really happen
BOOST_LOG_TRIVIAL(error) << "Font atlas height " << io.Fonts->TexHeight
<< " still exceeds GL_MAX_TEXTURE_SIZE (" << gl_max_tex_size << ")"
// Needs both a very large glyph set and a small GL_MAX_TEXTURE_SIZE.
BOOST_LOG_TRIVIAL(error) << "Font atlas " << io.Fonts->TexWidth << "x" << io.Fonts->TexHeight
<< " does not fit GL_MAX_TEXTURE_SIZE (" << gl_max_tex_size << ")"
<< " after " << max_retries << " attempts; rendering may be incomplete";
}
@@ -3332,8 +3375,9 @@ const char* ImGuiWrapper::clipboard_get(void* user_data)
wxTextDataObject data;
wxTheClipboard->GetData(data);
if (data.GetTextLength() > 0) {
self->m_clipboard_text = into_u8(data.GetText());
const wxString text = data.GetText();
if (text.Length() > 0) {
self->m_clipboard_text = into_u8(text);
res = self->m_clipboard_text.c_str();
}
}
+16
View File
@@ -3,10 +3,12 @@
#include <string>
#include <map>
#include <vector>
#include <cstdlib>
#include <imgui/imgui.h>
#include <wx/colour.h>
#include <wx/string.h>
#include "libslic3r/Point.hpp"
@@ -299,6 +301,20 @@ public:
int num_segments = 0,
float thickness = 4.f);
/// <summary>
/// Fill a rect with a filament gradient ramp, one band per pixel row, ramp.front() along
/// the bottom edge. Bands rather than one interpolated rect, because the ramp follows the
/// slot's gradient curve and ImGui's corner interpolation could only draw a straight fade.
/// </summary>
/// <param name="draw_list">Define where to draw it</param>
/// <param name="top_left">Upper left corner of the rect</param>
/// <param name="bottom_right">Lower right corner of the rect</param>
/// <param name="ramp">Colours printed, bottom of the model first</param>
static void draw_gradient_ramp(ImDrawList * draw_list,
const ImVec2 & top_left,
const ImVec2 & bottom_right,
const std::vector<wxColour> &ramp);
/// <summary>
/// Check that font ranges contain all chars in string
/// (rendered Unicodes are stored in GlyphRanges)
+1 -1
View File
@@ -74,7 +74,7 @@ bool ImageDPIFrame::Show(bool show)
}
void ImageDPIFrame::set_bitmap(const wxBitmap &bit_map) {
if (&bit_map && bit_map.IsOk()) {
if (bit_map.IsOk()) {
m_bitmap->SetBitmap(bit_map);
}
}
+5
View File
@@ -2330,6 +2330,11 @@ bool MainFrame::get_enable_slice_status()
}
}
// A mixed filament whose components were deleted, or whose components disagree in type,
// cannot be resolved at slicing time. Block the slice until the user fixes it.
if (enable && m_plater->sidebar().has_broken_mixed_filament())
enable = false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable;
return enable;
}
+22 -37
View File
@@ -39,13 +39,14 @@ static std::map<int, std::string> error_messages = {
namespace Slic3r {
namespace GUI {
MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl2 *media_ctrl, const wxPoint &pos, const wxSize &size)
MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos, const wxSize &size)
: wxPanel(parent, wxID_ANY, pos, size)
, m_media_ctrl(media_ctrl)
{
SetLabel("MediaPlayCtrl");
SetBackgroundColour(*wxWHITE);
m_media_ctrl->Bind(wxEVT_MEDIA_STATECHANGED, &MediaPlayCtrl::onStateChanged, this);
m_media_ctrl->SetIdleImage(from_u8(resources_dir() + "/images/live_stream_default.png"));
m_button_play = new Button(this, "", "media_play", wxBORDER_NONE);
m_button_play->SetCanFocus(false);
@@ -177,13 +178,6 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
if (machine == m_machine) {
if (m_last_state == MEDIASTATE_IDLE && IsEnabled())
Play();
else if (m_last_state == MEDIASTATE_LOADING && m_tutk_state == "disable"
&& m_last_user_play + wxTimeSpan::Seconds(3) < wxDateTime::Now()) {
// resend ttcode to printer
if (auto agent = wxGetApp().getAgent())
agent->get_camera_url(machine, [](auto) {}, wxGetApp().get_printer_cloud_provider());
m_last_user_play = wxDateTime::Now();
}
return;
}
m_machine = machine;
@@ -313,7 +307,7 @@ void MediaPlayCtrl::Play()
// !m_lan_mode && !m_remote_proto && m_lan_proto == LVL_None (x)
if (m_lan_proto <= MachineObject::LVL_Disable && (m_lan_mode || !m_remote_proto)) {
Stop(m_lan_proto == MachineObject::LVL_None
Stop(m_lan_proto == MachineObject::LVL_None
? _L("A problem occurred. Please update the printer firmware and try again.")
: _L("LAN Only Liveview is off. Please turn on the liveview on printer screen."));
return;
@@ -351,7 +345,7 @@ void MediaPlayCtrl::Play()
url += "&cli_id=" + wxGetApp().app_config->get("slicer_uuid");
url += "&cli_ver=" + std::string(SLIC3R_VERSION);
}
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: " << hide_passwd(url,
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: " << hide_passwd(url,
{"?uid=", "authkey=", "passwd=", "license=", "token="});
CallAfter([this, m, url] {
if (m != m_machine) {
@@ -426,7 +420,7 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2)
auto tunnel = m_url.empty() ? "" : into_u8(wxURI(m_url).GetPath()).substr(1);
if (auto n = tunnel.find_first_of("/_"); n != std::string::npos)
tunnel = tunnel.substr(0, n);
if (last_state != wxMEDIASTATE_PLAYING && m_failed_code != 0
if (last_state != wxMEDIASTATE_PLAYING && m_failed_code != 0
&& m_last_failed_codes.find(m_failed_code) == m_last_failed_codes.end()
&& (m_user_triggered || m_failed_retry > 3)) {
m_last_failed_codes.insert(m_failed_code);
@@ -560,7 +554,7 @@ void MediaPlayCtrl::ToggleStream()
url += "&cli_id=" + wxGetApp().app_config->get("slicer_uuid");
url += "&cli_ver=" + std::string(SLIC3R_VERSION);
}
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::ToggleStream: " << hide_passwd(url,
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::ToggleStream: " << hide_passwd(url,
{"?uid=", "authkey=", "passwd=", "license=", "token="});
CallAfter([this, m, url] {
if (m != m_machine) return;
@@ -580,8 +574,8 @@ void MediaPlayCtrl::ToggleStream()
}, wxGetApp().get_printer_cloud_provider());
}
void MediaPlayCtrl::msw_rescale() {
m_button_play->Rescale();
void MediaPlayCtrl::msw_rescale() {
m_button_play->Rescale();
}
void MediaPlayCtrl::jump_to_play()
@@ -715,7 +709,9 @@ void MediaPlayCtrl::media_proc()
break;
}
else if (url == "<play>") {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start play";
m_media_ctrl->Play();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end play";
}
else {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start load";
@@ -771,15 +767,15 @@ bool MediaPlayCtrl::start_stream_service(bool *need_install)
if (!boost::filesystem::exists(file_dll) || boost::filesystem::last_write_time(file_dll) != boost::filesystem::last_write_time(file_dll2))
boost::filesystem::copy_file(file_dll2, file_dll, boost::filesystem::copy_options::overwrite_existing);
}
boost::process::child process_source(file_source, file_url2.ToStdWstring(), boost::process::start_dir(tools_dir),
boost::process::windows::create_no_window,
boost::process::child process_source(file_source, file_url2.ToStdWstring(), boost::process::start_dir(tools_dir),
boost::process::windows::create_no_window,
boost::process::std_out > intermediate, boost::process::limit_handles);
boost::process::child process_ffmpeg(file_ffmpeg, configss, boost::process::windows::create_no_window,
boost::process::child process_ffmpeg(file_ffmpeg, configss, boost::process::windows::create_no_window,
boost::process::std_in < intermediate, boost::process::limit_handles);
#else
boost::filesystem::permissions(file_source, boost::filesystem::owner_exe | boost::filesystem::add_perms);
boost::filesystem::permissions(file_ffmpeg, boost::filesystem::owner_exe | boost::filesystem::add_perms);
boost::process::child process_source(file_source, file_url2.data().AsInternal(), boost::process::start_dir(start_dir),
boost::process::child process_source(file_source, file_url2.data().AsInternal(), boost::process::start_dir(start_dir),
boost::process::std_out > intermediate, boost::process::limit_handles);
boost::process::child process_ffmpeg(file_ffmpeg, configss, boost::process::std_in < intermediate, boost::process::limit_handles);
#endif
@@ -830,27 +826,16 @@ bool MediaPlayCtrl::get_stream_url(std::string *url)
}}
void wxMediaCtrl2::DoSetSize(int x, int y, int width, int height, int sizeFlags)
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height)
{
#ifdef __WXMAC__
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
#else
wxMediaCtrl::DoSetSize(x, y, width, height, sizeFlags);
#endif
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_gtk_video_window) {
const wxSize client_size = GetClientSize();
m_gtk_video_window->SetSize(0, 0, client_size.GetWidth(), client_size.GetHeight());
}
#endif
if (sizeFlags & wxSIZE_USE_EXISTING) return;
wxSize size = m_video_size;
wxSize size = videoSize;
if (!size.IsFullySpecified()) size = {16, 9};
int maxHeight = (width * size.GetHeight() + size.GetHeight() - 1) / size.GetWidth();
if (maxHeight != GetMaxHeight()) {
// BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2::DoSetSize: width: " << width << ", height: " << height << ", maxHeight: " << maxHeight;
SetMaxSize({-1, maxHeight});
CallAfter([this] {
if (auto p = GetParent()) {
if (maxHeight != ctrl->GetMaxHeight()) {
// BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl_OnSize: width: " << width << ", height: " << height << ", maxHeight: " << maxHeight;
ctrl->SetMaxSize({-1, maxHeight});
ctrl->CallAfter([ctrl] {
if (auto p = ctrl->GetParent()) {
p->Layout();
p->Refresh();
}
+4 -4
View File
@@ -8,7 +8,7 @@
#ifndef MediaPlayCtrl_h
#define MediaPlayCtrl_h
#include "wxMediaCtrl2.h"
#include "wxMediaCtrl3.h"
#include <wx/panel.h>
@@ -30,7 +30,7 @@ namespace GUI {
class MediaPlayCtrl : public wxPanel
{
public:
MediaPlayCtrl(wxWindow *parent, wxMediaCtrl2 *media_ctrl, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
~MediaPlayCtrl();
@@ -75,7 +75,7 @@ private:
// token
std::shared_ptr<int> m_token = std::make_shared<int>(0);
wxMediaCtrl2 * m_media_ctrl;
wxMediaCtrl3 * m_media_ctrl;
wxMediaState m_last_state = MEDIASTATE_IDLE;
std::string m_machine;
int m_lan_proto = 0;
@@ -90,7 +90,7 @@ private:
bool m_device_busy = false;
bool m_disable_lan = false;
wxString m_url;
std::deque<wxString> m_tasks;
boost::mutex m_mutex;
boost::condition_variable m_cond;
File diff suppressed because it is too large Load Diff
+183
View File
@@ -0,0 +1,183 @@
#ifndef slic3r_MixedFilamentDialog_hpp_
#define slic3r_MixedFilamentDialog_hpp_
#include <array>
#include <string>
#include <utility>
#include <vector>
#include <wx/bitmap.h>
#include <wx/panel.h>
#include <wx/tglbtn.h>
#include <wx/stattext.h>
#include "GUI_Utils.hpp"
#include "libslic3r/FilamentMixer.hpp"
class Button;
class CheckBox;
class ComboBox;
class wxMouseEvent;
class wxScrolledWindow;
class wxTextCtrl;
class wxWrapSizer;
namespace Slic3r {
namespace GUI {
class GradientCurveEditor;
class RatioLabelPanel;
struct MixedFilamentResult {
std::vector<unsigned int> components; // 1-based physical filament indices
std::vector<int> ratios; // percentages, sum = 100
bool gradient_enabled = false;
int gradient_direction = 0; // 0 = A→B, 1 = B→A (only for 2-color)
bool per_part_gradient = false; // valid only when gradient_enabled == true
// Optional Photoshop-style custom curve overriding the linear A→B gradient.
// Empty -> use linear (gradient_direction). Non-empty -> cubic Hermite over [0,1]^2
// with optional per-anchor tangent overrides (see GradientAnchor).
std::vector<GradientAnchor> gradient_curve;
};
class MixedFilamentDialog : public DPIDialog
{
public:
MixedFilamentDialog(wxWindow* parent,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_names,
const std::vector<std::string>& physical_types = {});
MixedFilamentDialog(wxWindow* parent,
const MixedFilamentResult& existing,
const std::vector<std::string>& physical_colors,
const std::vector<std::string>& physical_names,
const std::vector<std::string>& physical_types = {});
~MixedFilamentDialog();
MixedFilamentResult get_result() const { return m_result; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void build_ui();
wxBoxSizer* create_preview_panel();
wxBoxSizer* create_material_selection();
wxBoxSizer* create_ratio_slider();
wxBoxSizer* create_triangle_picker();
wxBoxSizer* create_gradient_section();
wxBoxSizer* create_recommendation_grid();
wxBoxSizer* create_button_panel();
void on_filament_changed();
void on_ratio_changed(int new_ratio_a);
void on_gradient_toggled();
void on_gradient_direction_changed();
void on_gradient_curve_changed();
void on_per_part_gradient_toggled();
void on_add_material();
void on_remove_material();
void on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b);
void on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c);
void apply_manual_ratio(size_t idx, int value);
void apply_dragged_triangle_ratio(int r0, int r1, int r2);
void reset_manual_ratio_state();
void refresh_ratio_labels();
void sync_triangle_weights_from_ratios();
void start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect);
void commit_ratio_editor(bool apply);
void commit_ratio_editor_from_background(wxMouseEvent& e);
void update_preview();
void update_ok_button_state();
void update_gradient_direction_items();
void update_component_count_ui();
// Picks dialog (width, height) based on current state so the gradient curve
// editor and the recommendation list stay visible at the same time.
wxSize compute_dialog_size() const;
void rebuild_all_combos();
void rebuild_recommendation_items();
void refresh_curve_editor_colors();
void paint_warning_panel(wxPaintEvent& evt);
wxBitmap make_swatch_bitmap(size_t idx);
// Reserves the same width on every material row label so the combo boxes line up.
static void apply_uniform_label_width(wxStaticText* lbl);
// Appends one "Filament N" label + combo row to m_material_rows_sizer. N follows the
// number of rows already there, so callers must not renumber anything themselves.
void append_material_row();
// Helpers for component/ratio access
size_t num_components() const { return m_result.components.size(); }
unsigned int comp(size_t i) const { return (i < m_result.components.size()) ? m_result.components[i] : 1; }
int ratio(size_t i) const { return (i < m_result.ratios.size()) ? m_result.ratios[i] : 0; }
wxColour comp_colour(size_t i) const;
MixedFilamentResult m_result;
bool m_edit_mode{false};
std::vector<std::string> m_physical_colors;
std::vector<std::string> m_physical_names;
std::vector<std::string> m_physical_types;
wxString m_type_mismatch_msg;
// Combo item index -> 1-based physical filament index (per combo)
std::vector<std::vector<unsigned int>> m_combo_to_physical;
// UI controls
wxPanel* m_preview_canvas{nullptr};
wxPanel* m_summary_panel{nullptr};
std::vector<ComboBox*> m_combo_filaments;
wxBoxSizer* m_material_rows_sizer{nullptr};
wxPanel* m_ratio_bar{nullptr};
wxPanel* m_triangle_panel{nullptr};
RatioLabelPanel* m_label_ratio_a{nullptr};
RatioLabelPanel* m_label_ratio_b{nullptr};
wxPanel* m_ratio_editor_panel{nullptr};
wxTextCtrl* m_ratio_editor{nullptr};
CheckBox* m_chk_gradient{nullptr};
wxStaticText* m_label_gradient{nullptr};
ComboBox* m_combo_gradient_dir{nullptr};
wxBoxSizer* m_gradient_sizer{nullptr};
GradientCurveEditor* m_curve_editor{nullptr};
wxBoxSizer* m_curve_sizer{nullptr};
CheckBox* m_chk_per_part_gradient{nullptr};
wxStaticText* m_label_per_part_gradient{nullptr};
wxBoxSizer* m_per_part_gradient_sizer{nullptr};
Button* m_btn_add_material{nullptr};
Button* m_btn_remove_material{nullptr};
Button* m_btn_ok{nullptr};
Button* m_btn_cancel{nullptr};
wxBoxSizer* m_warning_sizer{nullptr};
wxPanel* m_warning_panel{nullptr};
wxBoxSizer* m_ratio_sizer{nullptr};
wxBoxSizer* m_triangle_sizer{nullptr};
wxBoxSizer* m_right_sizer{nullptr};
wxScrolledWindow* m_recommendation_scroll{nullptr};
wxWrapSizer* m_recommendation_grid{nullptr};
// Drag state. The ratio bar and the triangle picker capture the mouse
// independently, so they must not share a flag: a mouse-up on one would
// otherwise clear the other's flag and skip its ReleaseMouse().
bool m_ratio_dragging{false};
bool m_tri_dragging{false};
std::vector<size_t> m_ratio_manual_order;
size_t m_ratio_editor_idx{0};
bool m_ratio_editor_committing{false};
wxWindow* m_ratio_editor_anchor{nullptr};
// Triangle picker drag point (barycentric weights)
double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334};
// Cached triangle color bitmap (invalidated when colors or size change)
wxBitmap m_tri_cache_bmp;
wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2;
wxSize m_tri_cache_size;
std::array<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr};
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_MixedFilamentDialog_hpp_
-1
View File
@@ -34,7 +34,6 @@
#include "Widgets/AxisCtrlButton.hpp"
#include "Widgets/TextInput.hpp"
#include "Widgets/StaticLine.hpp"
#include "wxMediaCtrl2.h"
#include "MediaPlayCtrl.h"
///////////////////////////////////////////////////////////////////////////
+4
View File
@@ -162,6 +162,8 @@ enum class NotificationType
//BBL: plugin install hint
BBLPluginInstallHint,
BBLFlushingVolumeZero,
// A mixed-color filament references a deleted component, or its components disagree in type.
BBLMixedFilamentBroken,
BBLPluginUpdateAvailable,
BBLPreviewOnlyMode,
BBLPrinterConfigUpdateAvailable,
@@ -172,6 +174,8 @@ enum class NotificationType
BBLBedFilamentIncompatible,
BBLMixUsePLAAndPETG,
BBLNozzleFilamentIncompatible,
// A mixed-color filament is printed on a single-nozzle printer (frequent changes and purging).
BBLSingleExtruderMixedFilamentRisk,
OrcaSharedProfilesAvailable,
OrcaCloudAPIError,
OrcaSyncConflict,
+133 -2
View File
@@ -6,6 +6,7 @@
#include <sstream>
#include <regex>
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include <future>
#include <glad/gl.h>
#include <boost/algorithm/string.hpp>
@@ -1675,6 +1676,25 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead.
{
auto& project_config = wxGetApp().preset_bundle->project_config;
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based;
for (int e : plate_extruders)
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
plate_extruders.clear();
for (unsigned int e : expanded)
plate_extruders.push_back((int)(e + 1));
}
}
return plate_extruders;
}
@@ -1836,6 +1856,24 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead.
{
auto* is_mixed_opt = full_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = full_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based;
for (int e : plate_extruders)
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
plate_extruders.clear();
for (unsigned int e : expanded)
plate_extruders.push_back((int)(e + 1));
}
}
return plate_extruders;
}
@@ -1889,6 +1927,25 @@ std::vector<int> PartPlate::get_extruders_without_support(bool conside_custom_gc
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead.
{
auto& project_config = wxGetApp().preset_bundle->project_config;
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based;
for (int e : plate_extruders)
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
plate_extruders.clear();
for (unsigned int e : expanded)
plate_extruders.push_back((int)(e + 1));
}
}
return plate_extruders;
}
@@ -1990,6 +2047,50 @@ bool PartPlate::check_tpu_printable_status(const DynamicPrintConfig & config, co
return true;
}
// A mixed-color filament alternates between its components constantly. On a single-nozzle
// printer every one of those switches is a full filament change plus a purge, so warn before
// slicing. Multi-nozzle printers keep the components loaded at once and are not affected.
bool PartPlate::check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const
{
warning_text.clear();
auto *nozzle_diameter_opt = config.option<ConfigOptionFloatsNullable>("nozzle_diameter");
if (!nozzle_diameter_opt || nozzle_diameter_opt->values.size() > 1)
return false;
auto *is_mixed_opt = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
if (!is_mixed_opt || !has_any_mixed_filament(is_mixed_opt->values))
return false;
auto is_mixed_slot = [&](int extruder_1based) {
size_t idx = (size_t)(extruder_1based - 1);
return idx < is_mixed_opt->values.size() && is_mixed_opt->values[idx];
};
const std::string mixed_warn_msg = _u8L("Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, "
"which may significantly increase waste and the risk of nozzle / waste-chute clogging.");
for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) {
if (!contain_instance_totally(obj_idx, 0))
continue;
ModelObject *mo = m_model->objects[obj_idx];
int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1;
if (is_mixed_slot(obj_ext)) {
warning_text = mixed_warn_msg;
return true;
}
for (ModelVolume *mv : mo->volumes) {
int vol_ext = mv->config.has("extruder") ? mv->config.extruder() : obj_ext;
if (is_mixed_slot(vol_ext)) {
warning_text = mixed_warn_msg;
return true;
}
}
}
return false;
}
bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config)
{
bool has_pla = false;
@@ -4445,8 +4546,6 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini
//this may be happened after machine changed
void PartPlateList::reset_size(int width, int depth, int height, bool reload_objects, bool update_shapes)
{
Vec3d origin1, origin2;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height;
if ((m_plate_width != width) || (m_plate_depth != depth) || (m_plate_height != height))
@@ -6386,6 +6485,31 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
}
//parse filament info
plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result());
// Record mixed (virtual) filaments actually used on this plate.
// Source is ToolOrdering::used_mixed_filaments (slots that appeared in
// layer tools before resolve), persisted on GCodeProcessorResult / Print —
// not print->extruders() which only reflects assignment.
{
std::vector<unsigned int> used_mixed;
if (auto *slice_result = m_plate_list[i]->get_slice_result())
used_mixed = slice_result->used_mixed_filaments;
if (used_mixed.empty() && print)
used_mixed = print->get_slice_used_mixed_filaments();
if (!used_mixed.empty() && print) {
const auto &fila_types = print->config().filament_type.values;
const auto &fila_colors = print->config().filament_colour.values;
const auto &fila_comps = print->config().filament_mixed_components.values;
for (unsigned int fid : used_mixed) {
PlateMixedFilamentInfo mixed_info;
mixed_info.id = (int) fid + 1;
if (fid < fila_types.size()) mixed_info.type = fila_types[fid];
if (fid < fila_colors.size()) mixed_info.color = fila_colors[fid];
if (fid < fila_comps.size()) mixed_info.components = fila_comps[fid];
plate_data_item->mixed_filaments_info.push_back(mixed_info);
}
}
}
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result()
<< ", result valid = " << m_plate_list[i]->is_slice_result_valid();
@@ -6452,6 +6576,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f
m_plate_list[index]->slice_filaments_info = plate_data_list[i]->slice_filaments_info;
gcode_result->warnings = plate_data_list[i]->warnings;
gcode_result->filament_maps = plate_data_list[i]->filament_maps;
gcode_result->used_mixed_filaments.clear();
for (const auto &mixed_info : plate_data_list[i]->mixed_filaments_info) {
if (mixed_info.id > 0)
gcode_result->used_mixed_filaments.push_back(static_cast<unsigned int>(mixed_info.id - 1));
}
if (Print *print = dynamic_cast<Print*>(fff_print))
print->set_slice_used_mixed_filaments(gcode_result->used_mixed_filaments);
// Reconstruct the device-side nozzle grouping from the loaded 3mf so
// the monitor/preview can map filaments to physical nozzles.
+3
View File
@@ -354,6 +354,9 @@ public:
bool check_filament_printable(const DynamicPrintConfig & config, wxString& error_message);
bool check_tpu_printable_status(const DynamicPrintConfig & config, const std::vector<int> &tpu_filaments);
bool check_mixture_of_pla_and_petg(const DynamicPrintConfig & config);
// Warns when a mixed-color filament is used on a single-nozzle printer, where every
// component switch costs a full filament change and purge.
bool check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const;
bool check_mixture_filament_compatible(const DynamicPrintConfig& config, std::string &error_msg);
bool check_compatible_of_nozzle_and_filament(const DynamicPrintConfig & config, const std::vector<std::string>& filament_presets, std::string& error_msg);
+25
View File
@@ -472,6 +472,31 @@ PlateSettingsDialog::PlateSettingsDialog(wxWindow* parent, const wxString& title
m_sizer_main->AddSpacer(FromDIP(5));
m_sizer_main->Add(m_other_layers_seq_panel, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30));
// A mixed-color slot resolves to a different physical filament per layer, so a user-defined
// filament order cannot be honoured; grey out the choice and explain that in the dialog.
{
auto &proj_cfg = wxGetApp().preset_bundle->project_config;
auto *is_mixed_opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed");
if (is_mixed_opt && Slic3r::has_any_mixed_filament(is_mixed_opt->values)) {
m_first_layer_print_seq_choice->Enable(false);
m_other_layers_seq_panel->enable_seq_choice(false);
auto *warn_sizer = new wxBoxSizer(wxHORIZONTAL);
auto *warn_icon = new wxStaticBitmap(this, wxID_ANY, create_scaled_bitmap("warning", this, 16),
wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
auto *warn_text = new wxStaticText(this, wxID_ANY,
_L("The filament list contains mixed filaments. Custom filament sequence will not take effect."));
warn_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00")));
warn_text->SetFont(Label::Body_12);
warn_text->Wrap(FromDIP(300));
warn_sizer->Add(warn_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
warn_sizer->Add(warn_text, 1, wxALIGN_CENTER_VERTICAL, 0);
m_sizer_main->AddSpacer(FromDIP(5));
m_sizer_main->Add(warn_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30));
}
}
auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"});
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](auto& e) {
+3
View File
@@ -62,6 +62,9 @@ public:
int get_layers_print_seq_choice() { return m_other_layer_print_seq_choice->GetSelection(); };
std::vector<LayerSeqInfo> get_layers_print_seq_infos() { return m_layer_seq_infos; }
// Lets callers grey out the sequence choice (e.g. when a mixed filament makes a
// user-defined filament order impossible).
void enable_seq_choice(bool enable) { m_other_layer_print_seq_choice->Enable(enable); }
protected:
void append_layer(const LayerSeqInfo* layer_info = nullptr);
+2113 -77
View File
File diff suppressed because it is too large Load Diff
+32 -2
View File
@@ -5,6 +5,7 @@
#include <vector>
#include <boost/filesystem/path.hpp>
#include <wx/colour.h>
#include <wx/panel.h>
// BBS
#include <wx/notebook.h>
@@ -86,6 +87,10 @@ using t_optgroups = std::vector <std::shared_ptr<ConfigOptionsGroup>>;
class Plater;
enum class ActionButtonType : int;
// Sentinel filament id meaning "use the slot the sidebar context menu was opened on"
// (Sidebar::priv::m_menu_filament_id) rather than an explicit index.
inline constexpr int kSidebarContextMenuFilamentId = -2;
#define EVT_PUBLISHING_START 1
#define EVT_PUBLISHING_STOP 2
@@ -188,7 +193,7 @@ public:
void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default
void change_filament(size_t from_id, size_t to_id); // 0 base
void edit_filament();
void add_custom_filament(wxColour new_col);
void add_custom_filament(wxColour new_col, const std::string& preset_name = std::string(), bool skip_preset_validation = false);
bool is_new_project_in_gcode3mf();
// BBS
void on_bed_type_change(BedType bed_type);
@@ -262,6 +267,20 @@ public:
std::vector<PlaterPresetComboBox*>& combos_filament();
void clear_combos_filament_badge();
void udpate_combos_filament_badge();
// Mixed-color filament sidebar section
void add_mixed_filament();
void edit_mixed_filament(size_t idx);
void delete_mixed_filament_at(size_t idx);
void decompose_filament_color(int filament_idx);
void recalc_filament_scroll_sizes();
void update_mixed_filament_list();
bool has_broken_mixed_filament() const;
bool has_broken_mixed_filament(const PartPlate* plate) const;
void collect_physical_filament_info(std::vector<std::string>& color_strs,
std::vector<std::string>& names,
std::vector<std::string>& types,
std::vector<size_t>* config_indices = nullptr);
Search::OptionsSearcher& get_searcher();
std::string& get_search_line();
void update_printer_thumbnail();
@@ -313,6 +332,11 @@ public:
const SLAPrint& sla_print() const;
SLAPrint& sla_print();
// Helper: returns config indices where filament_is_mixed == true
std::vector<size_t> mixed_filament_config_indices() const;
// Helper: returns config indices where filament_is_mixed == false
std::vector<size_t> physical_filament_config_indices() const;
int new_project(bool skip_confirm = false, bool silent = false, const wxString& project_name = wxString());
// BBS: save & backup
void load_project(wxString const & filename = "", wxString const & originfile = "-");
@@ -568,7 +592,7 @@ public:
void on_filament_change(size_t filament_idx);
void on_filament_count_change(size_t extruders_count);
void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1);
void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1, const std::vector<unsigned char>& is_mixed_before_delete = {});
std::vector<Slic3r::ColorRGBA> get_extruders_colors();
// BBS
void on_bed_type_change(BedType bed_type);
@@ -583,6 +607,12 @@ public:
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const;
std::vector<std::string> get_filament_colors_render_info() const;
std::vector<std::string> get_filament_color_render_type() const;
// Per slot, the colours a gradient mixed filament actually prints, sampled bottom (index 0)
// to top, so the sidebar, the paint gizmo and the extruder icons draw the same fade the
// editor previews rather than a straight blend of two endpoints. A slot that is not a
// gradient mixed filament gets an empty ramp. Cached; recomputed when the config changes.
const std::vector<std::vector<wxColour>>& get_filament_gradient_ramps() const;
std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const;
void set_global_filament_map_mode(FilamentMapMode mode);
+1 -1
View File
@@ -79,7 +79,7 @@ public:
Bind(wxEVT_LEFT_DOWN, &WikiLabel::OnLeftDown, this);
}
void SetLabel(const wxString& label)
void SetLabel(const wxString& label) override
{
m_label = label;
m_last_wrap_width = -1; // force re-wrap
+2 -2
View File
@@ -163,7 +163,7 @@ public:
BedType bedType() const { return m_BedType; }
virtual void init() override;
virtual std::map<std::string, std::string> extendedInfo() const
virtual std::map<std::string, std::string> extendedInfo() const override
{
return {{"bedType", std::to_string(static_cast<int>(m_BedType))},
{"timeLapse", std::to_string(m_timeLapse)},
@@ -200,7 +200,7 @@ public:
PrintHost* printhost);
virtual void init() override;
virtual std::map<std::string, std::string> extendedInfo() const;
virtual std::map<std::string, std::string> extendedInfo() const override;
private:
static constexpr const char* CONFIG_KEY_ENABLESELFTEST = "crealityprint_enable_self_test";
+2 -2
View File
@@ -662,10 +662,10 @@ PrinterFileSystem::File const &PrinterFileSystem::GetFile(size_t index, bool &se
void PrinterFileSystem::Attached()
{
boost::unique_lock lock(m_mutex);
m_recv_thread = std::move(boost::thread([w = weak_from_this()] {
m_recv_thread = boost::thread([w = weak_from_this()] {
boost::shared_ptr<PrinterFileSystem> s = w.lock();
if (s) s->RecvMessageThread();
}));
});
}
void PrinterFileSystem::Start()
-657
View File
@@ -1,657 +0,0 @@
/* bambusrc for gstreamer
* integration with proprietary Bambu Lab blob for getting raw h.264 video
*
* Copyright (C) 2023 Joshua Wise <joshua@accelerated.tech>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* Alternatively, the contents of this file may be used under the
* GNU Lesser General Public License Version 2.1 (the "LGPL"), in
* which case the following provisions apply instead of the ones
* mentioned above:
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <gst/gst.h>
#include "gstbambusrc.h"
#include <stdio.h>
#include <unistd.h>
#ifndef EXTERNAL_GST_PLUGIN
#define BAMBU_DYNAMIC
#endif
#include "BambuTunnel.h"
#ifdef BAMBU_DYNAMIC
// From PrinterFileSystem.
#ifdef __cplusplus
extern "C"
#else
extern
#endif
BambuLib *bambulib_get();
BambuLib *_lib = NULL;
#define BAMBULIB(x) (_lib->x)
#else
#define BAMBULIB(x) (x)
#endif
GST_DEBUG_CATEGORY_STATIC (gst_bambusrc_debug);
#define GST_CAT_DEFAULT gst_bambusrc_debug
static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS_ANY);
//GST_STATIC_CAPS("video/x-h264,framerate=0/1,parsed=(boolean)false,stream-format=(string)byte-stream"));
enum
{
PROP_0,
PROP_LOCATION,
};
static void gst_bambusrc_uri_handler_init (gpointer g_iface,
gpointer iface_data);
static void gst_bambusrc_finalize (GObject * gobject);
static void gst_bambusrc_dispose (GObject * gobject);
static void gst_bambusrc_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec);
static void gst_bambusrc_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec);
static GstStateChangeReturn gst_bambusrc_change_state (GstElement *
element, GstStateChange transition);
static GstFlowReturn gst_bambusrc_create (GstPushSrc * psrc,
GstBuffer ** outbuf);
static gboolean gst_bambusrc_start (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_stop (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_is_seekable (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_query (GstBaseSrc * bsrc, GstQuery * query);
static gboolean gst_bambusrc_unlock (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_unlock_stop (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_set_location (GstBambuSrc * src,
const gchar * uri, GError ** error);
#define gst_bambusrc_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstBambuSrc, gst_bambusrc, GST_TYPE_PUSH_SRC,
G_IMPLEMENT_INTERFACE (GST_TYPE_URI_HANDLER,
gst_bambusrc_uri_handler_init));
static void
gst_bambusrc_class_init (GstBambuSrcClass * klass)
{
GObjectClass *gobject_class;
GstElementClass *gstelement_class;
GstBaseSrcClass *gstbasesrc_class;
GstPushSrcClass *gstpushsrc_class;
gobject_class = (GObjectClass *) klass;
gstelement_class = (GstElementClass *) klass;
gstbasesrc_class = (GstBaseSrcClass *) klass;
gstpushsrc_class = (GstPushSrcClass *) klass;
gobject_class->set_property = gst_bambusrc_set_property;
gobject_class->get_property = gst_bambusrc_get_property;
gobject_class->finalize = gst_bambusrc_finalize;
gobject_class->dispose = gst_bambusrc_dispose;
g_object_class_install_property (gobject_class,
PROP_LOCATION,
g_param_spec_string ("location", "Location",
"URI to pass to Bambu Lab blobs", "",
(GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
gst_element_class_add_static_pad_template (gstelement_class, &srctemplate);
gst_element_class_set_static_metadata (gstelement_class, "Bambu Lab source",
"Source/Network",
"Receive data as a client over the network using the proprietary Bambu Lab blobs",
"Joshua Wise <joshua@accelerated.tech>");
gstelement_class->change_state =
GST_DEBUG_FUNCPTR (gst_bambusrc_change_state);
gstbasesrc_class->start = GST_DEBUG_FUNCPTR (gst_bambusrc_start);
gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (gst_bambusrc_stop);
gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR (gst_bambusrc_unlock);
gstbasesrc_class->unlock_stop =
GST_DEBUG_FUNCPTR (gst_bambusrc_unlock_stop);
gstbasesrc_class->is_seekable =
GST_DEBUG_FUNCPTR (gst_bambusrc_is_seekable);
gstbasesrc_class->query = GST_DEBUG_FUNCPTR (gst_bambusrc_query);
gstpushsrc_class->create = GST_DEBUG_FUNCPTR (gst_bambusrc_create);
GST_DEBUG_CATEGORY_INIT (gst_bambusrc_debug, "bambusrc", 0,
"Bambu Lab src");
}
static void
gst_bambusrc_reset (GstBambuSrc * src)
{
gst_caps_replace (&src->src_caps, NULL);
if (src->tnl) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
}
}
static void
gst_bambusrc_init (GstBambuSrc * src)
{
src->location = NULL;
src->tnl = NULL;
gst_base_src_set_automatic_eos (GST_BASE_SRC (src), FALSE);
gst_base_src_set_live(GST_BASE_SRC(src), TRUE);
gst_bambusrc_reset (src);
}
static void
gst_bambusrc_dispose (GObject * gobject)
{
GstBambuSrc *src = GST_BAMBUSRC (gobject);
GST_DEBUG_OBJECT (src, "dispose");
G_OBJECT_CLASS (parent_class)->dispose (gobject);
}
static void
gst_bambusrc_finalize (GObject * gobject)
{
GstBambuSrc *src = GST_BAMBUSRC (gobject);
GST_DEBUG_OBJECT (src, "finalize");
g_free (src->location);
if (src->tnl) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
}
G_OBJECT_CLASS (parent_class)->finalize (gobject);
}
static void
gst_bambusrc_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstBambuSrc *src = GST_BAMBUSRC (object);
switch (prop_id) {
case PROP_LOCATION:
{
const gchar *location;
location = g_value_get_string (value);
if (location == NULL) {
GST_WARNING ("location property cannot be NULL");
goto done;
}
if (!gst_bambusrc_set_location (src, location, NULL)) {
GST_WARNING ("badly formatted location");
goto done;
}
break;
}
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
done:
return;
}
static void
gst_bambusrc_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstBambuSrc *src = GST_BAMBUSRC (object);
switch (prop_id) {
case PROP_LOCATION:
g_value_set_string (value, src->location);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
int gst_bambu_last_error = 0;
static GstFlowReturn
gst_bambusrc_create (GstPushSrc * psrc, GstBuffer ** outbuf)
{
GstBambuSrc *src;
src = GST_BAMBUSRC (psrc);
(void) src;
GST_DEBUG_OBJECT (src, "create()");
int rv;
Bambu_Sample sample;
if (!src->tnl) {
return GST_FLOW_ERROR;
}
while ((rv = BAMBULIB(Bambu_ReadSample)(src->tnl, &sample)) == Bambu_would_block) {
GST_DEBUG_OBJECT(src, "create would block");
usleep(33333); /* 30Hz */
}
if (rv == Bambu_stream_end) {
return GST_FLOW_EOS;
}
if (rv != Bambu_success) {
gst_bambu_last_error = rv;
return GST_FLOW_ERROR;
}
#if GLIB_CHECK_VERSION(2,68,0)
gpointer sbuf = g_memdup2(sample.buffer, sample.size);
#else
gpointer sbuf = g_memdup(sample.buffer, sample.size);
#endif
*outbuf = gst_buffer_new_wrapped_full(0, sbuf, sample.size, 0, sample.size, sbuf, g_free);
/* Synthesize monotonic timestamps at the announced frame rate, anchored
* to the first frame's arrival time. The X1C's RTSPS server emits
* unreliable decode timestamps (wildly non-monotonic jumps, or sometimes
* none at all); forwarding them directly froze the pipeline after a few
* seconds. Pacing on a synthesized clock — the same trick mpv uses when
* it reports "No video PTS! Making something up." — gives smooth
* playback regardless of network jitter, and only drops late frames if
* the printer can't keep up. A snap-back resets the anchor if real
* arrival drifts more than two frame periods from the synthesized
* timeline (e.g. announced framerate was wrong).
*/
GstClock *clock = GST_ELEMENT_CLOCK(psrc);
GstClockTime base_time = gst_element_get_base_time((GstElement *)psrc);
GstClockTime running_now = GST_CLOCK_TIME_NONE;
if (clock) {
GstClockTime now = gst_clock_get_time(clock);
if (now != GST_CLOCK_TIME_NONE && now >= base_time)
running_now = now - base_time;
}
/* Adapt the period to actual inter-arrival time via EWMA. The announced
* frame_rate is unreliable on Bambu printers (X1C announces 30 but
* delivers ~28), so trusting it causes the synthesized timeline to drift
* relative to real time, which makes the sink consider frames late and
* skip pacing entirely. Measuring the real rate keeps PTS in step with
* arrival on average, so the sink can pace inside bursts while still
* tracking the printer's actual frame cadence.
*/
if (src->avg_period == 0) {
int fps = src->frame_rate > 0 ? src->frame_rate : 30;
src->avg_period = GST_SECOND / fps;
}
if (running_now != GST_CLOCK_TIME_NONE && src->last_arrival != 0) {
GstClockTimeDiff delta = GST_CLOCK_DIFF(src->last_arrival, running_now);
/* clamp to plausible video frame periods (5..200 ms) so a one-off
* burst-of-zero or long stall doesn't poison the average */
if (delta > 5 * GST_MSECOND && delta < 200 * GST_MSECOND) {
src->avg_period = (src->avg_period * 15 + (GstClockTime)delta) / 16;
}
}
src->last_arrival = (running_now != GST_CLOCK_TIME_NONE) ? running_now : src->last_arrival;
GstClockTime period = src->avg_period;
/* Lead time: schedule frames a few periods in the future of their
* arrival, so the sink has a small jitter buffer. Without this, frames
* arriving slightly later than expected land behind the running clock
* and the sink renders them immediately, producing visible stutter.
* 100ms is invisible for a live print-monitor view.
*/
const GstClockTime LEAD = 100 * GST_MSECOND;
if (!src->sttime) {
src->sttime = (running_now != GST_CLOCK_TIME_NONE) ? running_now + LEAD : LEAD;
src->frame_count = 0;
}
GstClockTime pts = src->sttime + src->frame_count * period;
/* Safety net: with the lead applied, expected drift is roughly -LEAD
* (pts sits LEAD ns ahead of running_now). Re-anchor only if the
* synthesized timeline diverges from that expectation by several frame
* periods, which indicates a real disturbance (printer paused, stream
* resumed, large fps change) rather than ordinary jitter.
*/
if (running_now != GST_CLOCK_TIME_NONE) {
GstClockTimeDiff drift = GST_CLOCK_DIFF(pts, running_now);
GstClockTimeDiff expected = -(GstClockTimeDiff)LEAD;
GstClockTimeDiff slack = (GstClockTimeDiff)(4 * period);
if (drift > expected + slack || drift < expected - slack) {
GST_DEBUG_OBJECT(src, "ts drift %" G_GINT64_FORMAT " ns; re-anchoring", drift);
src->sttime = running_now + LEAD;
src->frame_count = 0;
pts = src->sttime;
}
}
GST_BUFFER_PTS(*outbuf) = pts;
GST_BUFFER_DTS(*outbuf) = pts;
GST_BUFFER_DURATION(*outbuf) = period;
src->frame_count++;
GST_DEBUG_OBJECT(src,
"sttime:%lu, DTS:%lu, PTS: %lu~",
src->sttime, GST_BUFFER_DTS(*outbuf), GST_BUFFER_PTS(*outbuf));
return GST_FLOW_OK;
}
static void _log(void *ctx, int lvl, const char *msg) {
GstBambuSrc *src = (GstBambuSrc *) ctx;
GST_DEBUG_OBJECT(src, "bambu: %s", msg);
BAMBULIB(Bambu_FreeLogMsg)(msg);
}
static gboolean
gst_bambusrc_start (GstBaseSrc * bsrc)
{
GstBambuSrc *src = GST_BAMBUSRC (bsrc);
GST_DEBUG_OBJECT (src, "start(\"%s\")", src->location);
if (src->tnl) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
}
#ifdef BAMBU_DYNAMIC
if (!_lib) {
_lib = bambulib_get();
if (!_lib->Bambu_Open) {
return FALSE;
}
}
#endif
if (BAMBULIB(Bambu_Create)(&src->tnl, src->location) != Bambu_success) {
return FALSE;
}
int rv = 0;
BAMBULIB(Bambu_SetLogger)(src->tnl, _log, (void *)src);
if ((rv = BAMBULIB(Bambu_Open)(src->tnl)) != Bambu_success) {
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
gst_bambu_last_error = rv;
return FALSE;
}
int n = 0;
while ((rv = BAMBULIB(Bambu_StartStream)(src->tnl, 1 /* video */)) == Bambu_would_block) {
usleep(100000);
}
if (rv != Bambu_success) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
gst_bambu_last_error = rv;
return FALSE;
}
src->video_type = AVC1;
n = BAMBULIB(Bambu_GetStreamCount)(src->tnl);
GST_INFO_OBJECT (src, "Bambu_GetStreamCount returned stream count=%d",n);
for (int i = 0; i < n; ++i) {
Bambu_StreamInfo info;
BAMBULIB(Bambu_GetStreamInfo)(src->tnl, i, &info);
GST_INFO_OBJECT (src, "stream %d type=%d, sub_type=%d", i, info.type, info.sub_type);
if (info.type == VIDE) {
src->video_type = info.sub_type;
src->frame_rate = info.format.video.frame_rate;
GST_INFO_OBJECT (src, " width %d height=%d, frame_rate=%d",
info.format.video.width, info.format.video.height, info.format.video.frame_rate);
}
}
src->sttime = 0;
src->frame_count = 0;
src->last_arrival = 0;
src->avg_period = 0;
return TRUE;
}
static gboolean
gst_bambusrc_stop (GstBaseSrc * bsrc)
{
GstBambuSrc *src;
src = GST_BAMBUSRC (bsrc);
GST_DEBUG_OBJECT (src, "stop()");
if (src->tnl) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
}
return TRUE;
}
static GstStateChangeReturn
gst_bambusrc_change_state (GstElement * element, GstStateChange transition)
{
GstStateChangeReturn ret;
GstBambuSrc *src;
src = GST_BAMBUSRC (element);
(void) src;
switch (transition) {
case GST_STATE_CHANGE_READY_TO_NULL:
//gst_bambusrc_session_close (src);
break;
default:
break;
}
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
return ret;
}
/* Interrupt a blocking request. */
static gboolean
gst_bambusrc_unlock (GstBaseSrc * bsrc)
{
GstBambuSrc *src;
src = GST_BAMBUSRC (bsrc);
GST_DEBUG_OBJECT (src, "unlock()");
return TRUE;
}
/* Interrupt interrupt. */
static gboolean
gst_bambusrc_unlock_stop (GstBaseSrc * bsrc)
{
GstBambuSrc *src;
src = GST_BAMBUSRC (bsrc);
GST_DEBUG_OBJECT (src, "unlock_stop()");
return TRUE;
}
static gboolean
gst_bambusrc_is_seekable (GstBaseSrc * bsrc)
{
GstBambuSrc *src = GST_BAMBUSRC (bsrc);
(void) src;
return FALSE;
}
static gboolean
gst_bambusrc_query (GstBaseSrc * bsrc, GstQuery * query)
{
GstBambuSrc *src = GST_BAMBUSRC (bsrc);
gboolean ret;
GstSchedulingFlags flags;
gint minsize, maxsize, align;
switch (GST_QUERY_TYPE (query)) {
case GST_QUERY_URI:
gst_query_set_uri (query, src->location);
ret = TRUE;
break;
default:
ret = FALSE;
break;
}
if (!ret)
ret = GST_BASE_SRC_CLASS (parent_class)->query (bsrc, query);
switch (GST_QUERY_TYPE (query)) {
case GST_QUERY_SCHEDULING:
gst_query_parse_scheduling (query, &flags, &minsize, &maxsize, &align);
flags = (GstSchedulingFlags)((int)flags | (int)GST_SCHEDULING_FLAG_SEQUENTIAL);
gst_query_set_scheduling (query, flags, minsize, maxsize, align);
break;
default:
break;
}
return ret;
}
static gboolean
gst_bambusrc_set_location (GstBambuSrc * src, const gchar * uri,
GError ** error)
{
if (src->location) {
g_free (src->location);
src->location = NULL;
}
if (uri == NULL)
return FALSE;
src->location = g_strdup (uri);
return TRUE;
}
static GstURIType
gst_bambusrc_uri_get_type (GType type)
{
return GST_URI_SRC;
}
static const gchar *const *
gst_bambusrc_uri_get_protocols (GType type)
{
static const gchar *protocols[] = { "bambu", NULL };
return protocols;
}
static gchar *
gst_bambusrc_uri_get_uri (GstURIHandler * handler)
{
GstBambuSrc *src = GST_BAMBUSRC (handler);
/* FIXME: make thread-safe */
return g_strdup (src->location);
}
static gboolean
gst_bambusrc_uri_set_uri (GstURIHandler * handler, const gchar * uri,
GError ** error)
{
GstBambuSrc *src = GST_BAMBUSRC (handler);
return gst_bambusrc_set_location (src, uri, error);
}
static void
gst_bambusrc_uri_handler_init (gpointer g_iface, gpointer iface_data)
{
GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface;
iface->get_type = gst_bambusrc_uri_get_type;
iface->get_protocols = gst_bambusrc_uri_get_protocols;
iface->get_uri = gst_bambusrc_uri_get_uri;
iface->set_uri = gst_bambusrc_uri_set_uri;
}
static gboolean gstbambusrc_init(GstPlugin *plugin)
{
return gst_element_register(plugin, "bambusrc", GST_RANK_PRIMARY, GST_TYPE_BAMBUSRC);
}
#ifndef EXTERNAL_GST_PLUGIN
// for use inside of Bambu Slicer
void gstbambusrc_register()
{
static int did_register = 0;
if (did_register)
return;
did_register = 1;
gst_plugin_register_static(GST_VERSION_MAJOR, GST_VERSION_MINOR, "bambusrc", "Bambu Lab source", gstbambusrc_init, "0.0.1", "GPL", "BambuStudio", "BambuStudio", "https://github.com/bambulab/BambuStudio");
}
#else
#ifndef PACKAGE
#define PACKAGE "bambusrc"
#endif
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, bambusrc, "Bambu Lab source", gstbambusrc_init, "0.0.1", "GPL", "BambuStudio", "https://github.com/bambulab/BambuStudio")
#endif
-78
View File
@@ -1,78 +0,0 @@
/* bambusrc for gstreamer
* integration with proprietary Bambu Lab blob for getting raw h.264 video
*
* Copyright (C) 2023 Joshua Wise <joshua@accelerated.tech>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* Alternatively, the contents of this file may be used under the
* GNU Lesser General Public License Version 2.1 (the "LGPL"), in
* which case the following provisions apply instead of the ones
* mentioned above:
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#ifndef __GST_BAMBUSRC_H__
#define __GST_BAMBUSRC_H__
#include <gst/gst.h>
#include <gst/base/gstpushsrc.h>
#include <glib.h>
G_BEGIN_DECLS
#define GST_TYPE_BAMBUSRC (gst_bambusrc_get_type())
G_DECLARE_FINAL_TYPE (GstBambuSrc, gst_bambusrc,
GST, BAMBUSRC, GstPushSrc)
typedef void *Bambu_Tunnel;
struct _GstBambuSrc
{
GstPushSrc element;
GstCaps *src_caps;
gchar *location;
Bambu_Tunnel tnl;
GstClockTime sttime;
int video_type;
int frame_rate;
guint64 frame_count;
GstClockTime last_arrival;
GstClockTime avg_period;
};
extern void gstbambusrc_register();
G_END_DECLS
#endif /* __GST_BAMBUSRC_H__ */
+8 -4
View File
@@ -681,7 +681,7 @@ SearchDialog::SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindo
SearchDialog::~SearchDialog() {}
void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
void SearchDialog::Popup(wxWindow *focus /*= nullptr*/)
{
/* const std::string& line = searcher->search_string();
search_line->SetValue(line.empty() ? default_string : from_u8(line));
@@ -696,17 +696,19 @@ void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
search_line2->SetValue(wxString(""));
//const std::string &line = searcher->search_string();
//searcher->search(into_u8(line), true);
PopupWindow::Popup();
PopupWindow::Popup(focus);
search_line2->SetFocus();
update_list();
}
#ifdef __WXMSW__
void SearchDialog::MSWDismissUnfocusedPopup()
{
Dismiss();
OnDismiss();
}
#endif // __WXMSW__
void SearchDialog::OnDismiss() { }
@@ -926,7 +928,7 @@ SearchObjectDialog::SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* p
SearchObjectDialog::~SearchObjectDialog() {}
void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
void SearchObjectDialog::Popup(wxWindow *focus /*= nullptr*/)
{
if (m_is_dismissing || this->IsShown()) {
return;
@@ -937,7 +939,7 @@ void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
// dropdown list, otherwise the text input won't be usable
m_object_list->SetFocus();
#endif
PopupWindow::Popup();
PopupWindow::Popup(focus);
search_line2->SetFocus();
m_object_list->assembly_plate_object_name();
@@ -945,11 +947,13 @@ void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
update_list();
}
#ifdef __WXMSW__
void SearchObjectDialog::MSWDismissUnfocusedPopup()
{
Dismiss();
OnDismiss();
}
#endif // __WXMSW__
void SearchObjectDialog::OnDismiss() {}
+12 -8
View File
@@ -216,10 +216,12 @@ public:
SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *search_btn);
~SearchDialog();
void MSWDismissUnfocusedPopup();
void Popup(wxPoint position = wxDefaultPosition);
void OnDismiss();
void Dismiss();
#ifdef __WXMSW__
void MSWDismissUnfocusedPopup() override;
#endif // __WXMSW__
void Popup(wxWindow *focus = nullptr) override;
void OnDismiss() override;
void Dismiss() override;
void Die();
void msw_rescale();
@@ -260,10 +262,12 @@ public:
SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* parent, TextInput* input);
~SearchObjectDialog();
void MSWDismissUnfocusedPopup();
void Popup(wxPoint position = wxDefaultPosition);
void OnDismiss();
void Dismiss();
#ifdef __WXMSW__
void MSWDismissUnfocusedPopup() override;
#endif // __WXMSW__
void Popup(wxWindow *focus = nullptr) override;
void OnDismiss() override;
void Dismiss() override;
void Die();
void OnInputText(wxCommandEvent& event);
+28 -4
View File
@@ -4,6 +4,7 @@
#include "libslic3r/Utils.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/Color.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GUI_Preview.hpp"
@@ -482,7 +483,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater)
m_link_edit_nozzle->Bind(wxEVT_LEFT_DOWN, [this](auto &e) {
if (this && this->m_is_in_sending_mode) {
if (m_is_in_sending_mode) {
return;
}
@@ -2846,7 +2847,7 @@ void SelectMachineDialog::on_ok_btn(wxCommandEvent &event)
});
// STUDIO-9580
/* use warning color if there are warning and normal messages* /
/* use warning color if there are warning and normal messages*/
/* use indexes if there are several messages*/
/* add header and ending if there are several messages or has none block warnings*/
if (confirm_text.size() > 1 || !is_printing_block)
@@ -5693,10 +5694,15 @@ void SelectMachineDialog::clone_thumbnail_data() {
m_preview_colors_in_thumbnail.resize(m_materialList.size());
}
while (iter != m_materialList.end()) {
int id = iter->first;
Material * item = iter->second;
MaterialItem *m = item->item;
m_preview_colors_in_thumbnail[id] = m->m_material_coloul;
// Orca: key the preview colours by filament slot, as m_cur_colors_in_thumbnail and
// SyncAmsInfoDialog already do, so recompute_mixed_slot_colors() below can look a mixed
// slot's component colours up by id (BBS keys this array by list position).
if (item->id >= m_preview_colors_in_thumbnail.size()) {
m_preview_colors_in_thumbnail.resize(item->id + 1);
}
m_preview_colors_in_thumbnail[item->id] = m->m_material_coloul;
if (item->id < m_cur_colors_in_thumbnail.size()) {
m_cur_colors_in_thumbnail[item->id] = m->m_ams_coloul;
}
@@ -5706,6 +5712,20 @@ void SelectMachineDialog::clone_thumbnail_data() {
}
iter++;
}
// Expand color arrays to cover mixed (virtual) slots and compute their blended colors
const auto& cfg = wxGetApp().preset_bundle->project_config;
size_t total = 0;
if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed"))
total = opt->values.size();
size_t target = std::max(total, m_cur_colors_in_thumbnail.size());
if (m_cur_colors_in_thumbnail.size() < target)
m_cur_colors_in_thumbnail.resize(target);
if (m_preview_colors_in_thumbnail.size() < target)
m_preview_colors_in_thumbnail.resize(target);
recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg);
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
//copy data
auto &data = m_cur_input_thumbnail_data;
m_preview_thumbnail_data.reset();
@@ -5880,6 +5900,10 @@ void SelectMachineDialog::change_default_normal(int old_filament_id, wxColour te
return;
}
}
// Recompute mixed slot colors after physical slot color change
const auto& cfg = wxGetApp().preset_bundle->project_config;
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
ThumbnailData& data = m_cur_input_thumbnail_data;
ThumbnailData& no_light_data = m_cur_no_light_thumbnail_data;
if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) {
+1 -1
View File
@@ -522,7 +522,7 @@ public:
bool is_timeout();
int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path);
void set_print_type(PrintFromType type) {m_print_type = type;};
bool Show(bool show);
bool Show(bool show) override;
void show_init();
bool do_ams_mapping(MachineObject *obj_,bool use_ams);
bool get_ams_mapping_result(std::string& mapping_array_str, std::string& mapping_array_str2, std::string& ams_mapping_info) const;
+9 -11
View File
@@ -978,18 +978,16 @@ void SendToPrinterDialog::on_ok(wxCommandEvent &event)
m_send_job->on_check_ip_address_fail([this, token = std::weak_ptr(m_token)](int result) {
CallAfter([token, this] {
if (token.expired()) { return; }
if (this) {
SendFailedConfirm sfcDlg;
auto res = sfcDlg.ShowModal();
m_status_bar->cancel();
SendFailedConfirm sfcDlg;
auto res = sfcDlg.ShowModal();
m_status_bar->cancel();
if (res == wxYES) {
wxQueueEvent(m_button_ensure, new wxCommandEvent(wxEVT_BUTTON));
} else if (res == wxAPPLY) {
wxCommandEvent *evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
wxQueueEvent(this, evt);
wxGetApp().show_ip_address_enter_dialog();
}
if (res == wxYES) {
wxQueueEvent(m_button_ensure, new wxCommandEvent(wxEVT_BUTTON));
} else if (res == wxAPPLY) {
wxCommandEvent *evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
wxQueueEvent(this, evt);
wxGetApp().show_ip_address_enter_dialog();
}
});
});
+1 -1
View File
@@ -180,7 +180,7 @@ public:
SendToPrinterDialog(Plater *plater = nullptr);
~SendToPrinterDialog();
bool Show(bool show);
bool Show(bool show) override;
bool is_timeout();
void on_rename_click(wxCommandEvent& event);
void on_rename_enter();
+37 -37
View File
@@ -983,7 +983,7 @@ void PrintingTaskPanel::paint(wxPaintEvent&)
dc.DrawBitmap(m_thumbnail_bmp_display, wxPoint(0, 0));
}
dc.SetFont(Label::Body_12);
if (m_plate_index >= 0) {
wxString plate_id_str = wxString::Format("%d", m_plate_index);
dc.DrawText(plate_id_str, wxPoint(4, 4));
@@ -1271,7 +1271,7 @@ void PrintingTaskPanel::set_plate_index(int plate_idx)
}
void PrintingTaskPanel::market_scoring_show()
{
{
m_score_staticline->Show();
m_score_subtask_info->Show();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " show market scoring page";
@@ -1402,7 +1402,7 @@ void StatusBasePanel::init_bitmaps()
m_bitmap_fan_off = ScalableBitmap(this, "monitor_fan_off", 22);
m_bitmap_speed = ScalableBitmap(this, "monitor_speed", 24);
m_bitmap_speed_active = ScalableBitmap(this, "monitor_speed_active", 24);
m_thumbnail_brokenimg = ScalableBitmap(this, "monitor_brokenimg", 120);
m_thumbnail_sdcard = ScalableBitmap(this, "monitor_sdcard_thumbnail", 120);
//m_bitmap_camera = create_scaled_bitmap("monitor_camera", nullptr, 18);
@@ -1530,7 +1530,7 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
// media_ctrl_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
// media_ctrl_panel->SetBackgroundColour(*wxBLACK);
// wxBoxSizer *bSizer_monitoring = new wxBoxSizer(wxVERTICAL);
m_media_ctrl = new wxMediaCtrl2(this);
m_media_ctrl = new wxMediaCtrl3(this);
m_media_ctrl->SetMinSize(wxSize(PAGE_MIN_WIDTH, FromDIP(288)));
m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString);
@@ -2458,7 +2458,7 @@ StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, co
m_project_task_panel->get_pause_resume_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_pause_resume), NULL, this);
m_project_task_panel->get_abort_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_abort), NULL, this);
m_project_task_panel->get_market_scoring_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_scoring), NULL, this);
m_project_task_panel->get_market_retry_buttom()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this);
m_project_task_panel->get_market_retry_buttom()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this);
m_project_task_panel->get_clean_button()->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_print_error_clean), NULL, this);
m_setting_button->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this);
@@ -2520,7 +2520,7 @@ StatusPanel::~StatusPanel()
m_project_task_panel->get_pause_resume_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_pause_resume), NULL, this);
m_project_task_panel->get_abort_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_subtask_abort), NULL, this);
m_project_task_panel->get_market_scoring_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_scoring), NULL, this);
m_project_task_panel->get_market_retry_buttom()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this);
m_project_task_panel->get_market_retry_buttom()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_market_retry), NULL, this);
m_project_task_panel->get_clean_button()->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_print_error_clean), NULL, this);
m_setting_button->Disconnect(wxEVT_LEFT_DOWN, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this);
@@ -2566,7 +2566,7 @@ StatusPanel::~StatusPanel()
if (sdcard_hint_dlg != nullptr)
delete sdcard_hint_dlg;
if (m_score_data != nullptr) {
if (m_score_data != nullptr) {
delete m_score_data;
}
}
@@ -2590,7 +2590,7 @@ void StatusPanel::init_scaled_buttons()
m_bpButton_e_down_10->SetCornerRadius(FromDIP(12));
}
void StatusPanel::on_market_scoring(wxCommandEvent &event) {
void StatusPanel::on_market_scoring(wxCommandEvent &event) {
if (obj && obj->is_makeworld_subtask() && obj->rating_info && obj->rating_info->request_successful) { // model is mall model and has rating_id
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_market_scoring" ;
if (m_score_data && m_score_data->rating_id == obj->rating_info->rating_id) { // current score data for model is same as mall model
@@ -2599,7 +2599,7 @@ void StatusPanel::on_market_scoring(wxCommandEvent &event) {
int ret = m_score_dlg.ShowModal();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": old data";
if (ret == wxID_OK) {
if (ret == wxID_OK) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": old data is upload";
m_score_data->rating_id = -1;
m_project_task_panel->set_star_count_dirty(false);
@@ -2621,11 +2621,11 @@ void StatusPanel::on_market_scoring(wxCommandEvent &event) {
std::string comment = obj->rating_info->content;
if (!comment.empty()) { m_score_dlg.set_comment(comment); }
std::vector<std::string> images_json_array;
images_json_array = obj->rating_info->image_url_paths;
if (!images_json_array.empty()) m_score_dlg.set_cloud_bitmap(images_json_array);
int ret = m_score_dlg.ShowModal();
if (ret == wxID_OK) {
@@ -3651,14 +3651,14 @@ void StatusPanel::update_basic_print_data(bool def)
void StatusPanel::update_model_info()
{
auto get_subtask_fn = [this](BBLModelTask* subtask) {
CallAfter([this, subtask]() {
CallAfter([this, subtask]() {
if (obj && obj->subtask_id_ == subtask->task_id) {
obj->set_modeltask(subtask);
}
});
};
if (wxGetApp().getAgent() && obj) {
BBLSubTask* curr_task = obj->get_subtask();
if (curr_task) {
@@ -3982,7 +3982,7 @@ void StatusPanel::reset_printing_values()
m_project_task_panel->update_left_time(NA_STR);
m_project_task_panel->update_layers_num(true, wxString::Format(_L("Layer: %s"), NA_STR));
update_calib_bitmap();
task_thumbnail_state = ThumbnailState::PLACE_HOLDER;
m_start_loading_thumbnail = false;
m_load_sdcard_thumbnail = false;
@@ -4037,7 +4037,7 @@ bool StatusPanel::check_axis_z_at_home(MachineObject* obj)
}
void StatusPanel::on_axis_ctrl_z_up_10(wxCommandEvent &event)
{
{
if (obj) {
obj->command_axis_control("Z", 1.0, -10.0f, 900);
if (!check_axis_z_at_home(obj))
@@ -5396,7 +5396,7 @@ void StatusPanel::msw_rescale()
m_calibration_btn->Rescale();
m_options_btn->SetMinSize(wxSize(-1, FromDIP(26)));
m_options_btn->Rescale();
m_options_btn->Rescale();
m_safety_btn->SetMinSize(wxSize(-1, FromDIP(26)));
m_safety_btn->Rescale();
@@ -5578,11 +5578,11 @@ ScoreDialog::ScoreDialog(wxWindow *parent, ScoreData *score_data)
, m_upload_status_code(StatusCode::CODE_NUMBER)
{
m_tocken.reset(new int(0));
wxBoxSizer *m_main_sizer = get_main_sizer(score_data->local_to_url_image, score_data->comment_text);
m_image_url_paths = score_data->image_url_paths;
this->SetSizer(m_main_sizer);
Fit();
@@ -5598,16 +5598,16 @@ void ScoreDialog::on_dpi_changed(const wxRect &suggested_rect) {}
void ScoreDialog::OnBitmapClicked(wxMouseEvent &event)
{
wxStaticBitmap *clickedBitmap = dynamic_cast<wxStaticBitmap *>(event.GetEventObject());
if (m_image.find(clickedBitmap) != m_image.end()) {
if (m_image.find(clickedBitmap) != m_image.end()) {
if (!m_image[clickedBitmap].is_selected) {
for (auto panel : m_image[clickedBitmap].image_broad) {
for (auto panel : m_image[clickedBitmap].image_broad) {
panel->Show();
}
m_image[clickedBitmap].is_selected = true;
m_selected_image_list.insert(clickedBitmap);
} else {
for (auto panel : m_image[clickedBitmap].image_broad) {
panel->Hide();
for (auto panel : m_image[clickedBitmap].image_broad) {
panel->Hide();
}
m_image[clickedBitmap].is_selected = false;
m_selected_image_list.erase(clickedBitmap);
@@ -5624,9 +5624,9 @@ void ScoreDialog::OnBitmapClicked(wxMouseEvent &event)
}
std::set <std::pair<wxStaticBitmap * ,wxString>> ScoreDialog::add_need_upload_imgs()
{
{
std::set<std::pair<wxStaticBitmap *, wxString>> need_upload_images;
for (auto bitmap : m_image) {
for (auto bitmap : m_image) {
if (!bitmap.second.is_uploaded) {
wxString &local_image_path = bitmap.second.local_image_url;
if (!local_image_path.empty()) { need_upload_images.insert(std::make_pair(bitmap.first, local_image_path)); }
@@ -5646,7 +5646,7 @@ std::pair<wxStaticBitmap *, ScoreDialog::ImageMsg> ScoreDialog::create_local_thu
cur_image_msg.local_image_url = local_path;
cur_image_msg.img_url_paths = "";
cur_image_msg.is_uploaded = false;
wxStaticBitmap *imageCtrl = new wxStaticBitmap(this, wxID_ANY, wxBitmap(wxImage(local_path, wxBITMAP_TYPE_ANY).Rescale(FromDIP(80), FromDIP(60))), wxDefaultPosition,
wxDefaultSize, 0);
imageCtrl->Bind(wxEVT_LEFT_DOWN, &ScoreDialog::OnBitmapClicked, this);
@@ -5711,7 +5711,7 @@ void ScoreDialog::update_static_bitmap(wxStaticBitmap* static_bitmap, wxImage im
}
wxBoxSizer *ScoreDialog::create_broad_sizer(wxStaticBitmap *bitmap, ImageMsg& cur_image_msg)
{
{
// tb: top and bottom lr: left and right
auto m_image_tb_broad = new wxBoxSizer(wxVERTICAL);
auto line_top = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
@@ -5755,7 +5755,7 @@ void ScoreDialog::init() {
fail_image = wxImage(Slic3r::resources_dir() + "/images/oss_picture_load_failed.png", wxBITMAP_TYPE_ANY);
}
wxBoxSizer *ScoreDialog::get_score_sizer() {
wxBoxSizer *ScoreDialog::get_score_sizer() {
wxBoxSizer *score_sizer = new wxBoxSizer(wxHORIZONTAL);
wxStaticText *static_score_text = new wxStaticText(this, wxID_ANY, _L("Rate"), wxDefaultPosition, wxDefaultSize, 0);
static_score_text->Wrap(-1);
@@ -5878,18 +5878,18 @@ wxBoxSizer *ScoreDialog::get_photo_btn_sizer() {
for (int i = 0; i < filePaths.GetCount(); i++) { //It's ugly, but useful
bool is_repeat = false;
for (auto image : m_image) {
if (filePaths[i] == image.second.local_image_url) {
if (filePaths[i] == image.second.local_image_url) {
is_repeat = true;
continue;
}
}
if (!is_repeat) {
local_path.push_back(std::make_pair(filePaths[i], ""));
if (local_path.size() + m_image.size() > m_photo_nums) {
break;
if (local_path.size() + m_image.size() > m_photo_nums) {
break;
}
}
}
load_photo(local_path);
@@ -6007,7 +6007,7 @@ wxBoxSizer *ScoreDialog::get_button_sizer()
}
}
progress_dialog->Hide();
if (progress_dialog) {
if (progress_dialog) {
delete progress_dialog;
progress_dialog = nullptr;
}
@@ -6141,7 +6141,7 @@ wxBoxSizer *ScoreDialog::get_main_sizer(const std::vector<std::pair<wxString, st
m_main_sizer->Add(m_photo_sizer, 0, wxEXPAND | wxTOP, FromDIP(8));
m_image_sizer = new wxGridSizer(5, FromDIP(5), FromDIP(5));
if (!images.empty()) {
if (!images.empty()) {
load_photo(images);
}
m_main_sizer->Add(m_image_sizer, 0, wxEXPAND | wxLEFT, FromDIP(24));
@@ -6153,7 +6153,7 @@ wxBoxSizer *ScoreDialog::get_main_sizer(const std::vector<std::pair<wxString, st
return m_main_sizer;
}
ScoreData ScoreDialog::get_score_data() {
ScoreData ScoreDialog::get_score_data() {
ScoreData score_data;
score_data.rating_id = m_rating_id;
score_data.design_id = m_design_id;
@@ -6164,20 +6164,20 @@ ScoreData ScoreDialog::get_score_data() {
score_data.comment_text = m_comment_text->GetValue();
score_data.image_url_paths = m_image_url_paths;
for (auto img : m_image) { score_data.local_to_url_image.push_back(std::make_pair(img.second.local_image_url, img.second.img_url_paths)); }
return score_data;
}
void ScoreDialog::set_comment(std::string comment)
{
if (m_comment_text) {
if (m_comment_text) {
m_comment_text->SetValue(wxString::FromUTF8(comment));
}
}
void ScoreDialog::set_cloud_bitmap(std::vector<std::string> cloud_bitmaps)
{
{
m_image_url_paths = cloud_bitmaps;
for (std::string &url : cloud_bitmaps) {
if (std::string::npos == url.find(m_model_id)) continue;
+10 -11
View File
@@ -14,7 +14,6 @@
#include <wx/sizer.h>
#include <wx/gbsizer.h>
#include <wx/webrequest.h>
#include "wxMediaCtrl2.h"
#include "MediaPlayCtrl.h"
#include "AMSSetting.hpp"
#include "Calibration.hpp"
@@ -195,11 +194,11 @@ public:
void set_cloud_bitmap(std::vector<std::string> cloud_bitmaps);
protected:
enum StatusCode {
UPLOAD_PROGRESS = 0,
UPLOAD_EXIST_ISSUE,
enum StatusCode {
UPLOAD_PROGRESS = 0,
UPLOAD_EXIST_ISSUE,
UPLOAD_IMG_FAILED,
CODE_NUMBER
CODE_NUMBER
};
std::shared_ptr<int> m_tocken;
@@ -217,7 +216,7 @@ protected:
{
wxString local_image_url; //local image path
std::string img_url_paths; // oss url path
vector<wxPanel *> image_broad;
vector<wxPanel *> image_broad;
bool is_selected;
bool is_uploaded; // load
wxBoxSizer * image_tb_broad = nullptr;
@@ -252,7 +251,7 @@ protected:
std::set<std::pair<wxStaticBitmap *, wxString>> add_need_upload_imgs();
std::pair<wxStaticBitmap *, ImageMsg> create_local_thumbnail(wxString &local_path);
std::pair<wxStaticBitmap *, ImageMsg> create_oss_thumbnail(std::string &oss_path);
};
class PrintingTaskPanel : public wxPanel
@@ -261,7 +260,7 @@ public:
PrintingTaskPanel(wxWindow* parent, PrintingTaskType type);
~PrintingTaskPanel();
void create_panel(wxWindow* parent);
private:
MachineObject* m_obj{nullptr};
@@ -353,7 +352,7 @@ public:
void set_plate_index(int plate_idx = -1);
void market_scoring_show();
void market_scoring_hide();
public:
ScalableButton* get_abort_button() {return m_button_abort;};
ScalableButton* get_pause_resume_button() {return m_button_pause_resume;};
@@ -443,7 +442,7 @@ protected:
wxStaticBitmap* m_camera_switch_button;
wxMediaCtrl2 * m_media_ctrl;
wxMediaCtrl3 * m_media_ctrl;
MediaPlayCtrl * m_media_play_ctrl;
Label * m_staticText_printing;
@@ -567,7 +566,7 @@ protected:
virtual void on_bed_temp_kill_focus(wxFocusEvent &event) { event.Skip(); }
virtual void on_bed_temp_set_focus(wxFocusEvent &event) { event.Skip(); }
virtual void on_nozzle_temp_kill_focus(wxFocusEvent &event) { event.Skip(); }
virtual void on_nozzle_temp_set_focus(wxFocusEvent &event) { event.Skip(); }
virtual void on_nozzle_temp_set_focus(wxFocusEvent &event) { event.Skip(); }
virtual void on_nozzle_fan_switch(wxCommandEvent &event) { event.Skip(); }
virtual void on_printing_fan_switch(wxCommandEvent &event) { event.Skip(); }
virtual void on_axis_ctrl_z_up_10(wxCommandEvent &event) { event.Skip(); }
+31
View File
@@ -30,6 +30,7 @@
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevMapping.h"
#include "DeviceCore/DevStorage.h"
#include "FilamentBitmapUtils.hpp"
using namespace Slic3r;
using namespace Slic3r::GUI;
@@ -2575,6 +2576,10 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list()
m_materialList.clear();
m_filaments.clear();
// Mixed-color slots are virtual: they never occupy a tray, so they must not appear as
// AMS sync targets.
auto* is_mixed_opt = preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
bool use_double_extruder = get_is_double_extruder();
if (use_double_extruder) {
const auto &project_config = preset_bundle->project_config;
@@ -2592,6 +2597,8 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list()
auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]);
if (extruder >= materials.size() || extruder < 0 || extruder >= display_materials.size())
continue;
if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder])
continue;
if (contronal_index % SYNC_FLEX_GRID_COL == 0) {
wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL);
@@ -2793,6 +2800,10 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list()
m_fix_materialList.clear();
m_fix_filaments.clear();
// Mixed-color slots are virtual: they never occupy a tray, so they must not appear as
// AMS sync targets.
auto* is_mixed_opt = preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
bool use_double_extruder = get_is_double_extruder();
if (use_double_extruder) {
const auto &project_config = preset_bundle->project_config;
@@ -2810,6 +2821,8 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list()
auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]);
if (extruder >= extruders.size() || extruder < 0 || extruder >= m_ams_combo_info.ams_filament_colors.size())
continue;
if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder])
continue;
if (contronal_index % SYNC_FLEX_GRID_COL == 0) {
wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL);
@@ -2931,6 +2944,20 @@ void SyncAmsInfoDialog::clone_thumbnail_data()
iter++;
}
}
// Expand color arrays to cover mixed (virtual) slots and compute their blended colors
const auto& cfg = wxGetApp().preset_bundle->project_config;
size_t total = 0;
if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed"))
total = opt->values.size();
size_t target = std::max(total, m_cur_colors_in_thumbnail.size());
if (m_cur_colors_in_thumbnail.size() < target)
m_cur_colors_in_thumbnail.resize(target);
if (m_preview_colors_in_thumbnail.size() < target)
m_preview_colors_in_thumbnail.resize(target);
recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg);
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
// copy data
auto &data = m_cur_input_thumbnail_data;
m_preview_thumbnail_data.reset();
@@ -3119,6 +3146,10 @@ void SyncAmsInfoDialog::change_default_normal(int old_filament_id, wxColour temp
return;
}
}
// Recompute mixed slot colors after physical slot color change
const auto& cfg = wxGetApp().preset_bundle->project_config;
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
ThumbnailData &data = m_cur_input_thumbnail_data;
ThumbnailData &no_light_data = m_cur_no_light_thumbnail_data;
if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) {
+1 -1
View File
@@ -371,7 +371,7 @@ public:
};
FinishSyncAmsDialog(InputInfo &input_info);
~FinishSyncAmsDialog() override;
void deal_ok();
void deal_ok() override;
void update_info(InputInfo& info);
bool Layout() override;
+2
View File
@@ -21,7 +21,9 @@
#ifdef _WIN32
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <psapi.h>
#endif /* _WIN32 */
+36 -12
View File
@@ -4,6 +4,7 @@
#include "PresetHints.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
@@ -2173,18 +2174,25 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
//Orca: sync filament num if it's a multi tool printer
if (opt_key == "extruders_count" && !m_config->opt_bool("single_extruder_multi_material")){
auto num_extruder = boost::any_cast<size_t>(value);
int old_filament_size = wxGetApp().preset_bundle->filament_presets.size();
std::vector<std::string> new_colors;
for (int i = old_filament_size; i < num_extruder; ++i) {
wxColour new_col = Plater::get_next_color_for_filament();
std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
new_colors.push_back(new_color);
const size_t num_extruder = boost::any_cast<size_t>(value);
auto *bundle = wxGetApp().preset_bundle;
Sidebar &sidebar = wxGetApp().plater()->sidebar();
// A tool changer feeds filament N from nozzle N, so the extruder count sizes the physical
// run only; mixed slots are virtual and keep the tail. Go one slot at a time through the
// sidebar's own +/- calls: they insert ahead of the mixed tail and renumber filament ids,
// painted facets, custom g-code and mixed components, which a bulk resize clamps away.
// Both also refresh the print tab and export the selections, so nothing to do afterwards.
size_t physical = bundle->num_physical_filaments();
while (physical != num_extruder) {
if (physical < num_extruder)
sidebar.add_custom_filament(Plater::get_next_color_for_filament());
else
sidebar.delete_filament(physical - 1); // physical > num_extruder >= 1
const size_t updated = bundle->num_physical_filaments();
if (updated == physical)
break; // the call declined, e.g. the total slot limit - do not spin
physical = updated;
}
wxGetApp().preset_bundle->set_num_filaments(num_extruder, new_colors);
wxGetApp().plater()->on_filament_count_change(num_extruder);
wxGetApp().get_tab(Preset::TYPE_PRINT)->update();
wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config);
}
//Orca: disable purge_in_prime_tower if single_extruder_multi_material is disabled
@@ -2629,6 +2637,7 @@ void TabPrint::build()
auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height");
optgroup->append_single_option_line("layer_height","quality_settings_layer_height");
optgroup->append_single_option_line("initial_layer_print_height","quality_settings_layer_height");
optgroup->append_single_option_line("enable_mixed_color_sublayer");
optgroup = page->new_optgroup(L("Line width"), L"param_line_width");
optgroup->append_single_option_line("line_width","quality_settings_line_width");
@@ -3497,6 +3506,21 @@ void TabPrintModel::activate_selected_page(std::function<void()> throw_if_cancel
f->set_value(boost::any(), false);
}
}
if (m_type == Preset::TYPE_PLATE)
static_cast<TabPrintPlate *>(this)->update_mixed_filament_seq_state();
}
// A mixed-color slot resolves to a different physical filament per layer, so a
// user-defined filament print order cannot be honoured while one exists.
void TabPrintPlate::update_mixed_filament_seq_state()
{
if (!m_active_page) return;
auto &proj_cfg = m_preset_bundle->project_config;
auto *opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed");
bool has_mixed = opt && has_any_mixed_filament(opt->values);
toggle_option("first_layer_sequence_choice", !has_mixed);
toggle_option("other_layers_sequence_choice", !has_mixed);
}
void TabPrintModel::on_value_change(const std::string& opt_id, const boost::any& value)
@@ -7506,7 +7530,7 @@ void Tab::delete_preset()
for (auto &preset2 : *m_presets)
if (preset2.inherits() == current_preset.name) {
++count;
presets += "\n - " + preset2.name;
presets += "\n - " + from_u8(preset2.name);
}
if (count > 0) {
msg = _L("Presets inherited by other presets cannot be deleted!");
+4 -2
View File
@@ -515,13 +515,13 @@ public:
bool has_key(std::string const &key);
protected:
virtual void activate_selected_page(std::function<void()> throw_if_canceled);
virtual void activate_selected_page(std::function<void()> throw_if_canceled) override;
virtual void on_value_change(const std::string& opt_key, const boost::any& value) override;
virtual void notify_changed(ObjectBase * object) = 0;
virtual void reload_config();
virtual void reload_config() override;
virtual void update_custom_dirty(std::vector<std::string> &dirty_options, std::vector<std::string> &nonsys_options) override;
@@ -545,6 +545,8 @@ public:
void build() override;
void reset_model_config() override;
int show_spiral_mode_settings_dialog(bool is_object_config) { return m_config_manipulation.show_spiral_mode_settings_dialog(is_object_config); }
// Disables the user-defined filament print order while a mixed-color filament exists.
void update_mixed_filament_seq_state();
protected:
virtual void on_value_change(const std::string& opt_key, const boost::any& value) override;
+1 -1
View File
@@ -40,7 +40,7 @@ public:
void SetBitmap(ScalableBitmap &bitmap);
bool Enable(bool enable = true);
bool Enable(bool enable = true) override;
void Rescale();
+1 -4
View File
@@ -36,7 +36,6 @@ public:
TabButton* pageButton;
private:
wxWindow* m_parent;
wxFlexGridSizer* m_buttons_sizer;
wxBoxSizer* m_sizer;
ScalableBitmap m_arrow_img;
@@ -166,7 +165,7 @@ public:
return true;
}
bool RemovePage(size_t n)
bool RemovePage(size_t n) override
{
if (!wxBookCtrlBase::RemovePage(n))
return false;
@@ -400,8 +399,6 @@ private:
unsigned m_showTimeout,
m_hideTimeout;
TabButtonsListCtrl *m_ctrl{nullptr};
};
//#endif // _WIN32
#endif // slic3r_Tabbook_hpp_
File diff suppressed because it is too large Load Diff
+407
View File
@@ -0,0 +1,407 @@
#pragma once
#include "GUI_Utils.hpp"
#include "Widgets/ProgressDialog.hpp"
#include "libslic3r/TexturePainting.hpp"
#include <wx/sizer.h>
#include <wx/stattext.h>
#include "Widgets/PopupWindow.hpp"
#include <wx/panel.h>
#include <wx/scrolwin.h>
#include <wx/textctrl.h>
#include "Widgets/SpinInput.hpp"
#include <wx/checkbox.h>
#include <wx/button.h>
#include "Widgets/Button.hpp"
#include <wx/glcanvas.h>
#include <wx/event.h>
#include <array>
#include <atomic>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include <string>
class AccentSlider;
namespace Slic3r { namespace GUI {
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent);
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent);
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent);
wxDECLARE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent);
enum class TextureImportState {
Idle,
Computing,
Ready,
Error
};
enum class TextureAutoMixMode {
CMYW,
RYBW
};
enum class TextureFilamentKind {
ExistingPhysical,
ExistingMixed,
NewPhysical,
NewMixed
};
struct TextureFilamentEntry {
TextureFilamentKind kind{TextureFilamentKind::ExistingPhysical};
int dialog_index{-1};
size_t project_config_index{size_t(-1)};
std::string color_hex;
std::string name;
std::string type;
std::string preset_name;
std::vector<unsigned int> mixed_components;
std::vector<int> mixed_ratios;
};
struct TextureNewMixedFilament {
int dialog_index{-1};
std::string color_hex;
std::vector<int> component_dialog_indices;
std::vector<int> ratios;
};
struct FilamentMappingRow {
int cluster_id = -1;
std::array<std::size_t, 3> source_color = {0, 0, 0};
std::string source_hex;
int target_filament_idx = 0;
wxPanel* source_panel = nullptr;
wxPanel* target_panel = nullptr;
};
class FilamentSelectPopup;
class AutoMixSelectPopup;
// Lightweight 3D preview panel using wxGLCanvas.
// Renders: original textured, multi-color, or filament-mapped.
class TexturePreviewCanvas : public wxGLCanvas
{
public:
enum class RenderMode { Original, MultiColor, FilamentMap };
TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs);
~TexturePreviewCanvas();
void set_mesh_data(
const std::vector<std::array<float, 3>>& vertices,
const std::vector<std::array<int, 3>>& indices);
void set_texture_data(
const std::vector<std::array<float, 2>>& uvs,
const unsigned char* tex_data, int tex_w, int tex_h, int tex_channels);
void set_texture_render_data(
const std::vector<std::vector<unsigned char>>& tex_pixels_rgb,
const std::vector<int>& tex_widths,
const std::vector<int>& tex_heights,
const std::vector<std::array<std::array<float,2>, 3>>& face_uvs,
const std::vector<int>& face_tex_ids);
void set_painted_mesh_data(
const std::vector<std::array<float, 3>>& vertices,
const std::vector<std::array<int, 3>>& indices);
void set_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors);
void set_original_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors);
void set_filament_color_map(const std::map<std::array<std::size_t, 3>, std::array<float, 3>>& color_map);
void set_render_mode(RenderMode mode);
RenderMode get_render_mode() const { return m_mode; }
void set_computing_overlay(bool show);
void reset_view();
private:
void on_paint(wxPaintEvent& evt);
void on_size(wxSizeEvent& evt);
void on_mouse(wxMouseEvent& evt);
void ensure_gl_ready();
void render();
void render_mesh();
void render_textured_original();
void render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size);
void upload_reset_icon_textures();
unsigned int upload_reset_icon_texture(const std::string& icon_name);
wxRect reset_overlay_rect() const;
bool handle_reset_overlay_mouse(wxMouseEvent& evt);
void upload_textures();
void compute_smooth_normals();
void update_bounding_box();
wxGLContext* m_context = nullptr;
bool m_gl_initialized = false;
RenderMode m_mode = RenderMode::Original;
float m_zoom = 1.0f;
float m_rot_x = -30.0f;
float m_rot_y = 30.0f;
float m_pan_x = 0.0f;
float m_pan_y = 0.0f;
wxPoint m_last_mouse_pos;
enum class DragMode { None, Rotate, Pan };
DragMode m_drag_mode = DragMode::None;
std::vector<std::array<float, 3>> m_vertices;
std::vector<std::array<int, 3>> m_indices;
std::vector<std::array<float, 2>> m_uvs;
std::vector<std::array<float, 3>> m_painted_vertices;
std::vector<std::array<int, 3>> m_painted_indices;
std::vector<std::array<float, 3>> m_face_colors_rgb;
std::vector<std::array<float, 3>> m_original_face_colors_rgb;
std::vector<std::array<float, 3>> m_filament_colors_rgb;
std::map<std::array<std::size_t, 3>, std::array<float, 3>> m_color_map;
unsigned int m_tex_id = 0;
int m_tex_w = 0;
int m_tex_h = 0;
int m_tex_channels = 3;
bool m_tex_dirty = false;
std::vector<unsigned char> m_tex_data;
std::vector<unsigned int> m_gl_tex_ids;
std::vector<std::vector<unsigned char>> m_tex_pixels_rgb;
std::vector<int> m_tex_widths;
std::vector<int> m_tex_heights;
std::vector<std::array<std::array<float,2>, 3>> m_face_uvs;
std::vector<int> m_face_tex_ids;
bool m_multi_tex_dirty = false;
std::vector<std::array<float, 3>> m_vertex_normals;
std::array<float, 3> m_center = {0, 0, 0};
float m_radius = 1.0f;
unsigned int m_reset_icon_tex = 0;
unsigned int m_reset_icon_hover_tex = 0;
unsigned int m_reset_icon_dark_tex = 0;
unsigned int m_reset_icon_dark_hover_tex = 0;
bool m_reset_overlay_hovered = false;
bool m_reset_overlay_pressed = false;
};
class TextureImportDialog : public DPIDialog
{
public:
TextureImportDialog(wxWindow* parent,
const Slic3r::TexturedMesh& textured_mesh,
const std::vector<TextureFilamentEntry>& filament_entries,
std::function<bool()> initial_cancel_callback = {},
std::function<bool(int)> initial_progress_callback = {});
~TextureImportDialog();
int ShowModal() override;
void on_dpi_changed(const wxRect& suggested_rect) override;
Slic3r::PaintedMesh get_painted_mesh() const;
std::vector<Slic3r::FilamentMatch> get_matches() const;
bool was_skipped() const { return m_skipped; }
bool fallback_to_geometry_only() const { return m_fallback_to_geometry_only; }
// Colors of virtual filaments that need to be created after dialog confirmation.
// Index i corresponds to filament index (m_existing_filament_count + i).
const std::vector<std::array<float, 4>>& get_new_filament_colors() const { return m_new_filament_colors; }
const std::vector<std::string>& get_new_filament_preset_names() const { return m_new_filament_preset_names; }
const std::vector<TextureNewMixedFilament>& get_new_mixed_filaments() const { return m_new_mixed_filaments; }
const std::vector<TextureFilamentEntry>& get_filament_entries() const { return m_filament_entries; }
size_t get_existing_filament_count() const { return m_existing_filament_count; }
private:
void build_ui();
void build_preview_panel(wxWindow* parent, wxSizer* sizer);
void build_params_panel(wxWindow* parent, wxSizer* sizer);
void build_mapping_panel(wxWindow* parent, wxSizer* sizer);
void build_bottom_buttons(wxSizer* sizer);
void set_state(TextureImportState new_state);
void update_ui_for_state();
void start_computation(bool auto_color = false, bool initial = false);
void cancel_computation();
void on_computation_complete(wxCommandEvent& evt);
void on_computation_progress(wxCommandEvent& evt);
void on_computation_error(wxCommandEvent& evt);
void on_mesh_repair_decision_required(wxCommandEvent& evt);
void rebuild_mapping_rows();
void do_auto_match();
// Reorder m_current_matches into a canonical, predictable order (ascending
// filament_index, with unmapped entries pushed to the end). Used right
// after the initial computation so the first view the user sees has a
// stable, intuitive layout.
void sort_current_matches_by_filament_index();
// Reorder m_current_matches so they appear in the same order as
// `previous_matches` (keyed by cluster_index). Entries whose cluster_index
// was not present before are appended at the end, preserving their current
// relative order. Used when the user toggles auto-merge so the rows do not
// visually jump around. Assumes each cluster_index appears at most once in
// both vectors (this invariant is currently guaranteed by do_auto_match,
// which produces one match per cluster).
void restore_current_match_order(const std::vector<Slic3r::FilamentMatch>& previous_matches);
std::vector<Slic3r::FilamentMatch> build_matches_from_rows() const;
void update_filament_color_map();
void show_filament_popup(size_t row_index);
void dismiss_filament_popup();
void dismiss_filament_popup_on_wheel(wxMouseEvent& evt);
void show_auto_mix_popup();
void dismiss_auto_mix_popup();
void set_auto_mix_mode(TextureAutoMixMode mode);
void apply_auto_standard_mix(TextureAutoMixMode mode);
void reset_auto_mix();
void update_auto_mix_reset_visibility();
bool add_decomposed_mixed_filament(size_t row_index);
int add_virtual_filament(const std::array<float, 4>& rgba, const std::string& hex,
const std::string& preset_name = std::string());
int add_virtual_mixed_filament(const std::string& color_hex,
const std::vector<int>& component_dialog_indices,
const std::vector<int>& ratios);
size_t max_filament_count() const;
bool can_add_virtual_filament() const;
// Recomputes m_drop_warning_label visibility from m_filaments_dropped and
// m_state. Safe to call whether or not the label has been created yet.
// Visibility reflects ONLY the result of the most recent do_auto_match():
// if the latest match did not drop any cluster, the label is hidden even
// if a previous match had dropped (no historical accumulation).
void update_drop_warning_visibility();
void compact_used_virtual_filaments();
int find_closest_filament_index(const std::array<std::size_t, 3>& color) const;
// Returns a vector indexed by dialog_index whose value is the 1-based
// display number that mirrors the final sidebar ordering produced by
// apply_textured_mesh_import_result (Plater.cpp): ExistingPhysical,
// NewPhysical, ExistingMixed, NewMixed. Used so the dialog shows the
// same IDs the sidebar will show after OK, instead of the raw
// dialog_index + 1 (which interleaves physicals and mixeds).
// MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896).
std::vector<int> compute_display_numbers() const;
void on_color_preset_clicked(wxCommandEvent& evt);
void on_color_slider_changed(wxCommandEvent& evt);
void on_color_spin_changed(wxCommandEvent& evt);
void on_color_spin_text_changed(wxCommandEvent& evt);
void on_smooth_slider_changed(wxCommandEvent& evt);
void on_smooth_spin_changed(wxCommandEvent& evt);
void on_smooth_spin_text_changed(wxCommandEvent& evt);
void on_apply_clicked(wxCommandEvent& evt);
void on_auto_merge_toggled(wxCommandEvent& evt);
void highlight_view_button(int view_index);
void on_skip_clicked(wxCommandEvent& evt);
void on_ok_clicked(wxCommandEvent& evt);
void set_color_count_value(int value, bool update_spin);
void set_smooth_value(int value, bool update_spin);
void preview_spin_text_value(SpinInput* spin, AccentSlider* slider, int& param,
int min_value, int max_value, const wxString& text,
std::function<void()> on_value_changed = {});
void update_color_count_preset_buttons();
bool has_valid_result() const;
bool is_params_dirty() const;
void update_confirm_button_state();
void style_confirm_button(bool dirty);
Slic3r::TexturedMesh m_textured_mesh;
std::vector<std::string> m_filament_color_strs; // existing + virtual
std::vector<std::string> m_filament_names; // existing + virtual
std::vector<std::array<float, 4>> m_filament_colors_rgba; // existing + virtual
std::vector<TextureFilamentEntry> m_filament_entries; // aligned with m_filament_colors_rgba
size_t m_existing_filament_count = 0;
std::vector<std::array<float, 4>> m_new_filament_colors; // only virtual (to be created)
std::vector<std::string> m_new_filament_preset_names; // only virtual, aligned with m_new_filament_colors
std::vector<TextureNewMixedFilament> m_new_mixed_filaments;
std::string m_default_virtual_filament_preset_name;
TextureImportState m_state = TextureImportState::Idle;
bool m_skipped = false;
bool m_fallback_to_geometry_only = false;
// True iff *the most recent* do_auto_match() ran into the global filament
// limit and had to drop one or more clusters. Reset to false on every
// do_auto_match() entry so it never accumulates across runs: a run that
// does not drop anything must observe false here, regardless of whether
// previous runs dropped. Drives the inline orange warning above the
// bottom buttons; never affects the mapping itself.
bool m_filaments_dropped = false;
bool m_auto_merge_enabled = true;
TextureAutoMixMode m_auto_mix_mode = TextureAutoMixMode::CMYW;
int m_auto_mix_font_point_size = 10;
Slic3r::PaintedMesh m_painted;
std::vector<Slic3r::FilamentMatch> m_current_matches;
std::unique_ptr<std::thread> m_worker;
std::atomic<bool> m_cancel_flag{false};
std::mutex m_result_mutex;
Slic3r::PaintedMesh m_pending_result;
std::function<bool()> m_initial_cancel_callback;
std::function<bool(int)> m_initial_progress_callback;
bool m_current_computation_initial = false;
bool m_initial_computation_pending = false;
bool m_initial_computation_cancelled = false;
bool m_initial_computation_failed = false;
bool m_initial_tooltips_set = false;
bool m_current_computation_auto_color = false;
Slic3r::TexturePaintingSettings::MeshRepairDecision m_mesh_repair_decision =
Slic3r::TexturePaintingSettings::MeshRepairDecision::Ask;
Button* m_btn_color_4 = nullptr;
Button* m_btn_color_8 = nullptr;
Button* m_btn_color_16 = nullptr;
Button* m_btn_color_auto = nullptr;
AccentSlider* m_color_slider = nullptr;
SpinInput* m_color_spin = nullptr;
AccentSlider* m_smooth_slider = nullptr;
SpinInput* m_smooth_spin = nullptr;
Button* m_btn_apply = nullptr;
wxCheckBox* m_auto_merge_cb = nullptr;
Button* m_btn_auto_mix = nullptr;
Button* m_btn_mix_reset = nullptr;
bool m_auto_mix_applied = false;
AutoMixSelectPopup* m_auto_mix_popup = nullptr;
wxScrolledWindow* m_mapping_scroll = nullptr;
wxBoxSizer* m_mapping_sizer = nullptr;
std::vector<FilamentMappingRow> m_mapping_rows;
FilamentSelectPopup* m_filament_popup = nullptr;
int m_filament_popup_row = -1;
int m_skip_next_filament_popup_row = -1;
TexturePreviewCanvas* m_preview_canvas = nullptr;
wxPanel* m_tab_panel = nullptr;
Button* m_btn_view_original = nullptr;
Button* m_btn_view_multicolor = nullptr;
ProgressDialog* m_progress_dlg = nullptr;
Button* m_btn_skip = nullptr;
Button* m_btn_ok = nullptr;
wxStaticText* m_drop_warning_label = nullptr;
int m_param_color_count = 4;
int m_param_smooth = 5;
int m_applied_color_count = -1;
int m_applied_smooth = -1;
wxStaticText* m_hint_label = nullptr;
static const int ID_COLOR_4 = wxID_HIGHEST + 200;
static const int ID_COLOR_8 = wxID_HIGHEST + 201;
static const int ID_COLOR_16 = wxID_HIGHEST + 202;
static const int ID_COLOR_AUTO = wxID_HIGHEST + 203;
static const int ID_BTN_APPLY = wxID_HIGHEST + 204;
static const int ID_BTN_SKIP = wxID_HIGHEST + 205;
static const int ID_VIEW_ORIGINAL = wxID_HIGHEST + 206;
static const int ID_VIEW_MULTICOLOR = wxID_HIGHEST + 207;
wxDECLARE_EVENT_TABLE();
};
}} // namespace Slic3r::GUI
+1 -1
View File
@@ -343,7 +343,7 @@ public:
UnsavedChangesDialog(const wxString &caption, const wxString &header, DynamicConfig *config, int from, int to, bool left_to_right, NozzleVolumeType nozzle);
~UnsavedChangesDialog() override = default;
int ShowModal();
int ShowModal() override;
void build(Preset::Type type, PresetCollection *dependent_presets, const std::string &new_selected_preset, const wxString &header = "");
void update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header);
+3 -3
View File
@@ -984,7 +984,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
if (cans->get_ams_id() == std::to_string(VIRTUAL_TRAY_MAIN_ID) || cans->get_ams_id() == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
for (auto ifo : m_ext_info) {
if (ifo.ams_id == ams_id) {
cans->Update(ifo);
cans->UpdateInfo(ifo);
cans->show_sn_value(m_ams_model == AMSModel::AMS_LITE ? false : true);
}
}
@@ -992,7 +992,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
else{
for (auto ifo : m_ams_info) {
if (ifo.ams_id == ams_id) {
cans->Update(ifo);
cans->UpdateInfo(ifo);
cans->show_sn_value(m_ams_model == AMSModel::AMS_LITE ? false : true);
}
}
@@ -1015,7 +1015,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
std::string id = ams_prv.second->get_ams_id();
auto item = m_ams_item_list.find(id);
if (item != m_ams_item_list.end())
{ ams_prv.second->Update(item->second->get_ams_info());
{ ams_prv.second->UpdateInfo(item->second->get_ams_info());
}
}
}
+15 -23
View File
@@ -325,7 +325,7 @@ AMSrefresh::AMSrefresh(wxWindow *parent, std::string ams_id, wxString can_id, Ca
m_can_id = can_id.ToStdString();
create(parent, wxID_ANY, pos, size);
Update(ams_id, info);
UpdateInfo(ams_id, info);
}
AMSrefresh::AMSrefresh(wxWindow *parent, std::string ams_id, int can_id, Caninfo info, const wxPoint &pos, const wxSize &size) : AMSrefresh()
@@ -333,7 +333,7 @@ AMSrefresh::AMSrefresh(wxWindow *parent, std::string ams_id, int can_id, Caninfo
m_can_id = wxString::Format("%d", can_id).ToStdString();
create(parent, wxID_ANY, pos, size);
Update(ams_id, info);
UpdateInfo(ams_id, info);
}
AMSrefresh::~AMSrefresh()
@@ -482,7 +482,7 @@ void AMSrefresh::paintEvent(wxPaintEvent &evt)
dc.DrawText(m_refresh_id, pot);
}
void AMSrefresh::Update(std::string ams_id, Caninfo info)
void AMSrefresh::UpdateInfo(std::string ams_id, Caninfo info)
{
if (m_ams_id == ams_id && m_info == info)
{
@@ -945,7 +945,7 @@ AMSLib::AMSLib(wxWindow *parent, std::string ams_idx, Caninfo info, AMSModelOrig
Bind(wxEVT_LEAVE_WINDOW, &AMSLib::on_leave_window, this);
Bind(wxEVT_LEFT_DOWN, &AMSLib::on_left_down, this);
Update(info, ams_idx, false);
UpdateInfo(info, ams_idx, false);
}
AMSLib::~AMSLib()
@@ -1730,7 +1730,7 @@ void AMSLib::on_pass_road(bool pass)
}
}
void AMSLib::Update(Caninfo info, std::string ams_idx, bool refresh)
void AMSLib::UpdateInfo(Caninfo info, std::string ams_idx, bool refresh)
{
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return;
@@ -1868,7 +1868,7 @@ AMSRoad::AMSRoad(wxWindow *parent, wxWindowID id, Caninfo info, int canindex, in
void AMSRoad::create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size) { wxWindow::Create(parent, id, pos, size); }
void AMSRoad::Update(AMSinfo amsinfo, Caninfo info, int canindex, int maxcan)
void AMSRoad::UpdateInfo(AMSinfo amsinfo, Caninfo info, int canindex, int maxcan)
{
m_amsinfo = amsinfo;
m_info = info;
@@ -2083,9 +2083,6 @@ void AMSRoad::OnPassRoad(std::vector<AMSPassRoadMode> prord_list)
}
}
/*
/*************************************************
Description:AMSRoadUpPart
**************************************************/
@@ -2124,7 +2121,7 @@ void AMSRoadUpPart::create(wxWindow* parent, wxWindowID id, const wxPoint& pos,
Refresh();
}
void AMSRoadUpPart::Update(AMSinfo amsinfo)
void AMSRoadUpPart::UpdateInfo(AMSinfo amsinfo)
{
if (m_amsinfo != amsinfo)
{
@@ -2616,7 +2613,7 @@ void AMSPreview::Close()
Hide();
}
void AMSPreview::Update(AMSinfo amsinfo)
void AMSPreview::UpdateInfo(AMSinfo amsinfo)
{
if (m_amsinfo == amsinfo)
{
@@ -2954,7 +2951,7 @@ AMSHumidity::AMSHumidity(wxWindow* parent, wxWindowID id, AMSinfo info, const wx
}
});
Update(info);
UpdateInfo(info);
}
void AMSHumidity::create(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size) {
@@ -2963,7 +2960,7 @@ void AMSHumidity::create(wxWindow* parent, wxWindowID id, const wxPoint& pos, co
}
void AMSHumidity::Update(AMSinfo amsinfo)
void AMSHumidity::UpdateInfo(AMSinfo amsinfo)
{
if (m_amsinfo != amsinfo)
{
@@ -3380,7 +3377,7 @@ void AmsItem::AddLiteCan(Caninfo caninfo, int canindex, wxGridSizer* sizer)
//m_can_road_list[caninfo.can_id] = m_panel_road;
}
void AmsItem::Update(AMSinfo info)
void AmsItem::UpdateInfo(AMSinfo info)
{
if (m_info == info)
{
@@ -3392,7 +3389,7 @@ void AmsItem::Update(AMSinfo info)
if (m_humidity)
{
m_humidity->Update(m_info);
m_humidity->UpdateInfo(m_info);
}
for (int i = 0; i < m_can_count; i++) {
@@ -3401,7 +3398,7 @@ void AmsItem::Update(AMSinfo info)
auto refresh = it->second;
if (refresh != nullptr){
refresh->Update(info.ams_id, info.cans[i]);
refresh->UpdateInfo(info.ams_id, info.cans[i]);
refresh->Show();
}
}
@@ -3410,7 +3407,7 @@ void AmsItem::Update(AMSinfo info)
AMSLib* lib = m_can_lib_list[std::to_string(i)];
if (lib != nullptr){
if (i < m_can_count){
lib->Update(info.cans[i], info.ams_id);
lib->UpdateInfo(info.cans[i], info.ams_id);
lib->Show();
}
else{
@@ -3419,12 +3416,7 @@ void AmsItem::Update(AMSinfo info)
}
}
if (m_panel_road != nullptr){
m_panel_road->Update(m_info);
}
if (true || m_ams_model == AMSModel::GENERIC_AMS) {
/*m_panel_road->Update(m_info, info.cans[0]);
m_panel_road->Show();*/
m_panel_road->UpdateInfo(m_info);
}
Layout();
+7 -7
View File
@@ -312,7 +312,7 @@ public:
~AMSrefresh();
public:
void Update(std::string ams_id, Caninfo info);
void UpdateInfo(std::string ams_id, Caninfo info);
std::string GetCanId() const { return m_info.can_id; };
@@ -492,7 +492,7 @@ public:
AMSModel m_ams_model;
AMSModelOriginType m_ext_type = { AMSModelOriginType::GENERIC_EXT };
void Update(Caninfo info, std::string ams_idx, bool refresh = true);
void UpdateInfo(Caninfo info, std::string ams_idx, bool refresh = true);
void UnableSelected() { m_unable_selected = true; };
void EableSelected() { m_unable_selected = false; };
void OnSelected();
@@ -581,7 +581,7 @@ public:
double m_radius = {4};
wxColour m_road_def_color;
wxColour m_road_color;
void Update(AMSinfo amsinfo, Caninfo info, int canindex, int maxcan);
void UpdateInfo(AMSinfo amsinfo, Caninfo info, int canindex, int maxcan);
std::vector<ScalableBitmap> ams_humidity_img;
@@ -614,7 +614,7 @@ public:
void create(wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize);
public:
void Update(AMSinfo amsinfo);
void UpdateInfo(AMSinfo amsinfo);
void OnVamsLoading(bool load, wxColour col = AMS_CONTROL_GRAY500);
void SetPassRoadColour(wxColour col);
@@ -715,7 +715,7 @@ public:
void Open();
void Close();
void Update(AMSinfo amsinfo);
void UpdateInfo(AMSinfo amsinfo);
void create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size);
void OnEnterWindow(wxMouseEvent &evt);
void OnLeaveWindow(wxMouseEvent &evt);
@@ -768,7 +768,7 @@ public:
int m_canindex = { 0 };
bool m_selected = { false };
double m_radius = { 12 };
void Update(AMSinfo amsinfo);
void UpdateInfo(AMSinfo amsinfo);
std::vector<ScalableBitmap> ams_humidity_imgs;
std::vector<ScalableBitmap> ams_humidity_dark_imgs;
@@ -801,7 +801,7 @@ public:
AmsItem(wxWindow *parent, AMSinfo info, AMSModel model, AMSPanelPos pos);
~AmsItem();
void Update(AMSinfo info);
void UpdateInfo(AMSinfo info);
void create(wxWindow *parent);
void AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* sizer);
void AddLiteCan(Caninfo caninfo, int canindex, wxGridSizer* sizer);
+1 -1
View File
@@ -124,7 +124,7 @@ void AxisCtrlButton::SetInnerBackgroundColor(StateColor const& color)
void AxisCtrlButton::SetBitmap(ScalableBitmap &bmp)
{
if (&bmp && (& bmp.bmp()) && (bmp.bmp().IsOk())) {
if (bmp.bmp().IsOk()) {
m_icon = bmp;
}
}
+5 -4
View File
@@ -503,8 +503,8 @@ void Button::OnParentMotion(wxMouseEvent& event)
{
if (!tipWindow)
{
tipWindow = new wxTipWindow(this, tip);
tipWindow->Bind(wxEVT_DESTROY, [this](wxEvent& event) { this->tipWindow = nullptr;});
tipWindow = wxTipWindow::New(this, tip);
if (!tipWindow) return event.Skip();
tipWindow->Enable(false);
}
@@ -522,7 +522,8 @@ void Button::OnParentMotion(wxMouseEvent& event)
{
if (tipWindow)
{
delete tipWindow;
tipWindow->Dismiss();
tipWindow->Destroy();
tipWindow = nullptr;
}
}
@@ -543,7 +544,7 @@ void Button::OnParentLeave(wxMouseEvent& event)
if (!screen_rect.Contains(pos))
{
tipWindow->Dismiss();
delete tipWindow;
tipWindow->Destroy();
tipWindow = nullptr;
}
}
+2 -3
View File
@@ -3,6 +3,7 @@
#include "../wxExtensions.hpp"
#include "StaticBox.hpp"
#include <wx/tipwin.h>
class ButtonProps
{
@@ -27,9 +28,9 @@ enum class ButtonType{
Expanded , // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
};
class wxTipWindow;
class Button : public StaticBox
{
wxTipWindow::Ref tipWindow;
wxRect textSize;
wxSize minSize; // set by outer
wxSize paddingSize;
@@ -43,8 +44,6 @@ class Button : public StaticBox
bool isCenter = true;
bool vertical = false;
wxTipWindow* tipWindow = nullptr;
static const int buttonWidth = 200;
static const int buttonHeight = 50;
+26 -10
View File
@@ -87,10 +87,18 @@ void ComboBox::SetSelection(int n)
return;
drop.SetSelection(n);
SetLabel(drop.GetValue());
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk())
SetIcon(items[drop.selection].icon_textctrl);
else
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) {
if (m_keep_drop_arrow) {
SetIcon("drop_down");
SetIcon_1(items[drop.selection].icon_textctrl);
} else {
SetIcon(items[drop.selection].icon_textctrl);
}
} else {
SetIcon("drop_down");
if (m_keep_drop_arrow)
SetIcon_1(wxNullBitmap);
}
if (drop.selection >= 0) {
SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap);
@@ -120,10 +128,18 @@ void ComboBox::SetValue(const wxString &value)
{
drop.SetValue(value);
SetLabel(value);
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk())
SetIcon(items[drop.selection].icon_textctrl);
else
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) {
if (m_keep_drop_arrow) {
SetIcon("drop_down");
SetIcon_1(items[drop.selection].icon_textctrl);
} else {
SetIcon(items[drop.selection].icon_textctrl);
}
} else {
SetIcon("drop_down");
if (m_keep_drop_arrow)
SetIcon_1(wxNullBitmap);
}
if (drop.selection >= 0) {
SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap);
@@ -192,7 +208,7 @@ bool ComboBox::SetFont(wxFont const& font)
int ComboBox::Append(const wxString &item, const wxBitmap &bitmap, int style)
{
if (&bitmap && bitmap.IsOk()) {
if (bitmap.IsOk()) {
return Append(item, bitmap, nullptr, style);
}
return Append(item, wxNullBitmap, nullptr, style);
@@ -203,7 +219,7 @@ int ComboBox::Append(const wxString &text,
void * clientData,
int style)
{
if (&bitmap && bitmap.IsOk()) {
if (bitmap.IsOk()) {
return Append(text, bitmap, wxString{}, clientData, style);
}
return Append(text, wxNullBitmap, wxString{}, clientData, style);
@@ -221,7 +237,7 @@ int ComboBox::Append(const wxString &text,
void *clientData,
int style)
{
auto valid_bit_map = (&bitmap && bitmap.IsOk()) ? bitmap : wxNullBitmap;
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
item.style = style;
items.push_back(item);
@@ -317,7 +333,7 @@ wxBitmap ComboBox::GetItemBitmap(unsigned int n) { return items[n].icon; }
void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
{
if (n >= items.size()) return;
items[n].icon = (&bitmap && bitmap.IsOk()) ? bitmap : wxNullBitmap;
items[n].icon = bitmap.IsOk() ? bitmap : wxNullBitmap;
drop.Invalidate();
}
+6
View File
@@ -16,6 +16,7 @@ class ComboBox : public wxWindowWithItems<TextInput, wxItemContainer>
bool drop_down = false;
bool text_off = false;
bool is_replace_text_to_image = false;
bool m_keep_drop_arrow = false; // When true, item icon goes to icon_1, keeping drop_down arrow
wxString replace_text;
wxString image_for_text;
@@ -31,6 +32,11 @@ public:
DropDown & GetDropDown() { return drop; }
// When true, item icon is shown as icon_1 (secondary), preserving drop_down arrow.
// Note: item bitmaps are set via raw wxBitmap (not ScalableBitmap), so they won't
// auto-rescale on DPI change. Caller should recreate items after DPI change.
void SetKeepDropArrow(bool keep) { m_keep_drop_arrow = keep; }
virtual bool SetFont(wxFont const & font) override;
public:
+4 -1
View File
@@ -427,7 +427,10 @@ void DropDown::render(wxDC &dc)
}
pt.y += (rcContent.height - textSize.y) / 2;
dc.SetFont(GetFont());
dc.SetTextForeground(text_color.colorForStates(states2));
// Dimmed items stay selectable, so they only borrow the disabled text tone rather
// than taking the disabled state itself.
const int text_states = (item.style & DD_ITEM_STYLE_DIMMED) ? (states2 & ~StateColor::Enabled) : states2;
dc.SetTextForeground(text_color.colorForStates(text_states));
dc.DrawText(text, pt);
if (group.IsEmpty() && !item.group_key.IsEmpty()) {
auto szBmp = arrow_bitmap.GetBmpSize();
+1
View File
@@ -13,6 +13,7 @@
#define DD_ITEM_STYLE_SPLIT_ITEM 0x0001 // ----text----, text with horizontal line arounds
#define DD_ITEM_STYLE_DISABLED 0x0002 // ----text----, text with horizontal line arounds
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
+2 -1
View File
@@ -98,7 +98,7 @@ void LabeledStaticBox::SetBorderColor(StateColor const &color)
Refresh();
}
void LabeledStaticBox::SetFont(wxFont set_font)
bool LabeledStaticBox::SetFont(const wxFont &set_font)
{
m_font = set_font;
@@ -109,6 +109,7 @@ void LabeledStaticBox::SetFont(wxFont set_font)
m_label_width = tW;
Refresh();
return true;
}
bool LabeledStaticBox::Enable(bool enable)
+1 -1
View File
@@ -42,7 +42,7 @@ public:
void SetBorderColor(StateColor const &color);
void SetFont(wxFont set_font);
bool SetFont(const wxFont &set_font) override;
bool Enable(bool enable) override;
+7 -7
View File
@@ -630,7 +630,7 @@ NozzleListTable::NozzleListTable(wxWindow* parent) : wxPanel(parent,wxID_ANY,wxD
SetSizer(sizer);
Layout();
m_web_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this,sizer](wxWebViewEvent& evt) {
m_web_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) {
std::string message = evt.GetString().ToStdString();
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << "Received message: " << message;
try {
@@ -1168,8 +1168,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr<DevNozzleRack> rack, bool
m_cancel_btn->SetLabel(_L("Ignore"));
m_confirm_btn->SetLabel(_L("Refresh"));
m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, ignore_opt](auto& e) {ignore_opt(); });
m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); });
m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, ignore_opt](auto& e) {ignore_opt(); });
m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); });
}
else if (has_unknown) {
m_cancel_btn->Show();
@@ -1178,8 +1178,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr<DevNozzleRack> rack, bool
m_cancel_btn->SetLabel(_L("Ignore"));
m_confirm_btn->SetLabel(_L("Refresh"));
m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, ignore_opt](auto& e) {ignore_opt(); });
m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); });
m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, ignore_opt](auto& e) {ignore_opt(); });
m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); });
}
else if (has_unreliable) {
m_cancel_btn->Show();
@@ -1188,8 +1188,8 @@ void MultiNozzleSyncDialog::UpdateButton(std::weak_ptr<DevNozzleRack> rack, bool
m_cancel_btn->SetLabel(_L("Refresh"));
m_confirm_btn->SetLabel(_L("Confirm"));
m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, refresh_cmd](auto& e) {refresh_cmd(); });
m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [this, rack, trust_cmd](auto& e) {trust_cmd(); });
m_cancel_btn->Bind(wxEVT_LEFT_DOWN, [rack, refresh_cmd](auto& e) {refresh_cmd(); });
m_confirm_btn->Bind(wxEVT_LEFT_DOWN, [rack, trust_cmd](auto& e) {trust_cmd(); });
}
else {
+1 -1
View File
@@ -167,7 +167,7 @@ class MultiNozzleSyncDialog : public DPIDialog
{
public:
MultiNozzleSyncDialog(wxWindow* parent, std::weak_ptr<DevNozzleRack> rack);
virtual void on_dpi_changed(const wxRect& suggested_rect) {};
virtual void on_dpi_changed(const wxRect& suggested_rect) override {};
std::vector<NozzleOption> GetNozzleOptions(const std::vector<MultiNozzleUtils::NozzleGroupInfo>& group_infos);
std::optional<NozzleOption> GetSelectedOption() {
+1 -1
View File
@@ -56,7 +56,7 @@ protected:
void paintEvent(wxPaintEvent &evt);
void render(wxDC &dc);
void doRender(wxDC &dc);
virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO);
virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override;

Some files were not shown because too many files have changed in this diff Show More